【小木箱成长营】包体积优化系列文章:

包体积优化 · 实战论 · 怎么做包体积优化? 做好能晋升吗? 能涨多少钱?

包体积优化 · 方法论 · 揭开包体积优化神秘面纱

一、引言

Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享包体积优化·工具论·初识包体积优化。小木箱从两个维度将 Android 包体积优化工具论解释清楚,本文主要说了四个部分内容,第一部分内容是业务问题和挑战。第二部分内容是包体优化基础知识。第三部分内容是代码优化。最后部分内容是总结与展望。

代码优化分为四部分内容,第一部分内容是代码优化的思路,第二部分内容是 7 款 apk 黑盒逆向工具,第三部分内容是 7 款代码分析工具,第四部分内容是代码优化注意事项。

如果学完小木箱包体积优化的工具论、方法论和实战论,那么任何人做包体优化都可以拿到结果。

二、业务问题与挑战

2.1 为什么要做包体优化

首先我们聊聊第一部分内容包体优化面临的业务问题与挑战,三个原因解释为什么要做包体优化。

2.1.1 下载转化率

第一个原因:下载转化率。海外市场上,根据 Google Play Store 包体积和转化率分析报告显示,平均每增加 1M,转化率下降 0.17%,转化率随着 Apk 变大而降低。

国内市场上,华为应用市场流量保护是 40M。如果我们的 App 体积超过 40M,那么在下载时候便有流量安装提醒。用户的下载请求被华为应用市场拦截,用户对 App 的安装多了一层筛选,用户安装成功率会降低。

2.1.2 渠道商要求

第二个原因:许多门户 app 一般会有一个 Lite 版,为什么要求做两款功能类似的应用呢” />第一, Lite 版可以提升 app 的下载转化率。

第二, 所有 app 做到一定体量,只要和华为、OPPO、三星、小米等手机厂商进行商务合作,App 体积越大,CDN 流量费用就越高,渠道拓展就越受限制。 因此,用户下载 Lite 版可以降低集团成本。

2.1.3 app 性能影响

第三个原因:体积过大对性能负面影响。其中主要表现在三个方面,安装时间和签名校验时间、运行时内存和 ROM 空间。

2.1.3.1 安装时间和签名校验时间

第一,安装时间和签名校验时间方面,相同机型和网络环境下,如果包体越大,用户安装时间越久,签名校验的时间越久。

在编译 ODex 期间,Android 5.0 、 6.0 系统,随着包体增大,耗费时间越久。Android 7.0 以后因为混合编译,安装时长方差不如 Android5.0、6.0 系统大。

2.1.3.2 运行时内存

第二,运行时内存方面,apk 的 Resource 资源、Library 以及 Dex 类加载会占用应用一部分内存。如果 apk 体积越大,运行时内存占用越大,那么性能越差。

2.1.3.3 ROM 空间

第三, ROM 空间方面,如果应用的安装包大小为 50MB,那么启动解压之后一定会超过 50MB。

如果闪存空间不足,很可能出现“写入放大”的情况,它是闪存和固态硬盘(SSD)中一种不良的现象。

闪存在可重新写入数据前必须先擦除,而擦除操作的粒度与写入操作相比低得多,执行操作就会多次移动(或改写)用户数据和元数据。

因此,要改写数据,就需要读取闪存某些已使用的部分,更新它们,并写入到新的位置,如果新位置在之前已被使用过,还需连同先擦除;

由于闪存工作方式,必须擦除改写的闪存部分比新数据实际需要的大得多。即最终可能导致实际写入的物理资料量是写入资料量的多倍。

2.2 包体优化性能指标

因此,基于下载转换率、渠道商要求和体积过大对 app 性能等诸多业务背景,我们希望能通过包体优化,达到降低流量成本,避免由于包体过大导致用户流失的目的。包体优化性能指标也就是我们上文说到的打包后安装包大小和安装包安装速度。

三、 包体优化基础

3.1 Apk 结构

紧接着来到我们的第二部分内容,代码优化,了解代码优化之前,首先,我们先了解下 apk 文件中都包含了什么。解压 apk 包,我们能看到 apk 整体目录结构如下:

Apk 的构成主要分为五个部分。

第一部分是 Dex,主要是 class data 源码文件。

第二部分是 Resource 文件,主要是图片、xml、string 等资源文件。

第三部分是 Assets 文件,主要存放一些类似签名摘要、音频、html 默认文件等。

最后一部分是 Native Library 文件,主要是 C++编写的 so,其中 lib 下存放不同架构的 so 库。

影响包体积主要有 lib、assets 和 META-INF 文件夹里的文件以及*.Dex 、 resources.arsc 文件。

上述五个影响包体积的目录和文件具体内容可以参考下面表格

目录/文件内容
lib/so 文件目录,可能会有 armeabi、armeabi-v7a、arm64-v8a、x86、x86_64、mips,大多数情况下只需要支持 armabi 与 x86 的架构即可,如果非必需,可以考虑拿掉 x86 的部分
assets/包含应用的资源;应用可以使用 AssetManager 对象检索资源。
META-INF/包含 CERT.SF 和 CERT.RSA 签名文件,以及 MANIFEST.MF 清单文件。
*.DexDalvik/ART 虚拟机可理解的 Dex 文件格式编译的类。
resources.arsc包含已编译的资源。此文件包含 res/values/ 文件夹的所有配置中的 XML 内容。打包工具会提取此 XML 内容,将其编译为二进制文件形式,并将相应内容进行归档。此内容包括语言字符串和样式,以及未直接包含在 resources.arsc 文件中的内容(例如布局文件和图片)的路径。
3.2 Apk 打包流程

