Android包体积优化-总纲

1,710 阅读7分钟

I Focus on optimizing app performance especially decreasing package size

我将分享一些包体积优化的通用思路(大纲)

后续还会补充一些技术文章如so上云方案、so压缩方案、apk内资源路径缩短、包体积监控治理平台建设等

优化蓝图:

包体积优化蓝图.png

一、我们为什么做包体积优化?

1)Google官方数据

reference: medium.com/googleplayd…

google_data.png

  1. 谷歌内部统计数据表明:越小的apk安装包体积与越高的apk安装转化率相关
  2. 低于100M的应用,安装包每增大6MB,对应的apk安装转化率下降1%
  3. 10MB左右的应用下载完成率比100MB左右的应用高出30%

基于google官方公布的数据,我们发现应用的包体积大小对于apk下载成功率以及用户的安装转化率有着极其重要的影响,能够直接影响到用户是否愿意下载这个应用,进而影响到app的活跃用户

2) 厂商合作(预装)

如果app与手机厂商有一些合作比如预装(手机出厂时预先安装好app,用户无需下载),那么大多数厂商都对app的包体积大小有着严格的限制,同时越大的包体积通常需要越昂贵的预装价格,这些都反映了做包体积优化的必要性。 预装价格通常与以下元素相关;

  1. app包大小
  2. app安装在手机的桌面还是文件夹中
  3. 用户激活率

当然如果来了某个厂商预装需求,但是此时app的包大小不满足厂商的预装要求,那么恭喜你,接下来的时间会变得非常忙了。常见的临时解决方法是

  1. 挑选体积较大的so上云或者压缩
  2. 裁剪非核心业务线(需要引导用户下载完整版app)
  3. ....

二、如何做包体积优化?

如何做包体积优化是一个很难回答的问题,就好比一个人如何变得有钱,很难一下子把这个问题回答的很好

  1. 技术硬实力

技术硬实力在做性能优化,特别是包体积特别重要,需要的技术栈繁杂且深入如gradle、AGP、Android资源运行时加载流程、so的加载流程、dex、c/c++、各种黑科技、系统源码....

  1. 思考

思考在如今的背景下(原生、RN...)等如何做包体积优化,在有无限业务需求迭代的情况下,如何真正解决包体积持续增长的问题,是否考虑技术架构转型比如转跨端ReactNative,业务需求的迭代最终如果能实现动态下发,最终解决体积无限增长的困局呢。

1)优化思路:

解包看apk下到底有那些内容,从结果向前推或许是个不错的思路

image.png

目录/文件说明收益
classes.dexDalvik 字节码文件,包含应用程序的所有 Java 类。通常一个 APK 至少包含一个 dex 文件。收益高、技术难度高
AndroidManifest.xml应用的核心配置文件,包含包名、组件声明、权限等信息。解压后为二进制格式,需要使用工具解析。几乎无收益
META-INF/包含签名和校验信息:
- CERT.RSA:签名证书
- CERT.SF:签名的元信息
- MANIFEST.MF:校验文件。
收益小、技术难度高
res/资源文件目录,存储布局 XML 文件、图片、动画等资源,未经过编译的部分。收益高、技术难度高
lib/包含原生库的目录,以 CPU 架构分类(如 arm64-v8a, armeabi-v7a),每个目录下是 .so 文件。收益高、技术难度高
assets/应用的原始资源文件夹,存放未编译的资源或数据,应用可直接读取此目录内容。收益高、技术难度高
resources.arsc已编译的资源索引文件,包含字符串、样式等的二进制表示,供应用快速访问资源。不让压缩后收益一般、技术难度高
kotlin/(可选)Kotlin 编译器生成的文件目录,用于支持 Kotlin 应用。未做调研

2)哪些优化项ROI高?

1. 开启代码混淆、无用代码、资源删减,开启代码混淆后包体积会下降很多,只需要开启混淆开关,几乎无风险

minifyEnabled true
shrinkResources true

2. 分架构打包64/32位

我们的app中可能会包含很多so文件,但通常每个so都会对应几种abi架构,如果打包时不单独处理几种架构都会被打到apk包。 构建创建64位和32位的flavor,针对64位包和32位包分别打包,然后应用商店中分别上传,应用商店都支持,且鼓励分包上传的方式预计会得到15%+的收益

3. 设置extractNativeLibs=true

这个设置在AndroidManifest中声明,打包时会将so压缩,安装后解压到app的安装目录,会明显降低apk大小(如果so比较多的话),AGP高版本在build.gradle中换成了useLegacyPackaging编译选项,by the way extractNativeLibs在minSdk和AGP版本不同的时候,默认值不同。

4. 开启资源混淆、压缩

  • 可以使用开源的AndResGuard库做资源混淆,这会减少Resource.arsc文件的大小,同时META-INF也会减少。资源名称减少1Byte整包会有4Byte的收益(by the way AGP4.2+之后开启资源缩短的编译选项+使用AndResGuard会有BUG,这块我单独适配过,说起来都是泪啊)。另外AndResGuard也不仅仅是资源名混淆的作用,还包含重复资源复用、删除、资源再压缩、整包使用7zip重压缩,收益是非常客观的,做包体积优化这个库我是推荐必用的,但是很久没人维护了,对高版本的AGP可能不适配
  • 资源方面推荐优先处理R文件内敛、png等图片转webp,这个做了收益会很大,且几乎无风险。推荐开源库booster森哥写的,在持续维护

