1

深入理解 android 包体积优化,给 apk 瘦身全部技巧

 2 years ago
source link: https://www.v2ex.com/t/810075
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

随着 iphone13p 最大内存放大到了 1T ,大内存手机的时代悄然降临,在 android 里面,三星也有,罗老师几年前说:如果我告诉你们我们在做 1T 的手机,你们可能以为我疯了

看看现在,估计未来会有更多手机有 1T 版,大家开始真香了。

但是,如果现在有人说:要做一个 1T 大小的 app ,那他可能是真疯了,至少未来十年不可能。因为手机内存是越大越好,你一个 app 当然是能小就小呀

Android app 的文件格式为 apk ,本文就是探讨对于一个 android apk ,有哪些方法可以减小体积

Apk 组成

要想减小体积,首先我们需要了解 apk 的构成

373c7fa912fa93d601a2bee46c76ae2d.jpg

  • 我们写的.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

image-20211023160756347

可以看到最大的 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 的结构

undefined

更详细的版本在官网,这里如果对这些结构的作用有兴趣,可以看下图的详细版本

image-20211023162712238

ProGuadrd

dex 是代码编译而来,而对于代码文件,最重要的优化就是混淆了,将方法名,属性名等变为又短又无意义的名字,不仅能缩小体积还能避免反编译被人破解

在 IDE 中,我们可以看到 qq 里面的类都是小写字母,里面的变量和方法都按字母顺序排列了,从 a 开始

image-20211023163108352

除了修改变量名,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} = 4M $$ 一张图 4M ,太离谱了!

上面是打开后在运存的占用,我们可以修改颜色通道,不然 ARGB565 来减小单个像素所占用运存,不过有点跑题,本篇我们讲的是 app 的大小,也就是所占用手机的内存(我们约定 手机运存 = 电脑内存,手机内存 = 电脑硬盘)

内存与运存中的图片存在形式是不一样的,压缩方法也不一样,很多人容易弄混

回到内存,内存中,图片是以 png ,jpg 等格式存储

我之前开发的时候都是先将 png 图片,往 tinypng 网站中压缩一下再放入,所以可以压缩图片,一般能压个三分之一~三分之二。

也可以更换图片格式,比如 webp ,svg 可以更小,android studio 也提供了对应的支持,但是没有最好的格式,只是适用场景不同

几种格式的优缺点

这里多提一下 webp ,因为这是 google 推出的,大家在谷歌浏览器下载图片的时候,一般默认下载下来就是 webp 格式,所谓更小的内存占用,本质上是对图片进行了压缩,webp 的压缩算法是 VP8 视频编码,核心逻辑就是将图片分割成更小的子块,然后预测周围像素值,预测越准,周围的像素值就可以删去,再在图片打开时算出删掉的像素

图片网络化

在微信或者 qq 聊天中,对方发来一张图片,我们在聊天窗口往往先看到一张很模糊的缩略图,当点击时才会加载出高清图,

这个思路也可以用在 apk 中,很多入口较深的高清大图,或者需要经常更新的图片,也许用户根本不看,就没有必要内置在 apk 中,看时加载即可,如果需要提前占位置,可以用缩略图代替

至于哪些图网络化,需要根据业务与用户体验来权衡了

比如淘宝,在断网情况下打开时,只有 icon 内置了

image-20211023211648469

无论是对 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 ,实现通信

image-20211023201811533

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

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

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

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

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

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

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

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

D8 Optimizations

Android 开发应该掌握的 Proguard 技巧


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK