V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
little2song
V2EX  ›  Android

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

  •  
  •   little2song · 38 天前 · 3894 次点击
    这是一个创建于 38 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    随着 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 技巧

    40 条回复    2021-10-25 12:00:42 +08:00
    geekvcn
        1
    geekvcn   38 天前   ❤️ 5
    毫无意义,一个技术过硬的码农,可以完全用 NDK 开发一个纯原生,甚至图标界面全都用代码矢量绘制,完事一个体积小巧性能飞快的 APP 诞生了,然后呢?

    然后淘宝 微信 QQ 等一众垃圾代码写的软件还内置个垃圾 webview 把省下的空间一秒钟占掉,内存也占掉,这个技术过硬的码农的软件被系统后台强杀了,流氓软件在相互唤醒保活和其他非同阵营流氓软件打架。

    不说用 NDK 开发,安卓现在能用 JAVA Kotlin 写原生软件的都不多了,webview 套壳多方便,热更新,前端随便招,软件 H5 部分全平台通用,JS 再慢又如何。
    zpxshl
        2
    zpxshl   38 天前 via Android   ❤️ 5
    @geekvcn 张口就来。
    1 主流 app 的包体积控制一直是 kpi ,包体积和转换率有正相关关系,多家实验做过了。
    2 内置 webview 在国内基本是必须的,做过的都知道国内手机 webview 的坑有多少,不同版本,各家魔改。
    3 楼主提包体积,你提保活,搭不上边吧。
    3 js 再慢又如何???
    不是所有 app 都是微信那种它做得再差用户都得用的。
    Cheons
        3
    Cheons   37 天前 via Android
    国内手机厂商坐一块把内置的 webview 标准给统一了,最简单也是最难的一步
    yuhuazhu
        4
    yuhuazhu   37 天前
    真羡慕 iOS 的 webview ,Android 不用第三方的一大堆问题要搞 T^T
    makelove
        5
    makelove   37 天前   ❤️ 1
    这个叫内存,那真的内存叫什么

    我对包体积不太在意,但很在意内存用量,象淘宝这种一打开就要用 1.6G 的 App,不知道他们内部会用低端一点的 4G 内存手机测试吗,简直没法用
    little2song
        6
    little2song   37 天前
    @makelove 文章有提到 [我们约定 手机运存 = 电脑内存,手机内存 = 电脑硬盘]
    little2song
        7
    little2song   37 天前   ❤️ 1
    @geekvcn 大哥,不要这么极端,另外,我之前参与了手 Q 开发,也使用过上面的一些方法来减小体积,纯原生的 apk 价值不大,可以做个小组件,或者工具类 App , 对于亿级流量的 app ,webview 是必须的,但是,也不至于全是 webview 套壳,事实上,手 Q 里面 Webview 的代码占比很小
    mxalbert1996
        8
    mxalbert1996   37 天前 via Android
    你对 R8 的理解是错的。R8 跟 ProGuard 一样都是 Code Shrinker ,功能也和 ProGuard 一样包括压缩混淆优化,是 Google 开发的 ProGuard 替代品。从 AGP 3.4.0 开始 R8 就是默认的 Shrinker ,如果不显式制定的话是不会用 ProGuard 的(也没有必要用,R8 比 ProGuard 更强大,能进行的优化更多)。
    little2song
        9
    little2song   37 天前
    @mxalbert1996 受教了,是我想当然了,还得多学习
    geekvcn
        10
    geekvcn   37 天前 via iPhone   ❤️ 4
    @zpxshl 别特么给自己找理由了,还 KPI ,国内手游明明可以做到启动器数据包分离,这样游戏安装包几十兆就能搞定,但是都内置数据包,完事解压运行双倍空间占用,有 KPI 脑子有坑才这样做。
    AX5N
        11
    AX5N   37 天前
    @makelove 真的内存就叫“真的内存”
    codehz
        12
    codehz   37 天前 via Android
    @geekvcn 还是国内生态的问题,没有统一商店分发,所有东西最好打包到一起才能“方便”地交给用户(运行时下载的体验实在不好),因此最好是由应用商店统一分发。不同厂商提供商店的差异很大,很多根本不支持单独分发 obb 。。。
    Caan07
        13
    Caan07   37 天前
    逆天了,我竟然不知道 iphone13p 最大内存放大到了 1T ?如果 1T 内存那 13p 的价格真的划算到极点。
    Jabin
        14
    Jabin   37 天前
    基本同意 @geekvcn
    瘦身文章一大堆, 关键还是
    1. 混淆
    2. 减少资源图片, 换用矢量图
    至于使用 webview, 就没啥可聊的了, 把 app 当浏览器使用没啥好说的
    模块 /插件化的东西不能完全算作减小 apk 体积大小的方法, 那根不不算一个完整的 apk, 用户更新安装也需要时间, 插件也占用内存, 和瘦身一起聊意思变味了
    zpxshl
        15
    zpxshl   37 天前 via Android   ❤️ 1
    @geekvcn 你世界观里是不是非黑即白。kpi 的意思是指标,不是不顾一切的指标,一个项目里面多少指标只考虑一个吗?
    王者荣耀新安装启动要下载 3g 的数据包体验很好??? 另外下载下来的数据包就不占空间吗? 下载的带宽成本呢?
    没搞过移动端你就别回复了。 作为用户你喷产品随便喷,这帖子好好讨论技术就别瞎杠了。
    zpxshl
        16
    zpxshl   37 天前 via Android
    还有一点楼主没提到,通过混淆删 kotlin 的部分生成代码,比如判空,实现简单收益良好
    zpxshl
        17
    zpxshl   37 天前 via Android
    @Jabin 反驳插件的看法。
    1 apk 的体积对用户下载留存率有正相关关系。
    2 插件一般用于非核心功能,没下载也大概率不影响用户使用。举个例子,我们内置的 webview 内核就是动态下发的,下发成功前降级使用系统的 webview 内核。
    这里包体积其实分两种,安装包的体积和安装后的体积,一般关注前者,说白了市场上大部分用户关注啥,产品就关注啥,哪个技术优化带来留存提高都是有实验的。
    NewYear
        18
    NewYear   37 天前
    路过,我家服务器也才 128G 内存,你家一部手机就 1T 内存了,您知道每增加 1G 内存就会增加多少耗电量么? 1T 内存就你那小身板的电池能扛得住?还 1T 内存,就你最离谱,讲的是专业的科普,说的却是基本名词都错。
    v2yllhwa
        19
    v2yllhwa   37 天前 via Android
    @NewYear [我们约定 手机运存 = 电脑内存,手机内存 = 电脑硬盘]
    我觉得没必要争论这个名词,尤其是在楼主已经约定了的情况下...
    loukky
        20
    loukky   37 天前 via Android
    其实不用约定,至少在塞班时代,手机运存真就等于电脑内存,手机内存真就等于电脑硬盘,前面的人真是少见多怪。
    jemyzhang
        21
    jemyzhang   37 天前
    不应该是手机内存=电脑内存,手机存储=电脑硬盘?哪里来的运存这个词
    jemyzhang
        22
    jemyzhang   37 天前   ❤️ 1
    英文 storage & memory
    loukky
        23
    loukky   37 天前 via Android
    @jemyzhang 运行内存的简称
    so898
        24
    so898   37 天前
    咋说呢,拼多多之前这样一套东西可能还有取舍,还有较为合理的 KPI 制定
    拼多多之后,开场一个 H5 ,所有 Native 组件都后面插件化下载装载,管他核心功能非核心功能……
    我们老大说过一句很经典的话:当你的应用做到拼多多那样 1M 不到的时候,用户不小心点了一个广告,还没来得及反悔,应用就下完了
    NewYear
        25
    NewYear   37 天前
    @v2yllhwa
    @loukky

    我也勉强作为一个开发者,我是真的不明白,为什么要把同一个东西搞两个名字,而不同的两个东西却使用同一个名称。

    普通人这不是更容易弄混吗?一个东西一个名称让普通人更加好记不是么?

    毕竟现在大家都有电脑和手机。两个东西都会遇到,交流的时候也会用到啊。
    AlexPUBLIC
        26
    AlexPUBLIC   37 天前   ❤️ 2
    其实压缩的奥义只有一个:一个聊天工具就干聊天的活就好,非要整什么看点,视频号,钱包;一个支付工具就做支付好了,不要总想着做社交,断舍离,组件少了,自然体积就下来了,啥都想干,去定制 OS 吧,比如 hm
    Lemeng
        27
    Lemeng   37 天前
    太用心了,有点长
    jiayong2793
        28
    jiayong2793   37 天前
    老板、领导:这不是没事找事吗?新功能写好了没?
    muzuiget
        29
    muzuiget   37 天前   ❤️ 1
    写这种文章的“内存”只是指 storage 而不是 memory ,就不想看了。
    cev2
        30
    cev2   37 天前   ❤️ 1
    下面楼都歪了,→_→我还是习惯手机上 RAM=内存,ROM=闪存的叫法
    bclerdx
        31
    bclerdx   37 天前 via Android
    @Jabin 把 app 当浏览器是不对的吧,app 应该调用外置浏览器吧。
    bclerdx
        32
    bclerdx   37 天前 via Android
    @AlexPUBLIC 不搞点啥,怎么创收呢。
    little2song
        33
    little2song   37 天前
    @muzuiget 不知道你哪来的优越感,我分享文章不是给您过目的
    sw926
        34
    sw926   36 天前
    优化包体积是一个“伪技术”,重复代码和重复资源太多,再怎么优化也没用,关键还是要编码的时候注意,代码要封装,资源要尽量复用。
    diaosi
        35
    diaosi   36 天前
    @little2song #6 不是手机开发,看到你一楼描述第一反应仍然是惊讶 iPhone 内存居然到 1T 了,再转头一想觉得怎么都不可能,才意识到你说的是存储空间。所以我们为什么要改变约定已久的称呼去重新约定呢?
    viosonlee114
        36
    viosonlee114   36 天前
    以前的手机可以插存储卡,所以有内置存储和外置存储的说法,所以很多人口中的手机内存就是指的手机内置存储。还有,有必要在这个点上批判楼主吗?非要杠精上身把愿意分享的人都轰走然后贴吧化?
    junyee
        37
    junyee   36 天前
    微信从早期的 几 MB 到现在 230MB 了.

    随便一个抓 APP,就有可能有 ffmpeg,webview ,合着全国人民都用着旗舰手机呢.
    不仅 dex 巨大, 内嵌资源还多. 多就算了,不常用的资源广告也往里塞,一联网就更新资源.
    让我们看广告也不替我们淘流量费.

    APP 版本恨不得一天一更新似的.
    lvsecoto
        38
    lvsecoto   36 天前
    aab
    Lxcm
        39
    Lxcm   36 天前 via Android
    纠结运存 内存的都是 00 后吗? 80 90 后的都经历过手机的变革,这些名词都是时代的印证,而且楼主还约定了,很好理解。那些纠结症患者是脑子转不过来吗?总是塞住不好。
    john6lq
        40
    john6lq   36 天前
    说白了还是技术的话语权没有产品的大,人家能吹牛,带来流量、拉来投资。
    所以说同一个人的开源项目基本可以秒杀他参与的商业项目。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   2564 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 14:53 · PVG 22:53 · LAX 06:53 · JFK 09:53
    ♥ Do have faith in what you're doing.