分析优化方案之前,我们首先要熟悉 APK 打包流程。因为教程面向企业级技术方案而非面试经验贴,APK 编译流程参考官网不做太深入讲解,给大家整理了两张官方的截图。

总结下来就三句话:首先用 aapt 打包资源文件, 生成 R.java 类(资源索引表)、.arsc 资源文件 和 res 文件。

其次生成 src 资源文件编译产物,aidl files 编译生成成 java 接口,并将 R.java 文件、工程源码文件、aidl.java 文件, 通过 javac 合成.class 文件。

最后.class 文件、第三方 jar 和 library,通过 dx 工具打包成 Dex 文件,最后是 apk 签名和 apk 对齐流程。apk 签名和对齐流程我们可以参考 yanlong107 的APK 编译流程这篇博客。

当然,如果同学们想更深入的了解编译产物表结构,那么大家在 B 站看一下马士兵的深入理解 java 虚拟机和唐朔飞计算机组成原理的免费课程也是可以的。毕竟不断被前端吞噬的移动端市场,如果客户端在Android 逆向或 Framework 斗角场差异化竞争,那么或许更能延续职业生命周期(题外话~)。

3.3 Apk 安装流程

传送门: https://www.jianshu.com/u/9ba051096272

Apk 打包流程我们说完了,接下来我们说一下 Apk 的安装流程,Apk 的安装流程主要讲解四个部分,第一个部分是 Apk 的安装方式.第二个部分是 Apk 安装过程,第三个部分是安装后文件所在目录,最后一个部分是卸载过程.

3.3.1 Apk 安装方式

首先我们先来说一下第一部分内容,Apk 的安装方式.Android Apk 的安装方式主要分为四种,第一种是系统程序安装,比如: 时钟,日历,设置等程序.一般由 ROM 厂商更新推送.第二种是通过 Android 市场安装,Google Play 采用 Android 市场方式进行安装.第三种是 ADB 安装,如果应用开发云真机部署 Debug 包,那么通常采用 ADB 安装方式. 最后一种是手机自带安装方式,如果想使用未发布应用商店的应用,那么我们可以采用手机自带安装方式.我们注重来说一下第二种,adb 安装,执行 adb install 命令即可。

3.3.2 Apk 安装过程

然后我们先来说一下第二部分内容,Apk 安装过程安装过程.首先复制 Apk 安装包到/data/app/pkg 目录下.然后解压并扫描安装包,把 dex 文件(Dalvilk 字节码)保存到/data/dalvik-cache 目录.其三在/data/data 目录创建对应的应用数据目录.其四 dexopt 执行应用数据目录并注册四大组件.最后,安装完成后,向手机发送安装成功的广播.

3.3.3 Apk 安装后文件所在目录

接着我们先来说一下第三部分内容,Apk 安装后文件所在目录。如果安装的是系统自带的应用程序,那么 app 是安装在/system/app 目录下,如果想要删除应用,那么需要获取 adb root 权限才行。

如果安装的不是系统自带的应用程序,那么 app 和用户数据主要存放在/data/app、/data/data 和/data/davilk-cache 三个目录下。其中,/data/app 主要是用户程序安装目录,安装时把 apk 复制到此目录下。

其中,/data/data 主要存放应用程序的数据。cd 到我们预装载应用包名,用 ls 命令查看到一些隐私存储文件,我们需要 root 权限才能访问,俗称沙箱。

最后,/data/davilk-cache 是将 apk 中的 dex 安装在/data/davilk-cache 目录下的。

3.3.4 卸载过程

最后,我们说一下卸载过程。卸载过程主要是删除安装过程中/data/data、 /data/davilk-cache 和/data/app 下创建的文件及目录。

3.4 Dex 简析

Apk 安装流程我们说完了,打包过程有一个很重要的中间产物Dex,Dex 是 Android 系统的可执行文件,包含应用程序的全部操作指令以及运行时数据。

因为 Dalvik 是一种针对嵌入式设备而特殊设计的 Java 虚拟机,所以 Dex 文件与标准的 Class 文件在结构设计上有着本质的区别。

当 Java 程序被编译成 class 文件之后,还需要使用 dx 工具将所有的 class 文件整合到一个 Dex 文件中。

Dex 文件就将原来每个 class 文件中都有的共有信息合成了一体,目的是保证其中的每个类都能够共享数据。

这在一定程度上降低了信息冗余,同时也使得文件结构更加紧凑。与传统 jar 文件相比,Dex 文件的大小能够缩减 50% 左右。

Dex 文件生成流程我们可以参考下面的图片:

听到这里,同学们可能会觉得纸上谈兵,枯燥乏味。甚至可能怀疑小木箱是从哪个博主抄的八股文。因此,为了更好学习 Dex,小木箱推荐一个可以解析 Dex 文件的工具 010 Editor。010 Editor 可以通过预置的模板让我们更清晰的了解 Dex 文件的格式, 具体使用方式参考 B 站视频【Android 逆向】Dex 文件结构详解(脱壳必学)。

由于篇幅原因,想更多了解关于 Dex 知识,欢迎移步路遥在路上的Android 逆向笔记 —— Dex 文件格式解析和有赞的浅谈 Dex进行学习。

3.5 Dex && Jar 比较

关于 Dex 和 Jar 有些同学会误解两者无必然联系,jar 是 Java Archive 缩写,中文翻译为 java二进制归档文件,可以理解为多个.class 文件打包的文件。而 Dex 是直接将.class 优化打包后的文件,dalvik虚拟机是.Dex 可执行文件

下面是.jar 和.Dex 的结构体,在逆向开发里面为了更清楚解析他们的文件格式,我们常常用 dex2jar 和 dx 进行互转。

