深入理解android 包体积优化,给apk瘦身全部技巧
Posted 小松漫步时
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深入理解android 包体积优化,给apk瘦身全部技巧相关的知识,希望对你有一定的参考价值。
前言
随着iphone13p最大内存放大到了1T,大内存手机的时代悄然降临,在android里面,三星也有,罗老师几年前说:如果我告诉你们我们在做1T的手机,你们可能以为我疯了。
看看现在,估计未来会有更多手机有1T版,大家开始真香了。
但是,如果现在有人说:要做一个1T大小的app,那他可能是真疯了,至少未来十年不可能。因为手机内存是越大越好,你一个app当然是能小就小呀
Android app的文件格式为apk,本文就是探讨对于一个android apk,有哪些方法可以减小体积
Apk组成
要想减小体积,首先我们需要了解apk的构成
-
我们写的.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
可以看到最大的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的结构
更详细的版本在官网,这里如果对这些结构的作用有兴趣,可以看下图的详细版本
ProGuadrd
dex是代码编译而来,而对于代码文件,最重要的优化就是混淆了,将方法名,属性名等变为又短又无意义的名字,不仅能缩小体积还能避免反编译被人破解
在IDE中,我们可以看到qq里面的类都是小写字母,里面的变量和方法都按字母顺序排列了,从a开始
除了修改变量名,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一样,也是对字节码的处理,有以下效果
- 内联函数,减少调用
- 删除无用代码
- 将只有一个实现类的接口或者父类用实现类代替
- 字符串混淆所见
……
不过这个我没用过,但是感觉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
=
4
M
2^{10} * 2^{10} * 2^2 = 2^{22} = 4M
210∗210∗22=222=4M
一张图4M,太离谱了!
上面是打开后在运存的占用,我们可以修改颜色通道,不然ARGB565来减小单个像素所占用运存,不过有点跑题,本篇我们讲的是app的大小,也就是所占用手机的内存(我们约定 手机运存 = 电脑内存,手机内存 = 电脑硬盘)
内存与运存中的图片存在形式是不一样的,压缩方法也不一样,很多人容易弄混
回到内存,内存中,图片是以png,jpg等格式存储
我之前开发的时候都是先将png图片,往tinypng网站中压缩一下再放入,所以可以压缩图片,一般能压个三分之一~三分之二。
也可以更换图片格式,比如webp,svg可以更小,android studio也提供了对应的支持,但是没有最好的格式,只是适用场景不同
👇
这里多提一下webp,因为这是google推出的,大家在谷歌浏览器下载图片的时候,一般默认下载下来就是webp格式,所谓更小的内存占用,本质上是对图片进行了压缩,webp的压缩算法是VP8视频编码,核心逻辑就是将图片分割成更小的子块,然后预测周围像素值,预测越准,周围的像素值就可以删去,再在图片打开时算出删掉的像素
图片网络化
在微信或者qq聊天中,对方发来一张图片,我们在聊天窗口往往先看到一张很模糊的缩略图,当点击时才会加载出高清图,
这个思路也可以用在apk中,很多入口较深的高清大图,或者需要经常更新的图片,也许用户根本不看,就没有必要内置在apk中,看时加载即可,如果需要提前占位置,可以用缩略图代替
至于哪些图网络化,需要根据业务与用户体验来权衡了
比如淘宝,在断网情况下打开时,只有icon内置了
其他策略
无论是对Dex还是对资源进行优化,虽然安全有效,但是本质上是将原来有的东西变得更小,对apk的瘦身程度是有限的,还有一些”七伤拳“,优化率极高,但是对apk的影响也很大,需要谨慎使用。
插件化
所谓插件化,就是将apk中的非主要功能弄成独立的apk,原主apk称为宿主。
比如支付宝里面,就是搞支付的,那么他里面的什么口碑,基金,天猫一堆乱七八糟,同时功能独立的东西就非常适合做成插件,用户用到的时候再从网络加载进来,这样极大的减少了apk占用。
但是这里涉及到比较多的技术问题:
- 用户现在只有宿主apk,如何让宿主加载到插件apk里面的代码?
- android四大组件都需要到manifest中注册,插件里面的组件显然不可能提前注册到宿主的manifest中(不然注册了,插件没加载进来,会找不到类),所以如何让系统认为下载下来的插件有注册?
- 宿主与插件资源能否正确互相引用?
一般来说,通过的是代理和反射来处理,腾讯有一个shadow框架可以大致实现”零反射“,
- 复用独立安装App的源码:
- 零反射无Hack实现插件技术:
- 全动态插件框架:
- 宿主增量极小:
- Kotlin实现:
不过插件化技术不在今天的讨论范围,有兴趣可以研究下tencent-shadow
当使用了插件化后,项目基本是要重构了,相比起改改Dex和图片,这个工程量极大,但是收益也会很高
webview
这里类似于图片网络化,相对于图片,直接将整个界面都变成url,
我们手机app中的小程序一般都是url显示在webview中
相关技术可以使用jsBridge与Hybird,本质上就是通过bridge连接h5与android ios,实现通信
不过代价就是,加载速度慢于原生,还要注意防止网址篡改等
小结
本文我们讨论的是apk的瘦身方案,首先先明确了apk的主要组成部分为dex文件与资源文件
-
对于dex文件,我们可以进行混淆,字节码重排序,移除多余库与代码
-
对于资源文件,我们可以替换格式,压缩图片,网络化
除了这些常规操作,我们还可以使用插件化与Webview方法极致减少体积,但是这两个技术工程量大,而且有性能代价,需要谨慎使用。
我是小松,专注于计算机以及android开发,如有兴趣,可以关注一波【小松漫步】公众号,搜集了不少电子书免费分享,感谢支持!
参考资料
以上是关于深入理解android 包体积优化,给apk瘦身全部技巧的主要内容,如果未能解决你的问题,请参考以下文章
apk优化 :android:extractNativeLibs 升级gradle之后发现 打包出来的apk体积突然大了将近一倍。