包体积优化 · 工具论 · 初识包体积优化

5,494 阅读29分钟

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

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

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

BaguTree包体积优化录播视频课(关注公众号小木箱成长营,回复“包体积优化”可免费获取课程PPT)

一、引言

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签名和对齐流程我们可以参考 yanlong107APK编译流程这篇博客。

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

3.3 Apk安装流程

传送门: www.jianshu.com/u/9ba051096…

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 -jar jd-gui-1.6.6-min.jar ./classes-Dex2jar.jar

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

四、代码优化

4.2.1 代码优化思路

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

A 代码混淆

第一部分内容,代码混淆给大家推荐ProguardProguard 是一个很强悍的工具,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 EnumerationsAndroid中代替枚举的@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 信息有效率 = define methods数量 / reference methods 数量
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-erasureaccess-marking 的优化效果一样,不仅能减少包大小,也能提升 App 的启动速度。

4.2.2 7款apk黑盒逆向工具

4.2.2.1 Google Apk Analyzer

传送门: developer.android.com/studio/debu…

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

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

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

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

4.2.2.2 ApkTool

传送门: bitbucket.org/iBotPeaches…

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

ApkTool工具打开方式:

cp apktool.jar apktool /usr/local/bin
cd /usr/local/bin && chmod +x apktool.jar apktool
apktool d 抖音.apk

4.2.2.3 Dex-tools

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

Dex-tools工具打开方式:

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

4.2.2.4 JADX

传送门: bbs.pediy.com/thread-2598…

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

JADX GUI工具打开方式:

java -jar jadx-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

传送门: 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

传送门: github.com/google/andr…

第五款逆向工具是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 -jar Classyshark.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

传送门: hex-rays.com/ida-free/#d…

第五款逆向工具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出现如下异常


