前言
随着开发不断迭代,App体积越来越大,包大小的增大也会给我们应用带来其他的影响 比如
- 下载率影响 过大的包体积会影响下载转化率,根据Google Play Store包体积和转化率分析报告显示,平均每增加1M,转化率下降0.2%左右
- 渠道限制 部分厂商预装强制要求安装包大小(比如国内市场在下载较大安装包就会提醒大流量是否继续下载的弹窗)
- 性能影响 过大的包体积在安装耗时和运行内存占用方面都会有很大影响
但是包大小的优化不是一次就可以搞定的,需要持续的维护做好打持久战的准备,此篇文章算是在本地生活对包大小实战和别人的经验的总结,日常学习记录仅做参考。
基础了解
在包体积优化前需要对APK做一个基本的了解
目录 | 内容 |
---|---|
lib | 文件夹下主要存放不同的cpu架构的so文件,会有armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持armabi与x86的架构即可 |
res | 文件夹下存放编译后的资源文件,drawable和layout资源 |
assets | 应用程序的资源、字体、音频文件等,需要通过AssetManager类来访问内容 |
dex | Android项目中的代码在编译后会生成.class文件,然后再通过dx工具转换为字节码文件,就是这个dex文件。一般情况下只有一个classes.dex,如果项目代码方法数超过了65535而采用了multidex的话,会有其他的.dex文件 |
META-INF | 签名信息,用来验证apk文件的完整性、合法性 |
resources.arsc | 二进制资源文件、AndroidManifest.xml清单文件 |
打包简要流程
打包主要有以下几步:
- 使用aapt工具处理所有的资源,生成一个R.java文件,一个resources.arsc文件以及其他资源。
- 处理.aidl文件,生成对应的Java接口文件。
- 将上述两步得到的R.java文件、Java接口文件,与Andorid源码一起,通过Java编译器,编译得到Java字节码文件.class文件。
- 获取依赖的第三方库文件,将其与上一步得到的.class文件一起,通过使用dx工具,生成.dex文件。
- 将资源索引文件resources.arsc、资源目录res、与上一步得到的.dex文件一起,通过apkbuilder工具,构建出初始的.apk文件。
- 使用jarsigner工具,对.apk文件进行签名。
- 使用zipalign工具,对.apk文件进行对齐。(让资源按四字节的边界进行对齐,加快资源的访问速度)
经过以上七步,一个完整的apk文件就诞生了。
那么针对压缩文件,主要的压缩体积方式分为:减少和压缩
包现状分析
使用AppChecker分析
包体分析主要借助的是腾讯AppChecker完成的,AppChecker分析包文件主要还是借助了andoid build-tool下面的 aapt工具
上图只是查看了各个文件类型占比,还支持统计 APK 中包含的 R 类、检查是否有多个动态库静态链接了 STL 、搜索 APK 中包含的无用资源、重复资源分析以及支持自定义检查规则等 (强烈推荐的检测工具)
借助AS提供的Analyze APK
可以直观的查看APK的组成大小占比等信息,也可以用来查看其他产品使用了那些三方库等信息。
常规优化方式
Lint自动检测
csharp
// 扫描res资源文件
Analyze > Run Inspection by Name > Unused resources
// 扫描无用代码
Analyze > Inspect code
不过需要注意这里扫描为静态扫描,部分资源可能存在动态调用,再删除的时候需要再三确认 ,Inspect code扫描出来的一样为静态扫描结果,反射和动态引用的代码是不会出现在这里的。
常规资源压缩
这里主要以图片资源来进行压缩优化,列举常用方案前,简单说下各个图片文件及其特征 jpg: 一种有损的基于直接色的图片格式 属于光栅类型,所以可以表示2的24次方种颜色,非常适合色彩丰富图片、渐变色,所以相对的 jpg图片文件大小较大。
png: 也是属于光栅类型,无损压缩格式的基于8为索引色的位图格式称为png-8,支持透明度 并且文件尺寸相比jpg更小。还有一种png-24则是基于直接色的位图格式,图片存储相对较大,但是可以展示比较丰富的图像色彩。
gif: 光栅格式的图像文件类型。它使用无损压缩,但将图像"限制"为每像素 8 位和 256 色的有限调色板,常用于动画图像。
svg: 矢量图像文件类型,使用笛卡尔平面上的线和曲线系统,与总面积相比,而不是任何单个像素,可以无限放大而不会失真
Webp: 供更好的无损和有损图像压缩而开发的图像格式,相较于png 格式还可以压缩 10% -30%大小并且可以获得相同的质量
主要有两种资源优化手段 :png文件压缩和替换为 Webp图片。
- png压缩,平时主要使用 tinypng.com/ ,不过很多UI平台都会在切图上传的时候进行自动压缩。
- 转为Webp, Android Studio可以右键资源文件 Convert to Webp 一键切换。
代码混淆
java
android {
...
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
压缩 ( -dontshrink 关闭压缩):默认开启,用以减小应用体积,移除未被使用的类和成员,并且会在优化动作执行之后再次执行(因为优化后可能会再次暴露一些未被使用的类和成员)。
优化( -dontoptimize ):默认开启,在字节码级别执行优化,让应用运行的更快。
diff
-dontoptimize 关闭优化
-optimizationpasses n 表示proguard对代码进行迭代优化的次数,Android一般为5
混淆( -dontobfuscate 关闭混淆):默认开启,增大反编译难度,类和类成员会被随机命名,除非用keep保护。
D8 R8优化
Android Studio 3.1 或之后的版本 D8 将会被作为默认的 Dex 编译器,相较于D8,重点说下R8.
根据官方说法,R8 是 Proguard 压缩与优化部分的替代品,并且它仍然使用与 Proguard 一样的 keep 规则。如果我们仅仅想在 Android Studio 中使用 R8,在 build.gradle 中打开混淆的时候,R8 就已经默认集成进 AGP 中了。
那么,R8 与混淆相比优势在哪里呢?
ProGuard 和 R8 都应用了基本名称混淆:它们 都使用简短,无意义的名称重命名类,字段和方法 。他们还可以 删除调试属性 。但是,R8 在 inline 内联容器类中更有效,并且在删除未使用的类,字段和方法上则更具侵略性 。例如,R8 本身集成在 ProGuard V6.1.1 版本中,在压缩 apk 的大小方面,与 ProGuard 的 8.5% 相比,使用 R8 apk 尺寸减小了约 10% 。并且,随着 Kotlin 现在成为 Android 的第一语言,R8 进行了 ProGuard 尚未提供的一些 Kotlin 的特定的优化。
从表面上看,ProGuard 和 R8 非常相似。它们都使用相同的配置,因此在它们之间进行切换很容易。放大来看的话,它们之间也存在一些差异。R8 能更好地内联容器类,从而避免了对象分配 。但是 ProGuard 也有其自身的优势,具体有如下几点:
- ProGuard 在将枚举类型简化为原始整数方面会更加强大 。它还传递常量方法参数,这通常对于使用应用程序的特定设置调用的通用库很有用。ProGuard 的多次优化遍历通常可以产生一系列优化 。例如,第一遍可以传递一个常量方法参数,以便下一遍可以删除该参数并进一步传递该值。删除日志代码时,多次传递的效果尤其明显。ProGuard 在删除所有跟踪(包括组成日志消息的字符串操作)方面更有效。
- ProGuard 中应用的模式匹配算法可以识别和替换短指令序列,从而提高代码效率并为更多优化打开了机会。在优化遍历的顺序中,尤其是数学运算和字符串运算可从中受益。
- ProGuard 具有独特的能力来优化使用 GSON 库将对象序列化或反序列化为 JSON 的代码 。该库严重依赖反射,这很方便,但效率低下。而 ProGuard 的优化功能可以 通过更高效,直接的访问方式 来代替它。
重复资源过滤
由于大项目大多采用组件或插件化,多个模块之间可能存在资源的重复引入,常见的解决方法是通过资源包中的每个ZipEntry的CRC-32 checksum来筛选出重复的资源;通过android-chunk-utils修改resources.arsc,把这些重复的资源都重定向到同一个文件上; 把其它重复的资源文件从资源包中删除。
根据美团的方案之前实践过,并没有落地 实现方式较复杂,并且收益率不太高,本人并没有落地到项目中,有落地的同学可以反馈下实际优化提升。
ENUM减少使用
如可以在开发过程 尽量减少 enum 的使用,每减少一个 enum 可以减少大约 1.0 到 1.4 KB 的大小
so文件移除
市面上的手机cpu大多是arm
架构的,所以保留arm的一种即可(定制的除外),armeabi-v7a
或armeabi
都可,其他直接删除。
kotlin
复制代码
android {
defaultConfig {
ndk {
abiFilters 'armeabi-v7a'
}
}
}
在模拟器调试,就加上x86
的架构,在local.properties
中变量控制,正式包移除即可
还有一种情况是不同的第三方库中存在相同的so文件 pickFirst只会打包第一个遇到的冲突的so,merge(碰到冲突会合并)和exclude(直接排除匹配到的文件,不建议使用)
kotlin
packagingOptions {
pickFirst 'lib/arm64-v8a/libgnustl_shared.so'
pickFirst 'lib/armeabi-v7a/libgnustl_shared.so'
}
三方库处理
实际项目开发中,各个模块都会有不同的移动团队开发,那么就可能存在重复引用的情况,比如某些不同的三方库,可能底层存在依赖同一套库的代码,那么在依赖的时候就可以进行去除
kotlin
implementation('com.allenliu.versionchecklib:library:2.0.5') {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support.constraint', module: 'constraint-layout'
exclude group: 'org.greenrobot', module: 'eventbus'
exclude group: 'com.squareup.okhttp3', module: 'okhttp'
}
三方库整合
比如RN中使用的图片加载库是 Fresco ,但是Native使用的Glide ,那么我们就可以通过整合此类Case 达到缩减包大小目的。
还有一种场景 比如一个三方库我们只使用一部分代码,完全可以拉出来魔改下,引入到自己的工具库中,减少多余代码引入。
进阶优化手段
插件化
依赖于插件化的特点,将整个App拆分为多个模块,每个模块可单独运行 都是APK,最终打包的时候将宿主Apk和插件Apk分开打包,发布的时候只需要发布宿主Apk即可,用户进入不同场景按需动态下载对应Apk即可,可以很大程度上优化包体积问题。
重复技术方案筛选
如果项目中由于历史包袱,存在多个跨端方案,比如存在 Web、小程序和 Flutter ,如果业务允许,可以将多余跨端方案移除,相对应的引擎So文件即可进行删除,起到减少包体积目的。
So动态化下载
以Flutter为例,考虑到引擎加载和初始化时间,项目中首页一般还是采取Native方式展示,跨端场景大多在二级页面或非首页场景,按照按需思想,Flutter So也可以不放在本地,我们可以在进入跨端页面路由前,或进入App闲时阶段,添加动态下载So逻辑,相同思想,比如一些音视频文件非启动必须得话,也可以按需下载加载,本地尽量不放大文件。
DebugItem
JVM 运行时加载的是 .class 文件,而 Android 为了使包大小更加紧凑、运行时更加高效就发明了 Dalvik 和 ART 虚拟机,两种虚拟机运行的都是 .dex 文件,当然 ART 虚拟机还可以同时运行 oat 文件。
所以 Dex 文件里的信息内容和 Class 文件包含的信息是一样的,不同的是 Dex 文件对 Class 中的信息做了去重,一个 Dex 包含了很多的 Class 文件,并且在结构上有比较大的差异,Class 是流式的结构,Dex 是分区结构,Dex 内部的各个区块间通过 offset 来进行索引。
为了在应用出现问题时,我们能在调试的时候去显示相应的调试信息或者上报 crash 或者主动获取调用堆栈的时候能通过 debugItem 来获取对应的行号,我们都会在混淆配置中加上下面的规则:
diff
-keepattributes SourceFile, LineNumberTable
这样就会保留 Dex 中的 debug 与行号信息。根据 Google 官方的数据,debugItem 一般占 Dex 的比例有 5% 左右
ReDex
ReDex 是 Facebook 开发的一个 Android 字节码的优化工具。它提供了 .dex 文件的读写和分析框架,并提供一组优化策略来提升字节码。官方提供预期优化效果:对dex文件优化为 8%
后期维稳
在打包发布环节,可以编写插件针对各个模块大小或资源文件扫描,设置规则,写进流程中,可以防止包大小爆发式增长,当然好的包大小资源压缩思维,也可以帮助Apk维持在一个健康的水位上。
思考
以上记录的种种方式,可根据项目状态进行选择优化,抓大放小,实事求是,也不能一味的以包大小越小越好,不影响业务,避免引起线上事故需要放在包大小之前衡量,避免得不偿失。