#安装jd-gui开发者工具
1.d2j-dex2jar.sh抖音.apk/classes.Dex
#Dex2jar抖音.apk/classes.Dex->./classes-Dex2jar.jar
2.java-jarjd-gui-1.6.6-min.jar./classes-Dex2jar.jar

如果对逆向方向感兴趣,同学们可以通过传送门到看雪论坛更深入的学习。

四、代码优化

4.2.1 代码优化思路

说完 Apk 的结构,我们来到第二部分内容,即代码优化。代码优化分为五部分内容。第一部分内容是代码混淆。第二部分内容是剔除无用、重复的 SDK、精简 SDK、建立 SDK 接入规范。第三部分内容是剔除无用的代码,优化重复的代码。第四部分内容是 Dex 压缩。第五部分内容是多 Dex 关联优化等环节。

A 代码混淆

第一部分内容,代码混淆给大家推荐Proguard ,Proguard 是一个很强悍的工具,Proguard 可以帮你在代码编译时对代码进行混淆,优化和压缩。后文会详细解释Proguard 使用方法。可以通过 classShark.jar 和 python 脚本自动生成混淆文件。

B 剔除 Unused 代码、剔除重复的 SDK、精简 SDK 和引入 SDK 接入长效管理机制

第二部分内容剔除 Unused 代码,操作方式也很简单,点击 AS 左上角导航栏 Refactor 的 Remove Unused Resources 即可,但是注意不要剔除换肤的图片。

当然随着业务需求的不断迭代,有些 SDK 可能是非必要的,我们确认好业务需求后利用**dependency-analysis-android-gradle-plugin** 插件剔除无用或重复的 SDK。

但,项目中可能多个地方用到了类似功能的第三方库,比如类似 ImagLoader 和 Glide、Recyclerview 和 BRGV、Sp 和 MMKV 等等目的相同,但实现方式不一样。

经过了一次次迭代后,可能会形成历史包袱,不要轻易重组合并。包体优化不能雪中送炭,锦上添花而已,优化前提是保证核心业务稳定性。如果影响到原有的核心业务,那么就得不偿失。

为了避免出现边治理边污染的情况,建立 SDK 接入走审核、不符合包大小规定需求走申请长效管理机制势在必行。

C 剔除无用的代码,优化重复的代码

第三部分内容是剔除无用的代码,优化重复的代码。删除无用代码是指当在 mainfest 清单里面的 activity 注册四大组件时候,并不会被 Proguard 混淆影响,所以,有确认不需要的功能,我们需要把对应代码清理掉。

优化重复的代码是指随着业务的不断迭代,有些功能实际是不可用的,但业务代码沉积在工程里面,所以我们务必及时对无用的业务进行清理。

D Dex 瘦身

第四部分内容是 Dex 瘦身,我们都知道一旦 Dex 的方法数就会超过 65536 个,就必须采用 mutildex 进行分包。但是此时每一个 Dex 可能会调用到其它 Dex 中的方法,这种 跨 Dex 调用的方式会造成许多冗余信息。冗余信息分为两种,

第一种是多余的 method id,跨 Dex 调用会导致当前 dex 保留被调用 dex 中的方法 id,冗余会导致每一个 dex 中可以存放的 class 变少,最终又会导致编译出来的 dex 数量增多,而 dex 数据的增加又会进一步加重这个问题。

第二种是其它跨 dex 调用造成的信息冗余,除了需要多记录被调用的 method id 之外,还需多记录其所属类和当前方法的定义信息,这会造成 string_ids、type_ids、proto_ids 这几部分信息的冗余。

因此,为了减少跨 Dex 调用的情况,我们必须尽量将有调用关系的类和方法分配到同一个 Dex 中。但是各个类相互之间的调用关系是非常复杂的,所以很难做到最优的情况。

所幸的是,ReDex 的 CrossDexDefMinimizer 类分析了类之间的调用关系,并 使用了贪心算法去计算局部的最优解(编译效果和 dex 优化效果之间的某一个平衡点)。使用 “InterDexPass” 配置项可以把互相引用的类尽量放在同个 Dex,增加类的 pre-verify,以此提升应用的冷启动速度。

ReDex 有 5 个功能,Interdex 模块主要是类重排和文件重排、Dex 分包优化。其中对于类重排和文件重排,Google 在 Android 8.0 的时候引入了 Dexlayout,它是一个用于分析 dex 文件,并根据配置文件对其进行重新排序的库。

与 ReDex 类似,Dexlayout 通过将经常一起访问的部分 dex 文件集中在一起,程序可以因改进文件位置从而拥有更好的内存访问模式,以节省 RAM 并缩短启动时间。

不同于 ReDex 的是它使用了运行时配置信息对 Dex 文件的各个部分进行重新排序。因此,只有在应用运行之后,并在系统空闲维护的时候才会将 dexlayout 集成到 dex2oat 的设备进行编译。

Oatmeal 模块主要是直接生成 Odex 文件。

StripDebugInfo 模块主要是去除 Dex 中的 Debug 信息。

access-marking 模块主要是删除 Java access 方法。

type-erasure 模块主要是类型擦除。

2015 年,根据 facebook 官方描述,ReDex优化结果比原来小了 25%,同时启动速度提高了 30%。

2016 年 11 月 28 日,鹅厂 Bugly 团队在 ReDex 初探与 InterDex 一文中推荐大家使用 ReDex,ReDex 是通过优化客户端磁盘上的字节码达到 Dex 文件压缩 && 优化启动速度双边收益。

2020 年5月 31 日,国内字节跳动团队在抖音 Android 包体积优化探索:基于 ReDex 的 Dex 优化落地实践

表述利用 ReDex 在抖音包体积优化方面取得了一些明显的收益,优化也被同步到了其他各大 App 上。在抖音、头条和其他应用上,他们的优化对 Apk 体积的缩减普遍达到了 4%以上,对 Dex 体积的缩减则可以达到 8% ~ 10%

然而,灰天鹅事件出来了,2020 年 6 月 30 日,国内 58 技术团队卢景在Android 字节码优化工具 reDex 初探一文中表述混淆后的 releaseApk,优化体积小于 100KB。4.X 系统上冷启动速度提升 20%左右,5.0+系统上冷启动速度无明显变化。从结果上看,4.X 系统上的冷启动优化较为明显。但是线上 4.X 用户的占比非常少。

感兴趣的同学可以测试一下 ReDex 在端上收益是正向的还是反向的。

E 多 Dex 关联优化

关于多 Dex 关联优化,2015 年 06 月 15 日,美团 Android Dex 自动拆包及动态加载简介 一文中, 提到了通过MultiDex内联 R Field 来解决 R Field 过多导致 MultiDex 65536 的问题,而这一步骤对代码瘦身能够起到明显的效果。

2022 年 01 月 13 日,字节跳动抖音 Android 包体积优化探索:从 Class 字节码入手精简 Dex 体积一文中提到了ByteX ,文章表明 bytex 大幅减少了 Dex 包体积,很大地促进了抖音的用户增长,同时也优化了启动时虚拟机对 Dex 加载耗时。

2020 年 6 月 1 日,字节跳动 开源 | BoostMultiDex:挽救 Android Dalvik 机型 APP 升级安装体验一文用数据方式说明了BoostMultiDex相比 MultiDex 而言,在安装速度上有更优的体验。

F 避免使用枚举

第六部分内容是避免使用枚举,Google 官方的Remove Enumerations 和 Android 中代替枚举的@IntDef 用法,使用注解的方式替换替代枚举类,每减少一个 ENUM 可减少大约 1.0 到 1.4 KB 的大小;使用方式参考下图:

G 减少模板代码

第七部分内容是减少模板代码,因为历史包袱问题,Databinding、ViewBinding、ButterKnife 自动生成模板代码,导致增大了 class 代码体量,因此,Apk 变得更大。我们要尽量避免使用类似依赖注入框架。

H 代码库精简

第八部分内容是代码库精简,Android Support V4 包的精简可以通过下载其中的 jar 包来进行删除,但Android Support V7 包则需要减少代码中不必要的引用了,这部分的优化难度比较大,各第三方库,组件化组件等都会引用到 support 的库,很难进行剔除。可见,Android Support V4 包优化空间还比较大的。

I R8 优化

第九部分内容是 R8 优化,R8 把 desugaring、shrinking、obfuscating、optimizing 和 dexing 都合并到一步进行执行,并且,R8 会对代码进行一系列的优化操作以删除更多未使用的代码,

如果 R8 检测到从不使用给定 if-else 语句的 else 分支,则 R8 将删除 else 分支的代码,如果我们的代码仅在一个地方调用方法,R8 可能会删除该方法并在单个调用位置内联它。

如果 R8 确定一个类只有一个唯一的子类,并且该类本身未实例化(例如,一个抽象基类仅由一个具体的实现类使用),那么 R8 可以组合这两个类并从 app 中删除一个类。

R8 会删除无引用方法,经过 R8 编译生成的 dex 方法数会明显减少。

最后,时刻保持良好的编程习惯,去除重复或者不用的代码,慎用第三方库,选用体积小的第三方 SDK

J Dex 分包

第十部分内容是Dex 分包,ReDex 的 CrossDexDefMinimizer 类分析了类间的调用关系,使用了贪心算法去计算局部的最优解。

使用 “InterDexPass” 配置项可以把互相引用的类尽量放在同个 Dex,增加类的 pre-verify,以此提升应用的冷启动速度。在 ReDex 中使用 Dex 分包优化跨 dex 调用造成的信息冗余的配置代码如下所示

{
"redex":{
"passes":[
"InterDexPass",
"RegAllocPass"
]
},
"InterDexPass":{
"minimize_cross_dex_refs":true,
"minimize_cross_dex_refs_method_ref_weight":33,
"minimize_cross_dex_refs_field_ref_weight":44,
"minimize_cross_dex_refs_type_ref_weight":55,
"minimize_cross_dex_refs_string_ref_weight":66
},
"RegAllocPass":{
"live_range_splitting":false
},
"string_sort_mode":"class_order",
"bytecode_sort_mode":"class_order"
}

为了进一步减少 Dex 的数量,让每个 Dex 的方法数不浪费,即装满 65536 个方法。衡量优化效果,我们使用 Dex 信息有效率进行计算:

Dex信息有效率=definemethods数量/referencemethods数量
L 删除 Java access 方法

为了能提供内部类和其外部类直接访问对方的私有成员的能力,又不违反封装性要求,Java 编译器在编译过程中自动生成 package 可见性的静态 access$xxx 方法,并且在需要访问对方私有成员的地方改为调用对应的 access 方法。

2019 年 8 月 1 日,西瓜技术团队对外发布了西瓜视频 apk 瘦身之 Java access 方法删除企业级别技术方案,当然,在 ReDex 中也提供了 access-marking 这个功能去除代码中的 Access 方法。

在开发过程中,需要注意在可能产生 access 方法的情况下,需要适当调整,比如去掉 private 改为 package 可见性和使用 ASM 在编译时删除生成的 access 方法可以避免产生 access 方法。

在 ReDex 还有 type-erasure 的功能,type-erasure与access-marking 的优化效果一样,不仅能减少包大小,也能提升 App 的启动速度。

4.2.2 7 款 apk 黑盒逆向工具
4.2.2.1 Google Apk Analyzer

传送门: https://developer.android.com/studio/debug/apk-analyzer

第一款 apk 逆向工具是来自 Google 的apk analyzer,只要安装 Android 开发工具 AS 即可上手,第一张图是 Apk 的文件内存占比信息。

第二张图是 Dex 文件信息查看

第三张图是 Apk 文件之间的对比

Apk Analyzer有 Google AS 自带 Buff 的优势,无需下载任何插件,劣势是对混淆过的 app 进行源码阅读比较鸡肋。 接下来,小木箱从看雪论坛开源工具链筛选七款不错的 apk 分析工具。

4.2.2.2 ApkTool

传送门: https://bitbucket.org/iBotPeaches/apktool/downloads/apktool_2.7.0.jar

第一款逆向工具是 apktool,如果直接解压.apk 文件,xml 文件打开是乱码的,apktool 优势是可以用来提取 xml 文件、AndroidManifest.xml 和图片等资源文件, 劣势是反编译的 app 代码是 smail 格式,可读性为 0,需要借助其他工具进行查看。

ApkTool工具打开方式:

cpapktool.jarapktool/usr/local/bin
cd/usr/local/bin&&chmod+xapktool.jarapktool
apktoold抖音.apk
4.2.2.3 Dex-tools

第二款逆向工具是 Dex-tools,Dex-tools(以前叫 Dex2jar),可以将 Dex 格式与 Jar 文件互相转换,当然也要借用 JAVA 反编译工具 jd-gui 查看 JAVA 源码。

Dex-tools工具打开方式:

#第一步:解压抖音
#第二步:将Dex转换成jar文件
d2j-dex2jar.shclasses.Dex
#第三步:启动JD-GUI查看源码
java-jarjd-gui-1.6.6-min.jar./classes-dex2jar.jar
4.2.2.4 JADX

传送门: https://bbs.pediy.com/thread-259870.html

第三款逆向工具是 JADX,JADX 是一个集成化的反编译开发工具,还可以将源文件导出为 Android Gradle 项目。

JADX GUI 工具打开方式:

java-jarjadx-gui-1.4.5.jar
  • JADX 有以下 7 个优势: – 反编译输出 Java 代码 – 查看源码时直接显示资源名称,而不是像 jd-gui 里显示的资源 ID – 导出 Gradle 工程 – 支持.dex, .apk, .jar or .class – 反混淆 – 支持代码跳转 – 支持搜索文本,类
  • JADX 有以下 2 个劣势:

    • 资源文件可能有缺失,资源文件还是通过 apktool 来获取
    • 在反编译较大的 apk 时,如果遇到 jadx-jui 卡顿和假死的情况,可适当优化 jvm 相关参数
4.2.2.5 Nimbledroid

传送门: https://nimbledroid.com/

第四款逆向工具是 Nimbledroid,Nimbledroid 是美国哥伦比亚大学的博士创业团队研发出来的分析 Android App 性能指标的系统,分析的方式有静态和动态两种

静态分析可以分析出 APK 安装包中大文件排行榜,Dex 方法数和知名第三方 SDK 的方法数及占代码整体的比例。

动态分析可以给出冷启动时间, 列出 Block UI 的具体方法, 内存占用, 以及 Hot Methods, 从分析报告中, 可以定位出具体的优化点。

Nimbledroid 有两方面优势,第一通过申请配置 api key,可持续集成到完整的 ci/cd 工业化平台。第二 Nimbledroid 免费且接入成本不高,适合研发人力投入低的小规模业务团队。

当然 Nimbledroid 也有两方面劣势,第一 Nimbledroid 的 gradle 插件与 Jenkins 集成过程中,对 Nimbledroid 团队长期高维护提出了更苛刻要求。第二 Nimbledroid 对合规解析数据结构体缺少可调用的接口。

下面我们看一下 Nimbledroid Prd 设计

4.2.2.5.1 Apk 文件大小排行榜
4.2.2.5.2 Tinypng 自动化处理
4.2.2.5.2 Block UI 的具体方法名
4.2.2.5.3 方法数报告
4.2.2.5.4 冷启动分析报告

优化前结果

优化后结果

4.2.2.6 Android-classyshark

传送门: https://github.com/google/android-classyshark

第五款逆向工具是 android-classyshark ,android-classyshark 是一个面向 Android 开发人员的独立二进制检查工具,android-classshark 可以浏览任何的 Android 可执行文件,并且检查出信息,代码压缩。比如类的接口、成员变量等等,此外,android-classshark 还可以支持多种格式,比如说 Apk、Jar、Class、So 以及所有的 Android 二进制文件如清单文件等等。

召集 ClassyShark.jar 文件,执行下面的命令我们就可以呼唤我们的 Classy 鲨鱼了。

java-jar~/ClassyShark.jar-open~/抖音.apk
java-jarClassyshark.jar-export~/抖音.apk

选择 android-classyshark 左上角 classes 标签可以对类文件进行观察,我们看到抖音有 21 个 Dex 文件。

选择 android-classyshark 左上角第二个 Method Count 标签,可以切换到以包为视图的饼图界面,下图为抖音三方库如 leakcanary、okio、retrofit、bytedance 等对应的代码结构和类文件信息,抖音共由 34 个类组成,方法数为 1018068 个:

通过android-classyshark分析 App 的项目结构和引用库的信息,不但帮助我们大致掌握了解项目架构,而且通过观察一流企业正在使用的开源库也帮助中小企业学习竞品企业优质代码,突破行业技术壁垒。

4.2.2.7 Ida64

传送门: https://hex-rays.com/ida-free/#download