private void <init>0
concurrent.AbstractIdleService$1:
woid<init>(concurrent.AbstractIdleService) public woid executeliava.lang.Runnable)
final svnthetic util.concurrent.AbstractIdleService thisso  collectImmutableSortedMultisetFauxverideShim::
public static cogoogle.common.collect.ImoutablesoctedMultiset$Builder builder() publicstaticcomgooglecommon.collect.ImmutableSortedMultisetof(java.lang.Object)
publicstaticcomgooglecommon.collecImmutableSortedMultiset of(java.lang Obiect java.lang.Obiect)
public static colecImmutableSortedMultiset of(iava. lang.0biect.fava. lang.Obiect.iava.lang.Obiect)
public static ImmutablesortedMultiset of(java.langObject ava.lang.object, java. lang, object, iava, lang.Obiect)
public static ImmutablesortedMultiset of(iaa,lang,Obiect,javalang.obiect,iava.lang.obiect,iava, lang.biect, java. lang.Obiect) public static varargs com.gooale.common.collectImmutableSortecMultiset
ofliava,lang,0biect.iava.lang.Obiect.iava.lang.Obiect.iava.lang.0biect,iava. lang,obiect,iava, lang,Obiect iava. lang.Obiecti
publicstatic com.aooglecommoncollectImmutableSortedMultiset copyOf(iav.lang.Obiectll) reflectImutableypeToInstanceMap:
public staticcom.google.comon.reflect.ImmutableTypeToInstanceHap of()
oublicstaticcomoooglecommon.reflect.ImmutableTvoeToInstanceHapSBuitder builder() private void <init>(ImmutableMap)
privatejava.lang.0bjecttrustedGetcom.googlecomonreflect.TypeToken)
syntheticvoid<init>( coectImutabepcomoogle.comonreflect.ImmutableTypeToInstanceMap$1) publicjava.lang.0bject getInstancecom.google.commonreflect.TypeToken) public java.lang.0bject getInstanceljava.lang.Class)
publicjava.lang.0bjectputInstance(cmgoogecommonreflectTypeTokenjava.lang.Object) publicjava.lang.0bject putInstanceliava.lang.Class.java.lang.0biect) io.reactivex.internal.operators.flowable,AbstractFlowablewithUpstrean:
public final org.reactivestreams.Publisher sourcel)
com.akewharton.rxbinding2.view.VienLayoutChangeEventObsevable 
public static final int ENV STG public static final int ENV PRE public static final int ENV DEV public static final int ENV GRA public static final int ENV PRD
io.reactivex.internal.operators.flowable.ElowableReduceSeedSingle:
publicvoid<init>(org.reactivestreams.PubisheriavaLang0bject.io.reactivex. functions.BiFunction io.reactivex.internal.schedulers.IoScheduler:
public woid shutdown() public int size()
private static final java.lang.String WORKERTHREAD_NAME PREFIX private static final java.lang.String EVICTOR_THREAD_NAME_PREFIX
WEV NEED AITUE TTUE
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

传送门: github.com/bytedance/B…

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

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

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

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

4.2.3.4 Simian

传送门: www.harukizaemon.com/simian/get_…

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

Similarity Analyser 2.6.0 - https://simian.quandarypeak.com
Copyright (c) 2003-2018 Simon Harris.  All rights reserved.
Simian is not free unless used solely for non-commercial or evaluation purposes.
{failOnDuplication=true, ignoreCharacterCase=true, ignoreCurlyBraces=true, ignoreIdentifierCase=true, ignoreModifiers=true, ignoreStringCase=true, threshold=6}
Found 6 duplicate lines with fingerprint 2340e9b1e2419bcb5516a5a1d9037271 in the following files:
 Between lines 70 and 82 in com/sun/corba/se/PortableActivationIDL/_ServerProxyImplBase.java
 Between lines 70 and 82 in com/sun/corba/se/spi/activation/_ServerImplBase.java
 Between lines 90 and 102 in org/omg/CosNaming/BindingIteratorPOA.java
Found 6 duplicate lines with fingerprint e94fb8a8017a3d05048dcdfb8bce8dff in the following files:
 Between lines 101 and 111 in javax/swing/plaf/synth/SynthOptionPaneUI.java
 Between lines 96 and 106 in javax/swing/plaf/synth/SynthMenuBarUI.java
Found 6 duplicate lines with fingerprint 16485a9bd0994dc56f52735c2395a7b2 in the following files:
 Between lines 290 and 295 in java/time/zone/ZoneRules.java
 Between lines 234 and 239 in java/time/zone/ZoneRules.java
Found 6 duplicate lines with fingerprint 7ca74bcd5707431bd195c0d867f5767e in the following files:
 Between lines 380 and 398 in org/omg/DynamicAny/_DynFixedStub.java
 Between lines 463 and 481 in org/omg/DynamicAny/_DynSequenceStub.java
...
Found 233 duplicate lines with fingerprint 8bc044fa6e21987c76424535dbc1fe47 in the following files:
 Between lines 77 and 377 in javax/swing/plaf/nimbus/TextFieldPainter.java
 Between lines 77 and 377 in javax/swing/plaf/nimbus/PasswordFieldPainter.java
 Between lines 77 and 377 in javax/swing/plaf/nimbus/FormattedTextFieldPainter.java
Found 382 duplicate lines with fingerprint 922ba26b84cbbf0edfabb0e25189c3b4 in the following files:
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_sv.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_es.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_fr.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_ko.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_zh_CN.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_pt_BR.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_zh_TW.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_de.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_it.java
 Between lines 81 and 482 in com/sun/org/apache/xalan/internal/res/XSLTErrorResources_ja.java
Found 141070 duplicate lines in 12134 blocks in 2406 files
Processed a total of 775314 significant (2402974 raw) lines in 7714 files
Processing time: 4.818sec
4.2.3.5 PMD

传送门: pmd.sourceforge.io/pmd-5.4.1/u…

  第五款代码分析工具是静态分析工具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

传送门: developer.android.com/studio/writ…

第六款代码分析工具是 Lint,什么是Lint呢?Lint是一种是一种由Google提供的静态代码分析器,只须在命令行调用 ./gradlew lint,Lint就能帮我们自动检索无用资源。

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

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

4.2.3.7 FireLine

传送门: magic.360.cn/zh/index.ht…

第六款代码分析工具是 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音乐、字节跳动、快手、手淘和蘑菇街等包体优化方案对咱中小企业仍然有借鉴意义。

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

优质技术方案参考