货拉拉Android 包体积优化实践
点击上方蓝字关注我,知识会给你力量
❝作者简介
❞
muye,货拉拉客户端架构师,货拉拉App Android端技术负责人,在Android App性能优化、稳定性提升等方向有丰富经验
背景介绍
为什么要做包体积优化?主要出于以下几方面的考虑
下载转化率
(1)很多应用市场流量保护限制是40M
(2)很多大型 App 一般都会有一个 Lite 版本的 App,也是出于下载转化率方面的考虑对app性能的影响
(1)安装时间:比如 文件拷贝、Library 解压,并且,在编译 ODEX 的时候,特别是对于 Android 5.0 和 6.0 系统来说,耗费的时间比较久,而 Android 7.0 之后有了 混合编译,所以还可以接受。最后,App 变大后,其 签名校验 的时间也会变长。
(2)运行时内存:Resource 资源、Library 以及 Dex 类加载都会占用应用的一部分内存。
(3)ROM 空间:如果应用的安装包大小为 50MB,那么启动解压之后很可能就已经超过 100MB 了CDN流量费用增加
安装包体积越大,单个apk下载流量越大
优化思路分析
apk主要由以下4个部分构成
(1)代码相关的dex文件
(2)资源相关的resources.arsc和清单文件等
(3)so相关的lib目录下的文件
(4)系统签名文件
所以我们的优化思路如下:
从apk整体优化
(1)插件化
动态加载安装包中的部分代码、资源、so
(2)动态资源加载
动态加载安装包中的资源、so代码相关的dex文件优化
(1)代码混淆
使用更短的混淆字段来混淆原始的类名、方法名、变量名
(2)删除未使用的代码
完全没使用的代码、只使用了一小部分却引入了整体功能的代码
(3)删除重复的代码
完全重复的代码、部分方法重复的代码
(4)sdk优化
重复功能的sdk(比如图片加载库)、只使用了部分功能却引入了完整的sdk
(5)dex压缩
使用更高压缩率的算法、可以替换成常量的字节码
(6)多dex关联优化
减少跨Dex调用的冗余信息
(7)字节码优化
方法内联、常量内联、优化多余赋值指令、getter和setter方法内联、删除日志等资源相关的文件优化
(1)shrinkResources
将项目中没有使用的图片、xml资源文件使用系统自带的资源替换
(2)删除未使用的资源
(3)删除重复资源
(4)AndResGuard-资源混淆
使用更短的混淆名来混淆原始的资源名
(5)属性代码替换shape xml文件
(6)语言资源优化
去掉多余的国际语言资源
(7)图片资源优化
图片格式优化、图片压缩、图片分辨率优化
(8)本地图片转网图
编译时将本地的图片从apk中删除,改成网络动态加载的方式so相关的文件优化
移除多余的so架构、移除调试符号
工欲善其事必先利其器,为了实现我们的优化效果,我们引入和开发部分工具、插件来帮助我们做apk、代码、资源的相关分析
货拉拉Android 包体积优化思维导图
APK构成
APK构成
apk各个部分的详细信息:
dex简介
Dex 是 Android 系统的可执行文件,包含 应用程序的全部操作指令以及运行时数据。因为 Dalvik 是一种针对嵌入式设备而特殊设计的 Java 虚拟机,所以 Dex 文件与标准的 Class 文件在结构设计上有着本质的区别。当 Java 程序被编译成 class 文件之后,还需要使用 dx 工具将所有的 class 文件整合到一个 dex 文件中,这样 dex 文件就将原来每个 class 文件中都有的共有信息合成了一体,这样做的目的是 保证其中的每个类都能够共享数据,这在一定程度上 降低了信息冗余,同时也使得 文件结构更加紧凑。与传统 jar 文件相比,Dex 文件的大小能够缩减 50% 左右
apk打包过程
(1)简化版
(2)详细版
Apk分析工具
zip解压
直接把apk后缀改成zip,然后解压缩
解压之后可以看到apk的各个组成部分
Android Studio自带的Analyze APK
直接在Android Studio中点击打开apk文件即可,打开之后可以看到各个部分的大小
还可以做apk的对比分析,可以看到新旧版本的体积对比,更直接客观的看出新版本哪部分体积增加了及哪部分体积减少了
另外,还可以点开每个dex文件,查看里面的具体类信息
反编译工具APKTool
APKTool主要包含三个部分:apktool、dex2jar、jd-gui,作用分别如下:
apktool
作用:资源文件获取,可以提取出图片文件和布局文件进行使用查看dex2jar 作用:将apk反编译成java源码(classes.dex转化成jar文件)
jd-gui 作用:查看APK中classes.dex转化成出的jar文件,即源码文件
反编译命令如下:
java -jar apktool_2.3.4.jar apktool d app-release.apk
反编译后可以得到smali码,通过jd-gui可以打开如下:
class-shark
(1)android-classshark 是一个 面向 Android 开发人员的独立二进制检查工具,它可以 浏览任何的 Android 可执行文件,并且检查出信息,比如类的接口、成员变量等等,此外,它还可以支持多种格式,比如说 APK、Jar、Class、So 以及所有的 Android 二进制文件如清单文件等等
(2)传送门
https://github.com/google/android-classyshark
(3)使用方式
双击打开 ClassShark.jar,拖动我们的 APK 到它的工作空间即可。接下来,我们就可以看到 Apk 的分析界面了,这里我们点击 classes 下的 classes.dex,在分析界面 左边 可以看到该 dex 的方法数和文件大小,并且,最下面还显示出了该 dex 中包含有 Native Call 的类
(4)点击左上角的 Methods count 还可以切换到 方法数环形图标统计界面,我们不仅可以 直观地看到各个包下的方法数和相对大小,还可以看到各个子包下的方法数和相对大小
nimbledroid
(1)nibledroid 是美国哥伦比亚大学的博士创业团队研发出来的分析 Android App 性能指标的系统,分析的方式有静态和动态两种方式
(2)传送门
https://nimbledroid.com/
(3)静态分析:可以分析出APK安装包中大文件排行榜,Dex 方法数和知名第三方 SDK 的方法数及占代码整体的比例
(4)动态分析:可以给出 冷启动时间, 列出 Block UI 的具体方法, 内存占用, 以及 Hot Methods, 从这些分析报告中, 可以 定位出具体的优化点
ApkChecker
(1)简介
ApkChecker是微信APM系统Matrix中的一个针对android安装包的分析检测工具
针对android安装包的分析检测工具,根据一系列设定好的规则检测apk是否存在特定的问题,并输出较为详细的检测结果报告,用于分析排查问题以及版本追踪。
(2)传送门
https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker
(3)怎么使用
Matrix-ApkChecker以一个jar包的形式提供使用,通过命令行执行 java -jar ApkChecker.jar 即可运行
(4)能统计啥
fileSize 列出超过一定大小的文件,可按文件后缀过滤,并且按文件大小排序
--min 文件大小最小阈值,单位是KB
--order 按照文件大小升序(asc)或者降序(desc)排列
--suffix 按照文件后缀过滤,使用","作为多个文件后缀的分隔符
countMethod 统计方法数
group 输出结果按照类名(class)或者包名(package)来分组
checkResProguard 检查是否经过了资源混淆(AndResGuard)
findNonAlphaPng 发现不含alpha通道的png文件
min png文件大小最小阈值,单位是KB
checkMultiLibrary 检查是否包含多个ABI版本的动态库
uncompressedFile 发现未经压缩的文件类型(即该类型的所有文件都未经压缩)
suffix 按照文件后缀过滤,使用","作为多个文件后缀的分隔符
countR 统计apk中包含的R类以及R类中的field count
duplicatedFile 发现冗余的文件,按照文件大小降序排序
checkMultiSTL 检查是否有多个动态库静态链接了STL
toolnm nm工具的路径
unusedResources 发现apk中包含的无用资源
rTxt R.txt文件的路径(如果在全局参数中给定了--input,则可以省略)
ignoreResources 需要忽略的资源,使用","作为多个资源名称的分隔符
unusedAssets 发现apk中包含的无用assets文件
ignoreAssets 需要忽略的assets文件,使用","作为多个文件的分隔符
unstrippedSo 发现apk中未经裁剪的动态库文件
(5)检测结果示例
代码分析工具
1、Proguard
proguard代码混淆时会生成dump、mapping、seeds、usage4个文件,如下:
以seeds.txt为例,会列出当前混淆规则下没有被混淆的类和成员,为后续进一步混淆优化提供指导
2、lint分析插件
Android Studio自带lint分析插件,以分析未使用声明为例
Analyze -> Run Inspection by Name -> unused declaration
此外还可以分析项目中未使用的class和resources资源等
3、自定义lint分析插件
除了AS自带的lint插件,还可以自定义lint插件,自定义lint插件可以扫描以下几类文件
(1)JavaScanner / JavaPsiScanner / UastScanner:扫描 Java 源文件
(2)XmlScanner:扫描 XML 文件
(3)ClassScanner:扫描 class 文件
(4)BinaryResourceScanner:扫描二进制资源文件
(5)ResourceFolderScanner:扫描资源文件夹
(6)GradleScanner:扫描 Gradle 脚本
(7)OtherFileScanner:扫描其他类型文件
以扫描java源文件为例,以下扫描项目中所有的Log日志代码:
4、自定义gradle插件
(1)自定义gradle插件的三种方式
Build script:在build.gradle
构建脚本中直接使用,只能在本文件内使用;
buildSrc project:新建一个名为buildSrc
的Module使用,只能在本项目中使用;
Standalone project:在独立的Module中使用,可以发布到本地或者远程仓库供其他项目使用。
(2)支持的语言
可以使用多种语言来实现Gradle插件,其实只要最终被编译为JVM字节码的都可以,常用的有Groovy
、Java
、Kotlin
(3)Build script示例
(4)buildSrc project和Standalone project使用
本质上实现方式一样
首先自定义插件类implements Plugin
实现「void」 apply(Project project)方法注册自定义的Transform类;
然后自定义类extends Transform,在transform()方法中实现自定义逻辑,例如可以自定义
ClassVisitor类访问和修改类的相关属性,自定义MethodVisitor类访问和修改方法的相关属性
5、coverage插件
(1)简介
coverage插件是由字节跳动开源的线上无用代码分析工具
(2)原理
由于代码设计不合理以及keep规则限制等原因,静态代码检查无法找出所有的无用代码。
我们可以从用户的角度去分析,对每个类插桩,执行时将信息上报到服务器。基于大量用户上报,用户没有用到的类可以被定义为无用类。
在抖音项目中,我们发现了1/6的无用类,不包含其引用的资源,共计3M(dex大小20M),如果能全部删除,将减少5%包大小
(3)传送门
https://github.com/bytedance/ByteX/blob/master/coverage/README-zh.md
6、pmd检测重复代码
(1)简介
PMD是一个静态源代码分析器。它找到常见的编程缺陷,如未使用的变量,空的catch块,不必要的对象创建等等。它主要关注Java和Apex,但支持其他六种语言。
PMD具有许多内置检查(在PMD术语,规则中),这些检查在规则参考中针对每种语言进行了记录。我们还支持广泛的API来编写您自己的规则,您可以使用Java或作为自包含的XPath查询来执行。
在集成到构建过程中时,PMD最有用。
(2)支持的4种运行方式
作为Maven的目标
作为Ant任务
作为Gradle任务
从命令行
(3)传送门
https://pmd.sourceforge.io/pmd-5.4.1/usage/cpd-usage.html
(4)检测结果示例
使用命令行方式:
./run.sh cpd --language java --minimum-tokens 100 --files /Users/xxxx/Work/code/DeliciousFood/Classes > ~/Desktop/codeCheck.txt
7、Simian检测重复代码
(1)Simian是一个可跨平台使用的重复代码检测工具,能够检测代码片段中除了空格、注释及换行外的内容是否完全一致,且支持的语言包括:
Java C# C++ C Objective-C JavaScript (ECMAScript) COBOL, ABAP Ruby Lisp SQL Visual Basic Groovy Swift
(2)传送门
http://www.harukizaemon.com/simian/get_it_now.html
(3)simian检测结果示例
代码体积优化
1、代码优化小建议
(1)时刻保持良好的编程习惯,去除重复或者不用的代码,慎用第三方库,选用体积小的第三方SDK
(2)尽量不要使用自动生成的代码的sdk 比如butterknife和viewbinding、databinding
(3)减少ENUM的使用,避免使用枚举 单个枚举会使应用的 classes.dex 文件增加大约 1.0 到 1.4KB 的大小 请考虑使用 @IntDef 注释
2、代码混淆Proguard
(1)作用
混淆器的 作用 不仅仅是 保护代码,它也有 精简编译后程序大小 的作用,其 通过缩短变量和函数名以及丢失部分无用信息等方式,能使得应用包体积减小。
i 瘦身:它可以检测并移除未使用到的类、方法、字段以及指令、冗余代码,并能够对字节码进行深度优化。最后,它还会将类中的字段、方法、类的名称改成简短无意义的名字。
ii 安全:增加代码被反编译的难度,一定程度上保证代码的安全。
(2)代码混淆形式
代码混淆的形式主要有 三种,如下所示:
i:将代码中的各个元素,比如类、函数、变量的名字改变成无意义的名字。例如将 hasValue 转换成单个的字母 a。这样,反编译阅读的人就无法通过名字来猜测用途。
ii:重写 代码中的 部分逻辑,将它变成 功能上等价,但是又 难以理解 的形式。比如它会 改变循环的指令、结构体。
iii:打乱代码的格式,比如多加一些空格或删除空格,或者将一行代码写成多行,将多行代码改成一行。
(3)Proguard踩坑经验
i:如果项目首次混淆,可能需要全局扫描所有的类和包名,可以先全量keep,然后再逐包放开混淆
ii:EventBus的java、kotlin的onEvent的坑
iii:没有序列化的内部属性类也需要keep
3、sdk优化
(1)sdk接入标准
i:不要为了某个小功能就随意引入sdk,可以考虑源码接入
ii:邮件通知审核sdk是否接入
(2)选择第三方 SDK 的时候,我们可以将包大小作为选择的指标之一,我们应该 尽可能地选择那些比较小的库来实现相同的功能
(3)不要选择重复功能的sdk,如果有,可以考虑去掉其他的 Picasso、Glide、Fresco
(4)某些库支持部分功能分离,不需要引入整个包 比如 Fresco,它将图片加载的各个功能,如 webp、gif 功能进行了剥离,它们都处于单个的库当中
4、删除重复的代码
可以使用上面的pmd和simian工具扫描出重复的代码
5、删除未使用的代码
可以使用上面的coverage插件来辅助统计出未使用的代码
6、dex压缩
(1)内联R Field
通过内联 R Field 来进一步对代码进行瘦身,此外,它也解决了 R Field 过多导致 MultiDex 65536 的问题。要想实现内联 R Field,我们需要 通过 Javassist 或者 ASM 字节码工具在构建流程中内联 R Field
实现原理:
android 中的 R 文件,除了 styleable 类型外,所有字段都是 int 型变量/常量,且在运行期间都不会改变。所以可以在编译时,记录 R 中所有字段名称及对应值,然后利用 ASM 工具遍历所有 Class,将除 R$styleable.class 以外的所有 R.class 删除掉,并且在引用的地方替换成对应的常量
使用工具:ThinRPlugin(美丽说团队开源)
插件部分实现:
(2)dex压缩--XZ Utils
i:XZ Utils 是具有高压缩率的免费通用数据压缩软件,它同 7-Zip 一样,都是 LZMA Utils 的后继产品,内部使用了 LZMA/LZMA2 算法。LZMA 提供了高压缩比和快速解压缩,因此非常适合嵌入式应用
ii:缺点 压缩 Dex 的方式,那么首次生成 ODEX 的时间可能就会超过1分钟
iii:传送门
https://tukaani.org/xz/
7、多dex关联优化-ReDex
(1)背景
Dex 的方法数就会超过65536个,因此,必须采用 mutildex 进行分包,但是此时每一个 Dex 可能会调用到其它 Dex 中的方法,这种 跨 Dex 调用的方式会造成许多冗余信息 (1)多余的 method id:跨 Dex 调用会导致当前dex保留被调用dex中的方法id,这种冗余会导致每一个dex中可以存放的class变少,最终又会导致编译出来的dex数量增多,而dex数据的增加又会进一步加重这个问题。(2)其它跨dex调用造成的信息冗余:除了需要多记录被调用的method id之外,还需多记录其所属类和当前方法的定义信息,这会造成 string_ids、type_ids、proto_ids 这几部分信息的冗余。
(2) ReDex方案
为了减少跨 Dex 调用的情况,我们必须 尽量将有调用关系的类和方法分配到同一个 Dex 中。但是各个类相互之间的调用关系是非常复杂的,所以很难做到最优的情况。所幸的是,ReDex 的 CrossDexDefMinimizer 类分析了类之间的调用关系,并 使用了贪心算法去计算局部的最优解(编译效果和dex优化效果之间的某一个平衡点)。使用 "InterDexPass" 配置项可以把互相引用的类尽量放在同个 Dex,增加类的 pre-verify,以此提升应用的冷启动速度
(3)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 模块:类型擦除。
(4)传送门
https://fbredex.com/docs/installation
资源体积优化
1、图片格式优化
(1)如果能用VectorDrawable
来表示的话优先使用VectorDrawable,如果支持WebP
则优先用WebP,而PNG
主要用在展示透明或者简单的图片,而其它场景可以使用JPG
格式。针对每种图片格式也有各类的优化手段和优化工具。
(2)使用矢量图片
可以使用矢量图形来创建独立于分辨率的图标和其他可伸缩图片。使用矢量图片能够有效的减少App中图片所占用的大小,矢量图形在Android中表示为VectorDrawable对象。使用VectorDrawable对象,100字节的文件可以生成屏幕大小的清晰图像,但系统渲染每个VectorDrawable对象需要大量的时间,较大的图像需要更长的时间才能出现在屏幕上。因此只有在显示小图像时才考虑使用矢量图形。有关使用VectorDrawable的更多信息,请参阅 Working with Drawables。
(3)使用WebP
如果App的minSdkVersion
高于14(Android 4.0+
)的话,可以选用WebP格式,因为WebP在同画质下体积更小(WebP支持透明度,压缩比比JPEG更高但显示效果却不输于JPEG,官方评测quality参数等于75均衡最佳), 可以通过PNG到WebP转换工具来进行转换。
2、图片压缩
(1)png格式图片可以在tinyPng网站上压缩,或者使用pngcrush、pngquant或zopflipng等工具压缩
而不会丢失图像质量。所有这些工具都可以减少PNG文件大小,同时保持图像质量。
pngcrush工具特别有效:此工具在PNG过滤器和zlib(Deflate)参数上迭代,使用过滤器和参数的每个组合来压缩图像。然后选择产生最小压缩输出的配置
(2)JPEG文件,可以使用packJPG或guetzli等工具将JPEG文件压缩的更小,这些工具能够在保持图片质量不变的情况下,把图片文件压缩的更小。guetzli工具更是能够在图片质量不变的情况下,将文件大小降低35%
3、开启资源压缩shrinkResources
(1)Android的编译工具链中提供了一款资源压缩的工具,可以通过该工具来压缩资源,如果要启用资源压缩,可以在build.gradle文件中将shrinkResources true
(2)需要注意的是,Android构建工具是通过ResourceUsageAnalyzer来检查哪些资源是无用的,
当检查到无用的资源时会把该资源替换成预定义的版本。主要是针对 .png、.9.png、.xml 提供了 TINY_PNG、TINY_9PNG、TINY_XML 这 3 个 byte 数组的预定义版本。资源压缩工具默认是采用 安全压缩模式 来运行,可以通过开启 严格压缩模式 来达到 更好的瘦身效果
4、语言资源优化
语言资源优化 让构建工具移除指定语言之外的所有资源(可以删除sdk里面的语言资源) resConfigs "zh", "zh-rCN"
5、图片分辨率优化
根据项目实际需要,大部分图片可以只保留一套xxhdpi图片
6、属性代码替代shape xml
(1)背景
项目中为了满足ui需求,使用了大量android shape来生成各式各样的背景。这些背景大多数只有圆角,描边,填充色等信息不一样,但是种类繁多,无法兼容,需要我们使用大量的xml文件来生产多种多样的背景,目前项目中多达数百个
(2)解决思路
i:自定义HllRoundBackground类来构建一个android 原生GradientDrawable来表示背景shape
ii:自定义属性来表示常用的android shape属性,包含圆角,填充色,描边,根据状态改变填充颜色,描边颜色,字体颜色,以及渐变色属性
iii:继承android原生LayoutInflater.Factory2类,用它来生成View,并检查该View上是否有自定义属性,如果有尝试生成背景,并设置到该View上。把该Factory注入到系统中,必要的时候,由我们代替系统的LayoutInflater创建View
(3)代码设计方案
(4)使用示例
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="20元"
app:hll_corners_radius = "6dp"
app:hll_stroke_width = "1dp"
app:hll_solid_normal_color = "@color/white"
app:hll_solid_selected_color = "@color/color_0dff6600"
app:hll_stroke_normal_color = "@color/gray_15_percent"
app:hll_stroke_selected_color = "@color/color_ff6600"/>
7、资源混淆-AndResGuard
(1) AndResGuard方案
直接处理apk. 不依赖源码,不依赖编译过程,仅仅输入一个安装包,得到一个混淆包
(2)AndResGuard处理流程
i:resources.arsc:它记录了资源文件的名称与路径,使用混淆后的短路径 res/s/a,可以减少文件的大小。
ii:metadata 签名文件:签名文件 MANIFEST.MF 与 CERT.SF 需要记录所有文件的路径以及它们的哈希值,使用短路径可以减少这两个文件的大小。
iii:ZIP 文件:ZIP 文件格式里面通过其索引记录了每个文件 Entry 的路径、压缩算法、CRC、文件大小等等信息。短路径的优化减少了记录文件路径的字符串大小
AndResGuard工作流程图
(3)传送门
https://github.com/shwenzhang/AndResGuard
(4)与7z极限压缩
(5)AndResGuard混淆后的资源名
(6)AndResGuard踩坑经验
i:资源混淆之白名单
代码扫描调用getIdentifier()方法的地方
ii:开启7zip压缩之后会影响图片加载速度,会对app启动速度有点影响
8、删除重复资源
可以使用上面的ApkChecker工具扫描出apk中重复的资源
9、删除未使用资源
(1)可以使用上面的ApkChecker工具扫描出apk中没有使用的资源
(2)也可以使用Android Studio自带的lint插件扫描项目中没有使用的资源
Analyze -> Run Inspection by Name -> unused Resources
其他包体积优化方案
1、资源动态加载方案
(1)原理
把一些使用频率相对低一些的资源不打包进apk,需要的时候在下载到本地进行使用(这些资源可能包括动画文件、字体文件、so库、zip压缩包等)
(2)资源动态加载架构图
(3)部分类UML设计
(4)资源动态配置示例
(5)动态so加载
i:正常so加载流程
安装app的时候,PMS会把指定架构的so库,拷贝到 data/data/[包名]/lib 下面
启动app的时候,会把系统的so文件夹,以及 安装包的so文件夹位置 给 BaseDexClassLoader 中的属性DexPathList 下面属性的 nativeLibraryDirectories 和 systemNativeLibraryDirectories 两个File集合
调用及使用 调用:System.loadLibrary("xxx")
ii:动态加载so方案
System.loadLibrary()和System.load()最后都会调用DexPathList 的 findLibrary(), 通过 DexPathList 中的 nativeLibraryDirectories 和systemNativeLibraryDirectories两个文件夹集合,生成一个NativeLibraryElement[],然后从这里面找对应的so,返回全路径
hook了DexPathList 中的 nativeLibraryDirectories,在这个文件夹集合中又添加一个自定义的文件夹
流程图如下:
2、本地图片转网图
(1)原理
编译时 (1)上传图片 (2)删除图片源文件 (3)保存链接信息
运行时 (1)解析链接信息 (2)Hook Android Drawable图片加载流程 (3)自定义Drawable,触发网络图片下载,还原系统的Drawable图片绘制流程
(2)aapt流程图
(3)本地图片转网图流程图
(4)编译时删除本地图片
(5)运行时加载图片
3、字节码优化Bytex
(1)Bytex简介
ByteX字节跳动团队开发的一个基于gradle transform api和ASM的字节码插件平台(或许,你可以把它当成一个有无限个插头的插座?)
目前集成了若干个字节码插件,每个插件完全独立,既可以脱离ByteX这个宿主而独立存在,又可以自动集成到宿主和其它插件一起整合为一个单独的Transform。插件和插件之间,宿主和插件之间的代码是完全解耦的(有点像组件化),这使得ByteX在代码上拥有很好的可拓展性,新插件的开发将会变得更加简单高效
(2)传送门
ByteX/README_zh.md at master · bytedance/ByteX
(3)字节码优化功能
i:优化多余赋值指令 (field-assign-opt-plugin)
编译期间去除代码中不必要或者重复的赋值(默认值)代码,在虚拟机实例化时分配的内存中默认会给予默认值,所以代码中的默认值是多余的,如下:
private boolean aBoolean = false;
private byte aByte = 0;
private short aShort = 0;
private char aChar = '\u0000';
private int anInt = 0;
private float aFloat = 0f;
private double aDouble = 0d;
private long aLong = 0l;
ii:删除某些方法调用 (method-call-opt-plugin)
比如我们的调试日志Log.d()只是在开发调试阶段使用,发布包完全不需要此类代码
iii:常量内联(const-inline-plugin)
编译期间内联并优化掉项目中的编译期间常量字段,插件将对编译期常量的运算(对应GETFIELD指令)进行内联操作(对应LDC指令),然后将对应的字段进行删除优化。插件会对可能的反射的代码进行分析,对于直接使用反射方式获取运行时常量字段进行忽略优化处理。
4、so包瘦身
(1)移除多余的so架构
defaultConfig {
ndk {
abiFilters "armeabi"
}
}
i:一般应用都不需要用到 neon 指令集,我们只需留下 armeabi 目录就可以了。因为 armeabi 目录下的 So 可以兼容别的平台上的 So
ii:缺点:别的平台使用时性能上就会有所损耗,失去了对特定平台的优化
(2)移除调试符号
使用 Android NDK 中提供的 arm-eabi-strip 工具从原生库中移除不必要的调试符号
5、Buck-删除 Native Library 中无用的导出 symbol
(1)Buck作用
分析代码中的 JNI 方法以及不同 Library 库的方法调用,然后找出无用的 symbol 并删除,这样 Linker 在编译的时候也会把 symbol 对应的无用代码给删除。在 Buck 有 NativeRelinker 这个类,它就实现了这个功能,其 类似于 Native Library 的 ProGuard Shrinking 功能
(2)使用
删除 Native Library 中无用的导出 symbol 使用facebook的Buck库,Buck有 NativeRelinker 这个类,可以删除 Native Library 中无用的导出 symbol,其 类似于 Native Library 的 ProGuard Shrinking 功能。
(3)传送门
https://github.com/facebook/buck
插件化
1、DL 动态加载框架 ( 2014 年底)
基于代理的方式实现插件框架,当启动插件组件时,首先启动一个代理组件,然后通过这个代理组件来构建,启动插件组件
支持的功能
(1)plugin无需安装即可由宿主调起。
(2)支持用R访问plugin资源
(3)plugin支持Activity和FragmentActivity(未来还将支持其他组件)
(4)基本无反射调用
(5)插件安装后仍可独立运行从而便于调试
(6)支持3种plugin对host的调用模式:
无调用(但仍然可以用反射调用)。
部分调用,host可公开部分接口供plugin调用。这前两种模式适用于plugin开发者无法获得host代码的情况。
完全调用,plugin可以完全调用host内容。这种模式适用于plugin开发者能获得host代码的情况。
(7)只需引入DL的一个jar包即可高效开发插件,DL的工作过程对开发者完全透明
传送门:
https://github.com/singwhatiwanna/dynamic-load-apk
2、DroidPlugin ( 2015 年 8 月)
360 手机助手实现的一种插件化框架,它可以直接运行第三方的独立 APK 文件,完全不需要对 APK 进行修改或安装。一种新的插件机制,一种免安装的运行机制,是一个沙箱
功能:
(1)插件APK完全不需做任何修改,可以独立安装运行、也可以做插件运行。要以插件模式运行某个APK,你「无需」重新编译、无需知道其源码。
(2)插件的四大组件完全不需要在Host程序中注册,支持Service、Activity、BroadcastReceiver、ContentProvider四大组件
(3)插件之间、Host程序与插件之间会互相认为对方已经"安装"在系统上了。
(4)API低侵入性:极少的API。HOST程序只是需要一行代码即可集成Droid Plugin
(5)超强隔离:插件之间、插件与Host之间完全的代码级别的隔离:不能互相调用对方的代码。通讯只能使用Android系统级别的通讯方法。
(6)支持所有系统API
(7)资源完全隔离:插件之间、与Host之间实现了资源完全隔离,不会出现资源窜用的情况。
(8)实现了进程管理,插件的空进程会被及时回收,占用内存低。
(9)插件的静态广播会被当作动态处理,如果插件没有运行(即没有插件进程运行),其静态广播也永远不会被触发
缺点:
(1)无法在插件中发送具有自定义资源的Notification
,例如:a. 带自定义RemoteLayout的Notification b. 图标通过R.drawable.XXX指定的通知(插件系统会自动将其转化为Bitmap)
(2)无法在插件中注册一些具有特殊Intent Filter的Service
、Activity
、BroadcastReceiver
、ContentProvider
等组件以供Android系统、已经安装的其他APP调用。
(3)缺乏对Native层的Hook,对某些带native代码的apk支持不好,可能无法运行。比如一部分游戏无法当作插件运行。
传送门:
https://github.com/DroidPluginTeam/DroidPlugin
3、Small ( 2015 年底)
实现原理:(1)动态加载类(2)资源分段(3)动态代理注册
传送门:
https://github.com/wequick/Small/wiki/Android
4、VirtualAPK (2017年 6 月)
VirtualAPK 是滴滴开源的一套插件化框架,支持几乎所有的 Android 特性,四大组件方面
VirtualAPK架构图
传送门:
https://github.com/didi/VirtualAPK/blob/master/README.md
5、RePlugin (2017 年 7 月) RePlugin是一套完整的、稳定的、适合全面使用的,占坑类插件化方案,由360手机卫士的RePlugin Team研发,也是业内首个提出”全面插件化“(全面特性、全面兼容、全面使用)的方案
RePlugin架构图
优点:
「极其灵活」:主程序无需升级(无需在Manifest中预埋组件),即可支持新增的四大组件,甚至全新的插件 「非常稳定」:Hook点「仅有一处(ClassLoader),无任何Binder Hook」!如此可做到其「崩溃率仅为“万分之一”,并完美兼容市面上近乎所有的Android ROM」 「特性丰富」:支持近乎所有在“单品”开发时的特性。「包括静态Receiver、Task-Affinity坑位、自定义Theme、进程坑位、AppCompat、DataBinding等」 「易于集成」:无论插件还是主程序,「只需“数行”就能完成接入」 「管理成熟」:拥有成熟稳定的“插件管理方案”,支持插件安装、升级、卸载、版本管理,甚至包括进程通讯、协议版本、安全校验等 「数亿支撑」:有360手机卫士庞大的「数亿」用户做支撑,「三年多的残酷验证」,确保App用到的方案是最稳定、最适合使用的
传送门:
https://github.com/Qihoo360/RePlugin/blob/dev/README_CN.md
6、Shadow
腾讯自主研发的Android插件框架,经过线上亿级用户量检验,号称“零hook”
Shadow主要具有以下特点:
(1)复用独立安装App的源码:插件App的源码原本就是可以正常安装运行的。
(2)零反射无Hack实现插件技术:从理论上就已经确定无需对任何系统做兼容开发,更无任何隐藏API调用和Google限制非公开SDK接口访问的策略完全不冲突。
(3)全动态插件框架:一次性实现完美的插件框架很难,但Shadow将这些实现全部动态化起来,使插件框架的代码成为了插件的一部分。插件的迭代不再受宿主打包了旧版本插件框架所限制。
(4)宿主增量极小:得益于全动态实现,真正合入宿主程序的代码量极小(15KB,160方法数左右)。(5)Kotlin实现:core.loader,core.transform核心代码完全用Kotlin实现,代码简洁易维护
传送门
https://github.com/Tencent/Shadow
总结
以上是我们目前在Apk包体积优化方面做的一些尝试和积累,可以根据自身情况取舍使用
通过上述优化措施,货拉拉32位包体积从82.69M减少到了33.86M,减少了60%
由于自身业务特点,我们暂时没有使用插件化框架;
最后,保持好的开发习惯,砍掉不必要的功能才是保证包体积持续优化的超级大招
向大家推荐下我的网站 https://xuyisheng.top/ 点击原文一键直达
专注 Android-Kotlin-Flutter 欢迎大家访问
往期推荐
更文不易,点个“三连”支持一下👇