第五款逆向工具 Ida64,俗话说,工欲善其事,必先利其器,在二进制安全的学习中,使用工具检测 so 漏洞尤为重要,而 IDA 又是玩二进制的神器。有 C++基础可以玩转一下。更方便观察 so 库版本的稳定性去根据业务需求引入工程

4.2.3 7 款代码分析工具

在 CI 流水线没有打通 FireLine 之前,剔除无用代码或重复代码,需要代码分析工具进行人肉检查。下面我们来说一下 7 款代码分析工具。

4.2.3.1 Proguard

第一款代码分析工具是 Proguard, Proguard 有两方面优势,安全方面,Proguard 增加了代码被反编译的难度,一定程度上保证代码的安全。

其中, 代码混淆有三种形式:第一种形式是将代码中的各个元素,比如类、函数、变量的名字改变成无意义的名字。例如将 downloadButton 转换成字母 a。这样,逆向开发者反编译代码的时候,无法通过名字猜测用途。

第二种形式是重写代码部分逻辑。将它变成功能上等价,但是又难以理解的形式。比如它会改变循环的指令、结构体。

第三种形式是打乱代码的格式。比如多加一些空格或剔除空格,或者将一行代码写成多行,将多行代码改成一行。

Proguard有四个常用的配置文件我们需要注意一下。

文件名描述
dump.txtAPK 中所有类文件的内部结构
mapping.txt提供原始与混淆过的类、方法和字段名称之间的转换,可以通过 proguard.obfuscate.MappingReader 来解析
seeds.txt列出未进行混淆的类和成员
usage.txt列出从 APK 移除的代码

混淆之后,可以发现有一个 seeds.txt 文件,列出未进行混淆的类和成员,我们不仅可以看到没有被混淆的字段、类、方法而且也可以看到应该被混淆但未被混淆字段、类、方法。

另外一个 usage.txt 文件,可以看到从 APK 移除的代码,相当于代码是不需要的。我们根据 usage 做优化,可以直接手动删掉 usage.txt 文件里应该被移除的变量、类、方法。

Proguard 代码混淆步骤总共分为三个步骤 Shrinking、Optimization 和 Obfuscation。第一个步骤是 Shrinking,Shrinking 是压缩的意思,可以在 proguard-rules.pro 文件通过配置 -dontshrink 符号选择开启/关闭压缩。

为了以减小应用体积,移除未被使用的类和成员,Proguard 默认是开启压缩的,而且会在优化动作执行之后再次执行,因为优化后可能会再次暴露一些未被使用的类和成员。

第二个步骤是 Optimization,Optimization 是优化的意思,可以在 proguard-rules.pro 文件通过配置 dontoptimize 符号选择开启/关闭优化。optimizationpasses 表示 Proguard 对代码进行迭代优化的次数,Android 一般为 5,在字节码级别执行优化,通过这些配置可以让应用运行的更快。

第三个步骤是 Obfuscation,Obfuscation 是混淆的意思,可以在 proguard-rules.pro 文件通过配置 dontobfuscate 符号选择开启/关闭混淆。Proguard 默认开启混淆,增大反编译难度,类和类成员会被随机命名,除非用优化字节码等规则进行保护。

瘦身方面,Proguard能通过缩短字段名、缩短函数名、缩短类名、丢弃未使用的类文件和字节码深度优化等编译方式,精简Apk包体积。

Proguard 的使用也会有一些坑。第一个是,如果项目首次混淆,可能需要全局扫描所有的类和包名,可以先全量 keep,然后再逐包放开混淆。第二个是,没有序列化的内部属性类也需要 keep。第三个是,EventBus 的 java、kotlin 的 onEvent 出现如下异常


