深入理解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一样,也是对字节码的处理,有以下效果

  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 = 4 M 2^{10} * 2^{10} * 2^2 = 2^{22} = 4M 21021022=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占用。

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

  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,实现通信

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

小结

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

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

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

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

我是小松,专注于计算机以及android开发,如有兴趣,可以关注一波【小松漫步】公众号,搜集了不少电子书免费分享,感谢支持!

参考资料

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

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

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

D8 Optimizations

Android 开发应该掌握的 Proguard 技巧

以上是关于深入理解android 包体积优化,给apk瘦身全部技巧的主要内容,如果未能解决你的问题,请参考以下文章

Apk 全面瘦身详解

Android App瘦身实战

Android 面试必知点——apk瘦身/包优化

Android性能优化系列之apk瘦身

apk优化 :android:extractNativeLibs 升级gradle之后发现 打包出来的apk体积突然大了将近一倍。

Android性能优化之APK瘦身详解(瘦身73%)