Sunday, October 24, 2021

V2EX - 技术

V2EX - 技术


深入理解 android 包体积优化,给 apk 瘦身全部技巧

Posted: 24 Oct 2021 04:21 AM PDT

前言

随着 iphone13p 最大内存放大到了 1T ,大内存手机的时代悄然降临,在 android 里面,三星也有,罗老师几年前说:如果我告诉你们我们在做 1T 的手机,你们可能以为我疯了

看看现在,估计未来会有更多手机有 1T 版,大家开始真香了。

但是,如果现在有人说:要做一个 1T 大小的 app ,那他可能是真疯了,至少未来十年不可能。因为手机内存是越大越好,你一个 app 当然是能小就小呀

Android app 的文件格式为 apk ,本文就是探讨对于一个 android apk ,有哪些方法可以减小体积

Apk 组成

要想减小体积,首先我们需要了解 apk 的构成

373c7fa912fa93d601a2bee46c76ae2d.jpg

  • 我们写的.java 文件会被编译为.class 文件,再由 dx 工具编译为 Classes.dex 文件,由于 android 限制,每个 dex 文件最多 65535 个方法,所以多出来的方法就生成 Classes2.dex , Classes3.dex~ClassesN.dex

  • Resource(res)与 Assets 比较像,区别是 res 目录下会生成资源 ID ,并在.R 文件中记录,可以直接使用,这里平常我们用得很多,而 assets 不会有 ID ,而是通过 AssetManager 接口获取;

    所以 res 类似于我们的桌面,一般放我们要操纵的控件资源,而 assets 类似于桌下的抽屉,放诸如数据库,html 这类资源

  • Native Libraries 平时打交道少,优化空间也很有限

上面是抽象的 apk 结构,下面我们看一个实际的

将 qq.apk 拖入 android studio

image-20211023160756347

可以看到最大的 R 文件夹,点进去,都是一些图片,第二大的是 assets ,里面是一些表情包以及插件图片

其他的我们刚刚也说过,值得注意的是,里面多了一个 META-INF

他存放了应用的签名信息,其中

  • .MF: 每一个资源都有一个 SHA1 签名,存放在这里

  • .SF: 文件存放.MF 经过 base64 编码后的签名

  • .RSA: 对.SF 文件使用 SHA1 算法生成数字摘要(注意:.MF 中是对每一个资源进行 SHA1 ,这里是对文件),然后进行 RSA 加密,再用开发者私钥进行签名,安装时使用公钥解密

这样子,一个 app 安装在手机时,解密这一数字摘要,然后与内部的.MF 文件比对,如果相符,证明资源内容没有被修改

Dex 文件

在 APK 组成中我们可以看到,占用内存最大的是 res ,assets 与 classs.dex 文件,这也是我们的优化方向,接下来,我们看看如何优化 dex

首先我们看看 dex 的结构

undefined

更详细的版本在官网,这里如果对这些结构的作用有兴趣,可以看下图的详细版本

image-20211023162712238

ProGuadrd

dex 是代码编译而来,而对于代码文件,最重要的优化就是混淆了,将方法名,属性名等变为又短又无意义的名字,不仅能缩小体积还能避免反编译被人破解

在 IDE 中,我们可以看到 qq 里面的类都是小写字母,里面的变量和方法都按字母顺序排列了,从 a 开始

image-20211023163108352

除了修改变量名,ProGuadrd 还可以在功能等价的基础上重写代码,比如把多个函数调用写到一个函数里面去,更加增大了阅读理解难度(虽然初学者一般已经这样做了),以及打乱格式,增加空格等

主要步骤如下

  • 压缩( Shrink ): 检测和删除没有使用的类,字段,方法和特性。

  • 优化( Optimize ) : 分析和优化 Java 字节码。

  • 混淆( Obfuscate ): 使用简短的无意义的名称,对类,字段和方法进行重命名。

  • 预检( Preveirfy ): 用来对 Java class 进行预验证(预验证主要是针对 JME 开发来说的,Android 中没有预验证过程,默认是关闭)。

D8 与 R8 优化

这两平时接触不多,他们主要是在字节码处做优化的,开发时感知不强(感觉就是用来面试的)

D8 主要是在编译字节码时重排序,将占用空间变得更小,比如对于 greetingType 方法,正常编译后的结果是

[000584] Main.greetingType:(LGreeting;)Ljava/lang/String; 0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I 0002: invoke-virtual {v2}, LGreeting;.ordinal:()I 0005: move-result v1 0006: aget v0, v0, v1 0008: packed-switch v0, 00000017  // 这里 

