0%

App体积优化

随着Android移动开发的需求越来越复杂,我们不可避免的遇到发布出去的apk体积越来越大的问题,
作为一个非心智成熟型App,apk影响拉新用户转化率,谷歌给出了一个很详细的数据,包体大小每上升 6MB,应用下载转化率就会下降 1%. 2020年app包大小从60M减小到35M,算上业务增量,实际缩包30M。

apk_size_timeline

下面是年底我们对包大小转化率做的实验数据,

apk_size_down_data

1 apk的组成

lib/ 存放so文件,可能会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi与x86的架构即可,如果非必需,可以考虑拿掉x86的部分
res/ 存放编译后的资源文件,例如:drawable、layout等等
assets/ 应用程序的资源,应用程序可以使用AssetManager来检索该资源
META-INF/ 该文件夹一般存放于已经签名的APK中,它包含了APK中所有文件的签名摘要等信息
classes(n).dex classes文件是Java Class,被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
resources.arsc 编译后的二进制资源映射文件表。
AndroidManifest.xml Android的清单文件,格式为XML,用于描述应用程序的名称、版本、所需权限、注册的四大组件

2 使用Android Studio分析apk

apk_analyz

3 资源优化

3.1 无用资源移除。

  1. 源码通过lint工具扫描无用图片资源,打包后的apk可以使用ApkChecker进行扫描。ApkChecker
  2. 使用 shrinkResources true属性,这里shrinkResources属性的作用并不大,因为在扫描无用资源过程中, 系统为了避免使用getIdentifier过程找不到资源,所以在清除资源的时候条件判断非常严格,经过测试,这个功能非常鸡肋,大量无用的资源无法删除。
  3. 检测资源文件md5一致的图片。 ApkChecker
  4. 支持特定的分辨率。现在大部分手机都是1080p,对应的xxhdpi的资源,所以我们只支持xxhdpi的资源,其他屏幕即使大一点,图片放大也不会太模糊。工具booster-task-resource-deredundancy

3.2 图片压缩

1 源码图片压缩缺陷较多,只能对工程内的图片进行压缩,无法压缩lib aar中的资源,采用gradle插件的方式能对全局图片进行压缩。 网上介绍webp格式的图片大小由于png,其实并不是,有些图片使用tinypng压缩后转为webp图片反而更大了。我们采取的方案是先将所有的图片png压缩,再尝试png转webp,如果webp更小则采用。这里需要注意webp格式4.0+才支持,对于包含透明度的4.2+才支持pngquant cwebp

AAPT 会使用内置的压缩算法来优化 res/drawable/ 目录下的 PNG 图片,但这可能会导致本来已经优化过的图片体积变大,因此,可以通过在 build.gradle 中 设置 cruncherEnabled 来禁止 AAPT 来优化 PNG 图片

1
2
3
aaptOptions {
cruncherEnabled = false
}

2 使用矢量图。 这个在我项目中后面大量用到了,缺点就是对5.+的系统支持的比较好,对于低版本是打包过程将矢量图直接转成了图片,没有效果。

3.3 移除适配资源

我们需要 根据 App 目前所支持的语言版本去选用合适的语言资源,例如使用了 AppCompat,如果不做任何配置的话,最终 APK 包中会包含 AppCompat 中所有已翻译语言字符串,无论应用的其余部分是否翻译为同一语言。对此,我们可以 通过 resConfig 来配置使用哪些语言,从而让构建工具移除指定语言之外的所有资源。同理,也可以使用 resConfigs 去配置你应用需要的图片资源文件类,如 “xhdpi”、”xxhdpi” 等等,代码如下所示:

4 AndResGuard资源优化和压缩

4.1 arsc文件优化

1 arsc文件它记录了资源文件id对应的名称与路径,可以看到图中res/drawable的字样,开启优化混淆成短路径 res/s/a,可以减少文件的大小。坑:因为混淆资源路径,所以使用getIdentifier的方法是获取不到资源的。这里需要配置白名单的方式不混淆路径。
arsc_info

2 zip文件压缩有deflate和stored两种方式,stored方式的文件是不会被压缩的,可以看到arsc文件以及png图片采用的是stored模式。如果文件采用deflate模式意味着AssetManager读取的时候需要解压。

  • 对.png、.jpg强制压缩,但是的确普遍压缩率在3%-5%,收益不是特别高;
  • 假若你的resources.arsc小于1M,可以对它进行压缩,这边可压缩50-70%。需要注意的是,如果你的resources.arsc是压缩的,程序需要把它读到内存,也就是会增大运行内存;

4.2 7zip压缩

7z压缩算法号称优化了字典,完全兼容zip,使用7zip的最大压缩模式比传统zip方式的确有所提升。对于生成的apk重用7zip进行压缩,收益1.5M。