privatevoid0
concurrent.AbstractIdleService$1:
woid(concurrent.AbstractIdleService)publicwoidexecuteliava.lang.Runnable)
finalsvntheticutil.concurrent.AbstractIdleServicethissocollectImmutableSortedMultisetFauxverideShim::
publicstaticcogoogle.common.collect.ImoutablesoctedMultiset$Builderbuilder()publicstaticcomgooglecommon.collect.ImmutableSortedMultisetof(java.lang.Object)
publicstaticcomgooglecommon.collecImmutableSortedMultisetof(java.langObiectjava.lang.Obiect)
publicstaticcolecImmutableSortedMultisetof(iava.lang.0biect.fava.lang.Obiect.iava.lang.Obiect)
publicstaticImmutablesortedMultisetof(java.langObjectava.lang.object,java.lang,object,iava,lang.Obiect)
publicstaticImmutablesortedMultisetof(iaa,lang,Obiect,javalang.obiect,iava.lang.obiect,iava,lang.biect,java.lang.Obiect)publicstaticvarargscom.gooale.common.collectImmutableSortecMultiset
ofliava,lang,0biect.iava.lang.Obiect.iava.lang.Obiect.iava.lang.0biect,iava.lang,obiect,iava,lang,Obiectiava.lang.Obiecti
publicstaticcom.aooglecommoncollectImmutableSortedMultisetcopyOf(iav.lang.Obiectll)reflectImutableypeToInstanceMap:
publicstaticcom.google.comon.reflect.ImmutableTypeToInstanceHapof()
oublicstaticcomoooglecommon.reflect.ImmutableTvoeToInstanceHapSBuitderbuilder()privatevoid(ImmutableMap)
privatejava.lang.0bjecttrustedGetcom.googlecomonreflect.TypeToken)
syntheticvoid(coectImutabepcomoogle.comonreflect.ImmutableTypeToInstanceMap$1)publicjava.lang.0bjectgetInstancecom.google.commonreflect.TypeToken)publicjava.lang.0bjectgetInstanceljava.lang.Class)
publicjava.lang.0bjectputInstance(cmgoogecommonreflectTypeTokenjava.lang.Object)publicjava.lang.0bjectputInstanceliava.lang.Class.java.lang.0biect)io.reactivex.internal.operators.flowable,AbstractFlowablewithUpstrean:
publicfinalorg.reactivestreams.Publishersourcel)
com.akewharton.rxbinding2.view.VienLayoutChangeEventObsevable
publicstaticfinalintENVSTGpublicstaticfinalintENVPREpublicstaticfinalintENVDEVpublicstaticfinalintENVGRApublicstaticfinalintENVPRD
io.reactivex.internal.operators.flowable.ElowableReduceSeedSingle:
publicvoid(org.reactivestreams.PubisheriavaLang0bject.io.reactivex.functions.BiFunctionio.reactivex.internal.schedulers.IoScheduler:
publicwoidshutdown()publicintsize()
privatestaticfinaljava.lang.StringWORKERTHREAD_NAMEPREFIXprivatestaticfinaljava.lang.StringEVICTOR_THREAD_NAME_PREFIX
WEVNEEDAITUETTUE
4.2.3.2 Gradle Plugin(自定义)

第二款代码分析工具是 Gradle Plugin(自定义),我们可以自定义 Gradle Plugin,在编码过程中,规避系统 Log。这样既能规避信息泄露风险,图片格式转换,也能适当减少包体积。自定义 Gradle Plugin 在扫描合规函数、日志输出等场景非常有用的,关于 ASM + Gradle Plugin 可以参考2021 年 10 月 20 日,京东零售技术平台研发张旋发表的ASM 在隐私合规扫描中的应用实战

4.2.3.3 Coverage

传送门: https://github.com/bytedance/ByteX/blob/master/coverage/README-zh.md

第三款代码分析工具是Coverage 插件, Coverage 插件是字节跳动开源的线上无用代码分析的工具。

由于代码设计不合理以及 keep 规则限制等原因,静态代码检查无法找出所有的无用代码,因此,字节跳动提供的一套动态检测分析方案。

Coverage 插件会对所有类插装,执行 Task 时候,将信息上报服务器,如果成批用户上报。定义为用户没用到的类。

抖音其实已经用了Coverage 插件,据了解除去资源文件以外,抖音有超过 1/6 的类没有被使用,共计 3M(dex 大小 20M),如果能全部删除,将减少 5%包大小。

4.2.3.4 Simian

传送门: http://www.harukizaemon.com/simian/get_it_now.html

第四款代码分析工具是静态分析工具 Simian,Simian 是一个可跨平台使用的重复代码检测工具,能够检测代码片段中除了空格、注释及换行外的内容是否完全一致,支持的语言 Java、C、C++、Groovy 和 Swift 等十几门语言。检测结果很详细,注明了重复的类文件和位置。

SimilarityAnalyser2.6.0-https://simian.quandarypeak.com
Copyright(c)2003-2018SimonHarris.Allrightsreserved.
Simianisnotfreeunlessusedsolelyfornon-commercialorevaluationpurposes.
{failOnDuplication=true,ignoreCharacterCase=true,ignoreCurlyBraces=true,ignoreIdentifierCase=true,ignoreModifiers=true,ignoreStringCase=true,threshold=6}
Found6duplicatelineswithfingerprint2340e9b1e2419bcb5516a5a1d9037271inthefollowingfiles:
Betweenlines70and82incom/sun/corba/se/PortableActivationIDL/_ServerProxyImplBase.java
Betweenlines70and82incom/sun/corba/se/spi/activation/_ServerImplBase.java
Betweenlines90and102inorg/omg/CosNaming/BindingIteratorPOA.java
Found6duplicatelineswithfingerprinte94fb8a8017a3d05048dcdfb8bce8dffinthefollowingfiles:
Betweenlines101and111injavax/swing/plaf/synth/SynthOptionPaneUI.java
Betweenlines96and106injavax/swing/plaf/synth/SynthMenuBarUI.java
Found6duplicatelineswithfingerprint16485a9bd0994dc56f52735c2395a7b2inthefollowingfiles:
Betweenlines290and295injava/time/zone/ZoneRules.java
Betweenlines234and239injava/time/zone/ZoneRules.java
Found6duplicatelineswithfingerprint7ca74bcd5707431bd195c0d867f5767einthefollowingfiles:
Betweenlines380and398inorg/omg/DynamicAny/_DynFixedStub.java
Betweenlines463and481inorg/omg/DynamicAny/_DynSequenceStub.java
...
Found233duplicatelineswithfingerprint8bc044fa6e21987c76424535dbc1fe47inthefollowingfiles:
Betweenlines77and377injavax/swing/plaf/nimbus/TextFieldPainter.java
Betweenlines77and377injavax/swing/plaf/nimbus/PasswordFieldPainter.java
Betweenlines77and377injavax/swing/plaf/nimbus/FormattedTextFieldPainter.java
Found382duplicatelineswithfingerprint922ba26b84cbbf0edfabb0e25189c3b4inthefollowingfiles:
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_sv.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_es.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_fr.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_ko.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_zh_CN.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_pt_BR.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_zh_TW.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_de.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_it.java
Betweenlines81and482incom/sun/org/apache/xalan/internal/res/XSLTErrorResources_ja.java
Found141070duplicatelinesin12134blocksin2406files
Processedatotalof775314significant(2402974raw)linesin7714files
Processingtime:4.818sec
4.2.3.5 PMD

传送门: https://pmd.sourceforge.io/pmd-5.4.1/usage/cpd-usage.html

第五款代码分析工具是静态分析工具PMD,PMD 是一个静态源代码分析器。

PMD能找到常见的编程缺陷,如未使用的变量,空的catch块,不必要的对象创建等等。

PMD 主要关注 Java 和 Apex,我们 Android 开发用 Java 就足够了。其实我们可以自定义,因为 PMD 开放了很多帮助我们自定义规则的 API 。

PMD 具有许多内置检查(在 PMD 术语,规则中),这些检查在规则参考中针对每种语言进行了记录。我们还支持广泛的 API 来编写您自己的规则,您可以使用 Java 或作为自包含的 XPath 查询来执行。

PMD 优势是在 CI/CD 持续化部署中可以集成到流水线中。PMD 支持方式也是多样化,Maven、Ant、Gradle 和命令行 PMD 都支持。下面看一下我们的检测报告如下:

4.2.3.6 Lint

传送门: https://developer.android.com/studio/write/lint

第六款代码分析工具是 Lint,什么是 Lint 呢” />

Lint 在检索完之后,Lint 会提供一份详细的 Lint 分析报告,通过 Lint 分析报告。我们有选择性的剔除“UnusedResources:Unused resources” 区域下的无用资源即可。

当然现实的企业开发,Lint 远远无法满足企业开发的,我们常常需要自定义定制 Lint 工具链,自定义定制 Lint 工具链可以参考朱利源的 Android Lint进行学习。

4.2.3.7 FireLine

传送门: http://magic.360.cn/zh/index.html

第六款代码分析工具是 FireLine,Fireline 是 360 公司技术委员会牵头,Web 平台部 Qtest 团队开发的一款免费静态代码分析工具。主要针对移动端 Android 产品进行静态代码分析。其最为突出的优点就是资源泄漏问题的全面检测。同时,火线与 360 信息安全部门合作,推出了一系列针对移动端安全漏洞的检测规则。360 火线提供免费使用,扫描速度快,并支持 Android Studio 插件,Jenkins 插件,Gradle 部署等多种集成方式。

火线拥有四大类规则,分别为安全类,内存类,日志类,基础类。

• 安全类:根据 360 信息安全部门最权威的 SDL 专门定制,每一条 SDL 都有真实的攻击案例

• 内存类:各种资源关闭类问题检测(本次评测的重点)

• 日志类:检测日志输出敏感信息内容的规则

• 基础类:规范类、代码风格类、复杂度检查规则

对于 Android 重复代码检测,FireLine 有着出乎意料的视觉体验,尽管目前 360 团队不再维护 FireLine,但是 Prd 设计仍值得每家互联网公司深度学习,希望 360 能继续做出类似 FireLine 优秀的工业产品。

4.2.4 注意事项

关于代码优化有四点注意事项,第一点是 sdk 接入标准,第二点是低代码入侵业务,第三点是 SDK 去重,最后一点是 SDK 代码剥离引入。

4.2.4.1 SDK 准入原则

我们先来说第一点,不要为了某个小功能就随意引入 sdk,可以考虑源码接入,小组邮件通知审核之后才进行 sdk 是否接入。

4.2.4.2 低代码入侵业务

然后我们说说第二点,选择第三方 SDK 的时候,我们可以将包大小作为选择的指标之一,我们应该尽可能地选择那些比较小的库来实现相同的功能。

4.2.4.3 SDK 去重

接着我们说说第三点,不要选择重复功能的 sdk。如果有,可以考虑去掉其他的,比如用了高德地图就不用使用百度地图了。

4.2.4.4 SDK 代码剥离引入

最后我们说说第四点,某些库支持部分功能分离,不需要引入整个包比如高德地图,如果只使用定位功能,就不要加入 map 能力了。

五、 总结与展望

本文主要说了三个部分内容,第一部分内容是业务问题和挑战。第二部分内容是包体优化基础知识。第三部分内容是代码优化。代码优化分为四部分内容,第一部分内容是代码优化的思路,第二部分内容是 7 款 apk 黑盒逆向工具,第三部分内容是 7 款代码分析工具,最后一部分内容是代码优化注意事项。

近些年来,中大厂门户 app 不断成熟,功能不断堆积和迭代,Android 打包后体积越来越大。安装包体大小不仅对用户留存、市场推广有负面影响,而且如果后续缺乏长效治理监管机制,那么包体大小会出现边治理边污染的现象。官方推荐、微信、美团、QQ 音乐、字节跳动、快手、手淘和蘑菇街等包体优化方案对咱中小企业仍然有借鉴意义。

下一篇方法论会从上而下带大家揭秘常见包体优化场景。我是小木箱,我们下一篇见~

优质技术方案参考

  • https://developer.android.com/topic/performance/reduce-apk-size
  • https://developer.android.com/studio/build/shrink-code?hl=zh-cn
  • https://www.guardsquare.com/manual/configuration/examples
  • 包体积优化(上):如何减少安装包大小?
  • 包体积优化(下):资源优化的进阶实践
  • 支付宝 App 构建优化解析:Android 包大小极致压缩
  • 美团 Android App 包瘦身优化实践
  • 美团 Android 对 so 体积优化的探索与实践
  • 字节 抖音 Android 包体积优化探索:从 Class 字节码入手精简 Dex 体积
  • 字节 抖音 Android 包体积优化探索:资源二进制格式的极致精简
  • 字节 抖音 Android 包体积优化探索:基于 ReDex 的 Dex 优化落地实践
  • 货拉拉 Android 包体积优化实践
  • 有道词典 Android 客户端包体积优化之路
  • 百度 App Android 包体积优化实践(一)总览
  • 百度 App Android 包体积优化实践(二)Dex 行号优化
  • 百度 APP Android 包体积优化实践(三)资源优化
  • 得物 App 包体积治理之路

本文由 mdnice 多平台发布