如果使用 D8 优化,编译后的结果

[0005f0] Main.greetingType:(LGreeting;)Ljava/lang/String; 0000: sget-object v0, LMain$1;.$SwitchMap$Greeting:[I 0002: invoke-virtual {v1}, LGreeting;.ordinal:()I 0005: move-result v1 0006: aget v0, v0, v1 -0008: packed-switch v0, 00000017  //  这里 +0008: const/4 v1, #int 1 +0009: if-eq v0, v1, 0014 +000b: const/4 v1, #int 2 +000c: if-eq v0, v1, 0017 

可以看到 0008 处后的几条指令有变化,多了几个 if ,对于不同的 case 做创建不同的变量,可以节省空间

R8 也类似,只是策略有些不一样

更详细的了解可以参考 D8 Optimizations

总之,他们的作用是就是,在不改变功能的情况下,重写部分 class 指令,减小空间占用,但是有可能会增加指令数量

Redex 优化

Redex 是 Facebook 推出的一个优化 Dex 文件的工具,和 D8R8 一样,也是对字节码的处理,有以下效果

  1. 内联函数,减少调用
  2. 删除无用代码
  3. 将只有一个实现类的接口或者父类用实现类代替
  4. 字符串混淆所见

……

不过这个我没用过,但是感觉 Proguard 与 D8R8 都多多少少能做到,可能是他在细节上用了更好的算法

但是不管多少框架,对 dex 文件的优化说来说去也就这些

移除多余的库与代码

最后是移除第三方库和冗余代码,属于业务逻辑上的原因

  • 多余的库

    对于自己的小项目,还好,对于多人参与的大型项目,很有可能对同一个功能,不同的人用了不同的轮子,手 Q 里面就有,比如要写单测,之前使用 Powermock ,后来用 JMock ,再后来改为 Mockk ,一个项目,三个单测框架

    由于不同的单测框架已经写了不少单测,短时间移除是不太可能的,但是可以慢慢转为同一种单测框架

  • 多余代码

    Android studio 会自己检测,没有用过的会置位灰色提醒,但是会漏掉很多,通过插件 Lint 可以检测,

资源清理

上面都是在代码层面减小 dex ,apk 的另一个空间占用大户,是资源,尤其是其中的图片,

图片,你可知道,多少 OOM 因你而起?多少 app 因你闪退?

图片压缩与更换格式

我们先看看图片为什么那么大

图片的显示,有 ARGB 4 个通道,其中默认的显示模式是 ARGB8888 ,ARGB8888 表示每个通道的颜色区间为[0,255],也就是两个 16 进制数表示,也就是 8bit -> 1 字节

所以 ARGB8888 模式下,一个像素 4 个通道下占用 4 字节,一张 1024*1024 的手机图片图片,就是 $$ 2^{10} * 2^{10} * 2^2 = 2^{22} = 4M $$ 一张图 4M ,太离谱了!

上面是打开后在运存的占用,我们可以修改颜色通道,不然 ARGB565 来减小单个像素所占用运存,不过有点跑题,本篇我们讲的是 app 的大小,也就是所占用手机的内存(我们约定 手机运存 = 电脑内存,手机内存 = 电脑硬盘)

内存与运存中的图片存在形式是不一样的,压缩方法也不一样,很多人容易弄混

回到内存,内存中,图片是以 png ,jpg 等格式存储

我之前开发的时候都是先将 png 图片,往 tinypng 网站中压缩一下再放入,所以可以压缩图片,一般能压个三分之一~三分之二。

也可以更换图片格式,比如 webp ,svg 可以更小,android studio 也提供了对应的支持,但是没有最好的格式,只是适用场景不同

几种格式的优缺点

这里多提一下 webp ,因为这是 google 推出的,大家在谷歌浏览器下载图片的时候,一般默认下载下来就是 webp 格式,所谓更小的内存占用,本质上是对图片进行了压缩,webp 的压缩算法是 VP8 视频编码,核心逻辑就是将图片分割成更小的子块,然后预测周围像素值,预测越准,周围的像素值就可以删去,再在图片打开时算出删掉的像素

图片网络化

在微信或者 qq 聊天中,对方发来一张图片,我们在聊天窗口往往先看到一张很模糊的缩略图,当点击时才会加载出高清图,

这个思路也可以用在 apk 中,很多入口较深的高清大图,或者需要经常更新的图片,也许用户根本不看,就没有必要内置在 apk 中,看时加载即可,如果需要提前占位置,可以用缩略图代替

至于哪些图网络化,需要根据业务与用户体验来权衡了

比如淘宝,在断网情况下打开时,只有 icon 内置了

image-20211023211648469

其他策略

无论是对 Dex 还是对资源进行优化,虽然安全有效,但是本质上是将原来有的东西变得更小,对 apk 的瘦身程度是有限的,还有一些"七伤拳",优化率极高,但是对 apk 的影响也很大,需要谨慎使用。

插件化

所谓插件化,就是将 apk 中的非主要功能弄成独立的 apk ,原主 apk 称为宿主。

比如支付宝里面,就是搞支付的,那么他里面的什么口碑,基金,天猫一堆乱七八糟,同时功能独立的东西就非常适合做成插件,用户用到的时候再从网络加载进来,这样极大的减少了 apk 占用。

但是这里涉及到比较多的技术问题:

  1. 用户现在只有宿主 apk ,如何让宿主加载到插件 apk 里面的代码?
  2. android 四大组件都需要到 manifest 中注册,插件里面的组件显然不可能提前注册到宿主的 manifest 中(不然注册了,插件没加载进来,会找不到类),所以如何让系统认为下载下来的插件有注册?
  3. 宿主与插件资源能否正确互相引用?

一般来说,通过的是代理和反射来处理,腾讯有一个 shadow 框架可以大致实现"零反射",

  • 复用独立安装 App 的源码
  • 零反射无 Hack 实现插件技术
  • 全动态插件框架
  • 宿主增量极小
  • Kotlin 实现

不过插件化技术不在今天的讨论范围,有兴趣可以研究下tencent-shadow

当使用了插件化后,项目基本是要重构了,相比起改改 Dex 和图片,这个工程量极大,但是收益也会很高

webview

这里类似于图片网络化,相对于图片,直接将整个界面都变成 url ,

我们手机 app 中的小程序一般都是 url 显示在 webview 中

相关技术可以使用 jsBridge 与 Hybird ,本质上就是通过 bridge 连接 h5 与 android iOS ,实现通信

image-20211023201811533

不过代价就是,加载速度慢于原生,还要注意防止网址篡改等

小结

本文我们讨论的是 apk 的瘦身方案,首先先明确了 apk 的主要组成部分为 dex 文件与资源文件

  • 对于 dex 文件,我们可以进行混淆,字节码重排序,移除多余库与代码

  • 对于资源文件,我们可以替换格式,压缩图片,网络化

除了这些常规操作,我们还可以使用插件化与 Webview 方法极致减少体积,但是这两个技术工程量大,而且有性能代价,需要谨慎使用。

参考资料

深入探索 Android 包体积优化(匠心制作-上)

Android 项目中资源文件 -- asset 目录和 res 目录

顶象 App 加固技术解析:DEX 文件格式的详解

D8 Optimizations

Android 开发应该掌握的 Proguard 技巧

看到 Go 与 MongoDB 的交互方式,我想放弃 Go 了

Posted: 24 Oct 2021 04:20 AM PDT

之前习惯了 python/js 这种语法,感觉很自然很方便。

今天看了下 MongoDB 官方的 Go 接口,哎呀,那交互方式,真的是痛苦。

例如查询用户为 1 的用户:{userid: 1},在 Go 里面你还得包裹为 bson.D{{"userid", 1}}

返回的结果是一个索引,要 Decode 下,Docode 还需要传递一个结构体过去。

还得传递一个 context (还没看为啥要这么做,其他语言不用)

感觉一点也不方便,代码很多不美观不优雅,习惯了 js/python 这种比较简单直观的语法,难以接受呀。

感觉 Google 最近出的东西,语法都那么特立独行的,还有一个是死亡嵌套 Flutter ,嵌套到怀疑人生。

官方教程: https://www.mongodb.com/blog/post/mongodb-go-driver-tutorial

filter := bson.D{{"name", "Ash"}}  update := bson.D{     {"$inc", bson.D{         {"age", 1},     }}, }  updateResult, err := collection.UpdateOne(context.TODO(), filter, update) if err != nil {     log.Fatal(err) }  fmt.Printf("Matched %v documents and updated %v documents.\n", updateResult.MatchedCount, updateResult.ModifiedCount) 

我坦白,我是一个假程序员

Posted: 24 Oct 2021 03:54 AM PDT

今天 10 月 24 号,程序员节
非常荣幸能够在 V2EX 认识了不少程序员朋友,祝你们节日快乐
今天我不能为自己庆祝,因为我不是一个真正的程序员
作为一名真正程序员,github 账户是必备吧
虽然我也有 github 账户,但是我从来没有用过 git cli
Github 和 codepen 是我的最爱,平时 fork 和 download 是我的日常
非科班出身,但是我自己也挺努力,自学了 PHP PYTHON GOLANG NODEJS VUE
我目前在做的网站主要是 miao.win 以及 dot.af ,欢迎各位莅临指导工作

js 生成中文 PDF 有啥好办法?

Posted: 24 Oct 2021 02:00 AM PDT

html2pdf 支持中文,html 排版,但是生成的 PDF 是图片的,无法复制里面的文字

jspdf 搞了好久无法支持中文,然后前端加载一个几十兆的字体也麻烦

debian 系统下,有没有简单的办法检测指定网卡配置是 dhcp/static/manual?

Posted: 24 Oct 2021 01:14 AM PDT

我想到的是通过查找网卡配置文件 /etc/network/interfaces 的内容来判定,但是网卡配置可能写到了 /etc/network/interfaces.d 下的某个文件,而且配置文件内容的一行要是被拆成了奇形怪状的好几行怎么办?虽然多写几句 bash script 也能搞定,但是想问问有没有别的办法。

那个 ifquery 命令我试过,没有用,按 help 输出和 manpage 操作,要么输出为空,要么根本不能正常解析它自己的选项,连示例都不能运行。

一个小白想法, Python 为什么不通过命令行开关切换两种 GIL 模式?

Posted: 23 Oct 2021 09:24 PM PDT

最近 python 出现一个惊艳的 no-gil 设计,想必大家已经知道了。

那么对于 python GIL 这个老问题,似乎常见的说法是:
粗粒度的 GIL 降低多线程的性能,细粒度的 GIL 降低单线程的性能。

那为什么不全都要,通过命令行开关切换两种 GIL 模式?
我本身并不太懂同步原语、线程安全、并行编程这些,所以想请教一下。欢迎指正!

是否有针对为了兼容的多余代码的代码检查?

Posted: 23 Oct 2021 06:44 PM PDT

Python 每个新版本中的标准库会新增一些更便捷的功能,但自己在写库时为了兼容旧版本 Python 就不会使用那些最近新增的功能。例如用 pathlib 删除一个文件,无论文件存在与否:

from pathlib import Path  # 写法一 (Python 3.8+) Path('file').unlink(missing_ok=True)  # 写法二 try:     Path('file').unlink() except FileNotFoundError:     pass 

因为 missing_ok 是 Python 3.8 时新增的选项,所以如果要兼容 Python 3.8 以前的版本,就会用写法二。

在最初库写成一段时间后,可能会决定放弃支持一些 Python 的旧版本。在这个例子里,如果我想放弃对 Python 3.7 的支持,如果有工具能找出现有代码中的可能为写法二的部分,提示我考虑写法一,那对维护工作就省心多了。那有这种代码检查吗?


题外话:分享一个解决我这个问题相反问题的工具:vermin。可以用来检查一个 Python 文件 /包所需要的最低 Python 版本。

Windows 11 的那个任务栏角溢出有办法和以前一样全部显示吗?

Posted: 23 Oct 2021 04:02 PM PDT

难道这个功能被删除了? 网上找个了一个办法执行下面的命令可以打开设置菜单,但每次重启后就失效了...

explorer shell:::{05d7b0f4-2121-4eff-bf6b-ed3f69b894d9}

请问如果要组建 100 个节点以上的集群,如何给机器配置免密登录呢?如果后期这个集群扩容到 200 个节点以上,该如何操作?有大佬提供下思路吗?

Posted: 23 Oct 2021 10:31 AM PDT

温州“遇见志同道合”线下沙龙 - 集合温州对互联网开发有热情的小伙伴

Posted: 23 Oct 2021 08:00 AM PDT

在温州的你,或是在北京、深圳、杭州、广州、上海的温州人的你, 是否也曾想要做一个互联网产品? 但苦于势单力薄,一直无法得到很好的执行。 时间流逝,不要等到年岁已高,再回过头,发现自己曾经的梦想早已经丢失不见,而后悔自己曾经没有去努力过。

众人拾柴火焰高,做互联网产品,需要坚强的团队,需要几个有热情、有能力的伙伴共同努力~ 温州其实有很多有热情的小伙伴,散落在温州各个角落,我希望通过这个活动能将大家聚集起来。

无论你是技术开发(前端 /后端)、UI 设计、产品经理、投资人,

欢迎加入,一起探讨,参加线下聚会,思想碰撞,遇见志同道合的伙伴,

或许就能成就自己一直想做的那个产品。

加微信 bigbod 添加的时候备注"温州互联网志同道合",我会拉你入群

将安排线下沙龙,一起探讨,一起交流

Language Server for Java ™ 1.0 在 VS Code 上正式发布!

Posted: 23 Oct 2021 04:01 AM PDT

今天,我们很高兴与大家宣布:Language Server for Java™ 的 1.0 版本在 Visual Studio Code 上正式发布了!这是 Java 在 Visual Studio Code 上的一个重要里程碑,也是微软,红帽以及整个社区之间多年合作的结果。在这里我们要感谢所有提出过意见或者做过贡献的用户,谢谢!

发布亮点

当我们开始发布第一个版本时,我们的目标是在 Visual Studio Code 上提供最好的 Java 开发体验。此后,我们一直保持每月 1-2 次的发布周期,经历了多次迭代。在这个 1.0 版本中,我们想重点介绍以下新特色

Java 17 支持

我们会始终尽全力去支持 Java 语言提供的最新技术。在我们的 1.0 版本中,我们的 Java 插件现在正式提供对 Java 17 的支持

性能改进

性能提升也是本次发布的另一个亮点。由于 LSP规范中的新功能,我们能够推迟某些计算,从而在许多场景中为我们带来更好的性能。此外,更好的默认JVM选项可以用于改善更大更复杂项目的用户体验。我们还在很多特定场景做了细微的改进,以提高语言服务器的响应能力。

类继承体系

现在用户可以方便地在 Visual Studio Code 中轻松访问类继承体系!这是我们最近推出的一项功能,并且一直是 Java 社区中最热门的需求之一。

库源代码定位

目前,我们允许用户快速定位某个库的源代码,有时这非常有用。现在,我们已将其支持扩展到普通项目(即没有使用构建工具的项目)。只要库来自 Maven Central repo,我们就可以解析其来源。还支持更高级的配置。

更多代码相关操作

用户体验是我们继续改进的另一个领域。我们在新版本中提供了更多的代码操作来优化用户开发体验

Gradle Kotlin (.kts) 支持

尽管我们已经使用 Groovy 脚本支持 Gradle 项目,但我们不断听到社区需要更好的 Kotlin 支持,所以在新版本中我们对 Kotlin 提供了一些基本的支持。我们希望可以继续扩展这一领域,并在未来为 Kotlin 提供出色的体验

Visual Studio Code 的 Java 之旅

Language Server for Java™ Java扩展包的基础,并提供了 Java 代码编辑、完成、重构、导航等核心体验。在过去的几年里,微软和红帽一直密切合作,不断为这个语言服务器添加新功能。我们还一直在微调语言服务器的性能,并通过修复 Bug 和添加新特性来提高稳定性。

除了主要的 Java 语言支持外,我们还基于语言服务器构建了各种 Java扩展,并扩展了 Visual Studio Code 上的 Java 开发体验,包括项目管理、对 Maven/Gradle 的构建工具支持、更好的测试支持等。这些努力背后的目标是在 Visual Studio Code 上提供最佳的 Java 开发体验。

展望未来

在未来,我们为 Visual Studio Code 上的 Java 语言支持有非常多的计划。其中一些包括:

  • 让用户更容易配置编译器的错误 /警告和代码格式
  • 更智能以及有用的代码自动完成建议
  • 继续提供更多的代码相关操作并最终达到与其它 Java IDE 相同的功能
  • 进一步提升语言服务器的整体性能和启动时间
  • 在插件中直接嵌入 Java Runtime 以改善整体上手体验

Language Server for Java™ 的 1.0 版本是 Java 在 Visual Studio Code 上的重要里程碑,我们还有很长的路要走。微软和红帽将继续在 Java 语言支持方面进行合作和投入,我们致力于提供出色的 Java 开发体验。

与往常一样,您的反馈对我们的产品改进至关重要,因此请不要犹豫尝试我们的产品。您可以点击此链接开始使用 Visual Studio Code 学习 Java,也可以通过此链接获得最新的 Java 插件包。

Android 12 发布了,鸿蒙什么时候能 rebase 下主干

Posted: 23 Oct 2021 03:52 AM PDT

一直安卓 10 也不是事啊

No comments:

Post a Comment