此外,抖音 Android 团队还开源了针对于海外市场 App Bundle APK 的 AabResGuard 资源混淆工具。

5 代码优化

5.1 R文件内联

Android在构建过程中会根据资源生成R文件,里面包含了资源索引,使用该索引可以在最终生成的resources.arsc资源映射表中找到对应资源,对于开发者来说在代码中引用资源很方便。在Library工程中引用的R资源索引不是final的,所以我们在Library工程不能在switch - case 和Annotation中使用资源索引。由于引用的资源不是final的,所以Library的产物aar中包含的class中使用的资源索引还是会以包名存在。在App工程中构建时会将依赖的AAR资源进行合并,根据合并的结果生成最终的R资源索引,这时的资源索引已经确定,所以全部是final的,java编译器在编译时会将final常量进行inline内联操作,也就是App工程中的java源码编译后的class中使用的R资源索引全部会替换为常量值。如果Library A依赖了Library B,则Library A包名的R资源文件中包含所有Library B的资源索引,所以在代码中既可以引用Library A包名的R资源索引使用Library B中的资源,也可以使用Library B包名的R资源索引使用Library B中的资源。R文件inline可以全局将将int id值直接改到代码中, Lib中的R文件类就可以删除。
工具:r-inline

5.2 无用log删除

其实项目中大量的日志在relase模式下是不输出的,那这块代码也是无用的。
工具:method-call-opt-plugin

5.3 编译插件升级

打包插件从3.0.1升级到3.4.2后,会开启d8和R8编译器。编译生成的dex产物更小。 收益2M。
ProGuard 和 R8 都应用了基本名称混淆:它们 都使用简短,无意义的名称重命名类,字段和方法。他们还可以 删除调试属性。但是,R8 在 inline 内联容器类中更有效,并且在删除未使用的类,字段和方法上则更具侵略性。例如,R8 本身集成在 ProGuard V6.1.1 版本中,在压缩 apk 的大小方面,与 ProGuard 的 8.5% 相比,使用 R8 apk 尺寸减小了约 10%。并且,随着 Kotlin 现在成为 Android 的第一语言,R8 进行了 ProGuard 尚未提供的一些 Kotlin 的特定的优化。

5.3 混淆优化

由于部分SDK偷懒,针对代码直接做暴力全部keep。

通过jadx工具反编译apk,可以看到apk内大量的代码未进行混淆,s1我们和消息sdk团队合作,将消息的混淆规则精细化,收益700+k.

6 SO动态下载

分析业务场景,实现部分业务so动态下载。通过数据分析,正式版本下载成功率在95%,错误主要集中在网络错误。

基于mtl插件,实现通过配置操作so的remove和上传,并将信息写入到asset配置文件中。

最初版本我们基于mtl老代码,直接对so进行上传下载,下载成率是95%, 新版本对so进行zip压缩再上传,下载体积减少一半,下载成功率从95.8%提示到98.0%。

7 科学的删除无用代码

7.1 建立代码覆盖率

1 我们通过lint扫描无用代码,在删除代码的时候风险较高,因为业务复杂,可能涉及到反射的不确定性,那我们需要建立一个机制降低删除代码的风险。

  • 代码覆盖率建设其实就是在编译器,通过插桩的方式在每个类的静态代码块中增加一条记录,当类加载的时候就会被上报。
  • 通过线上收集类的使用率,结合本地lint可安全删除代码。

2 对于部分下线业务,代码引用链依旧存在,lint无法扫码,但是可以通过代码覆盖率发现。

3 通过代码覆盖率实现责任制,对于lib多个版本覆盖率低的问题将会推进解决。
arsc_info

8 建立卡口

卡口设计
arsc_info

基于打包插件插件,对比当前版本和上个版本的依赖,打包后输出依赖dif文件信息。
arsc_info

arsc_info

因为现阶段项目使用的是集成模式,包大小卡口核心是前置卡口,是对于超过包大小的情况不让集成,输出大小差值,让开发感知,对于超过的大小需要优化不合理的地方并且邮件审批。所以我们在集成module超过阈值大小,直接打包失败。
arsc_info

总结

2020一直在对特价版包大小做优化,整体从60M减少到35M,而对比拼多多,拼多多从年初的27M增加到37M,特价版在快速发展中也遇到了大量的业务集成,算上其中的业务增量总共减少30M,在核心链路使用flutter的项目中拿到这个结果实属不易。
在没有建立合理的流程和机制过程前非常累,各种资源新增不规范,好不容易的缩包被新增业务抵消,后续我们建立合理的规范,形成责任制管理,让开发形成主观意识,也为后续管控包大小形成正向效果。