5. So上云

这块内容比较多,后续我可能单独写一篇文章详细讲解。目前的so上云方案都比较成熟了,网上基本也都大差不差,可以找找有没有能复用的。整体思路:打包自动抽离so ---> 运行时自动下载、按需加载

6. So压缩

这块也只提供下思路吧:这个compress${variant.name.capitalize()}Assets后 ,拿到so目录,根据配置文件使用xz压缩so后放到zip文件中,app启动后将存放so的路径添加到classLoader对so的查询路径中,随后提供sdk接口给业务方调用加载so,也可以hook系统的load或者loadLibrary方法

7. 资源的其他处理

  1. json gradle打包插件压缩成1行,去掉空格和换行
  2. dpi:不同dpi目录下同名资源只保留最高清晰度
  3. xml:把内容压缩成1行去掉多余空格和换行
  4. gif:gifsicle处理
  5. md5:md5相同资源只保留一份
  6. png:pngquant

3)如何持续优化大型应用包体积?

这块能说的内容太多了,让我缓缓,后续补充....


三、极致优化手段及收益

进入到深水区后,普通的技术优化项已经没有太明显的收益了,下面说下项目中极致优化的大致收益。 117MB -> 84.3MB,共优化33.3MB,优化收益率28+%!优化收益还是非常可观的 包含

  1. so上云
  2. so压缩
  3. AndResGuard
  4. 图片转webp
  5. access内联、R文件合并等
  6. ........

但由于篇幅限制,本文中不会聊具体实现,只谈可获得实际收益,后续可能单开文章聊技术实现

1)资源路径缩短

apk文件本质就是一个压缩文件,我们可以将资源文件的路径进行混淆(缩短)来达到减少apk文件的终极目的。听着就很极致、就很卷对吧,没错就是这么极致!

可能很多人会问:为什么只是缩短资源的路径,而不是所有文件的路径。这里会涉及到Resource.arsc文件以及Android系统的资源加载原理,我们app对于非反射方式使用的资源,实际上在编译时aapt工具就已经将资源替换成了16进制的资源ID,而Resource.arsc文件维护资源id到真实文件路径的映射关系,大多数时候我们代码中实际都是用的资源ID,而非完整的资源路径。

这个优化我推荐一个库,AndResGuard腾讯开源,但是很久不维护了,需要自己适配高版本AGP。同时AndResGuard对于资源路径优化得不够彻底,仍然保留了文件夹,极致方式应该是去掉文件夹,同时还可以优化一个资源配置多个configuration、甚至可以优化资源后缀(xml会有些特殊),资源名同化resource.arsc文件中的资源名可以同化为一个字符例如"_", 这样就所有资源都用一个符号(arsc格式中存在于String常量池中,可以复用),优化还是有较高技术成本,需要熟悉AndResGuard源码,同时修改arsc二进制文件的内容

AndResGuard在AGP4.2+以上使用会有bug,4.2之后提供了资源路径优化的开关,但是与AndResGuard会有冲突,原因是AGP优化资源路径后,可能出现a.png和A.png,但是Linux和macOS中文件系统对于名称是大小写不敏感的,通过File.exist()等api判断的时候会认为两个文件是同一个,但其实不是。熟悉AndResGuard原理的同学知道,这个库会首先解压apk文件,然后copy到tmp目录,通过exist判断文件已经存在就会少拷贝文件,最终导致资源文件丢失,运行时crash。

收益

每个资源路径优化1byte,apk的实际收益会放大4倍,达到4byte,假设每个资源名优化10byte,共有8w个资源文件需要优化。


其中:

n:每个资源名称平均优化的字节数m:优化 1 字节在 APK 所带来的收益倍数 (如 4 倍)N:可做资源优化的资源数n : \text{每个资源名称平均优化的字节数} \\ m : \text{优化 1 字节在 APK 所带来的收益倍数 (如 4 倍)} \\ N : \text{可做资源优化的资源数}

总收益为:

Total Gain (MB)=n×m×N1024×1024\text{Total Gain (MB)} = \frac{n \times m \times N}{1024 \times 1024}

假设

n=8,m=4,N=80,000n = 8, \quad m = 4, \quad N = 80,000

将以上值代入公式,计算总收益:

Total Gain (MB)=8×4×80,0001024×1024=2.44MB\text{Total Gain (MB)} = \frac{8 \times 4 \times 80,000}{1024 \times 1024} = 2.44 \, \text{MB}

这是资源缩短优化的优化收益,实际收益与理论基本匹配,平均每个资源优化字节数,与资源文件总数需要自己去预估,按照这个公式基本能预估到实际收益。

2)线上无用类分析

对于Android我们无法知道工程中有哪些代码已经不会用到了,这些不会用到的代码如果能下线会带来不错的收益。对这个优化感兴趣的同学可以看我的另一篇文章:juejin.cn/post/734606…

收益

根据dex文件解析,利用率比较高的dex如果包含8000-10000个类大小通常在3MB左右,也就是说如果下掉1w个类左右,我们是能得到3MB左右的收益的

image.png

下图是在包体积管理一站式平台搭建的无用类分析能力

image.png


四、Reference