原文发布于:小木箱成长营
首先我们来到第一部分内容关于 Native 层优化,关于 Native 层优化有六部分可以和大家说一下,第一部分内容是配置 abiFilters。第二部分内容是避免解压缩原生库.第三部分内容是移除调试符号。第四部分内容是 so 压缩方案.第五部分内容是 so 混淆方案.第六部分内容是包体监控。
首先我们聊聊第一部分内容配置 abiFilters,关于 so 包瘦身推荐大家移除多余的 so 架构,一个硬件设备对应一个架构(mips、arm 或者 x86),只保留与设备架构相关的库文件夹(主流的架构都是 arm 的,mips 属于小众,默认也是支持 arm 的 so 的,但 x86 的不支持)可以大大降低 lib 文件夹的大小,移除配置如下:
defaultConfig {
ndk {
abiFilters "armeabi"
}
}
一般应用都不需要用到 neon 指令集优化,我们只需留下 armeabi 目录就可以了。因为 armeabi 目录下的 So 可以兼容别的平台上的 So。
缺点是别的平台使用时性能上就会有所损耗,失去了对特定平台的优化。
然后我们聊聊第二部分内容是避免解压缩原生库,在编译应用的发布版本时,您可以通过在应用清单元素中设置 android:extractNativeLibs="false",打包 APK 中未压缩的 .so文件。停用此标记可防止 PackageManager 在安装过程中将 .so 文件从 APK 复制到文件系统,并具有减小应用更新的额外好处。
接着我们聊聊第三部分移除调试符号,使用 Android NDK 中提供的 arm-eabi-strip 工具从原生库中移除不必要的调试符号。
使用 strip 手动为 ndkaarch64-linux-android-strip命令移除动态库中的调试信息,配置如下:
SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,-s")
其四,我们聊聊设置编译器的优化 flag,编译器有个优化 flag 可以设置,分别是-Os(体积最小),-O3(性能最优)等。这里将编译器的优化 flag 设置为-Os,以便减少体积。
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")
LOCAL_CPPFLAGS += -Os
LOCAL_CFLAGS += -Os
除了直接删除占用体积较大的模块外,编译器优化是排下来优化空间最大的方法。设置完-Os后占用提交较大的前几个库体积对比:
其五,我们聊聊so 瘦身方案,关于 SO 压缩方案有很多种,XZ 和 7-Zip 是不错的选择,这边我推荐一款so 压缩神器Buck[4]。
自从 Android 开发切换到 Android Studio 之后,就一直使用 Gradle 进行项目的构建,随着工程 Module 的增加,代码的一处改动,都要花费几分钟的时间重新编译,实在是浪费时间;Facebook 的 Buck 主要用来替换 Gradle,快速构建 Android 多模块系统。
分析代码中的 JNI 方法以及不同 Library 库的方法调用,然后找出无用的 symbol 并删除,这样 Linker 在编译的时候也会把 symbol 对应的无用代码给删除。在 Buck 有 NativeRelinker 这个类,它就实现了这个功能,其类似于 Native Library 的 ProGuard Shrinking 功能。
Buck 除了实现了 Library Merge 和 Relinker 功能之外,还实现了多语言拆分、分包支持和 ReDex 支持三大功能。
因为篇幅有限,SO 动态化方案实战论讲解,技术方案比较多,但成熟稳定开源库却少,FaceBook 提供了SoLoader[5]适应于 RN 平台会改变平台构建方式,前两个东家公司有做动态化,某游戏直播公司DQ-Android-Labs[6]源码已经开源、某同城货运公司提供了很好的技术思路[7],最近发现工作一年的Pika也着手做了一款SillyBoy[8],大家可以点赞支持一下。
最后,包体积优化之后,我们需要包体监控,目的是防止包体积大小劣化现象反复。关于包体监控主要分为三个纬度的监控。
第一个维度是大小监控,通常是记录当前版本与上一个或几个版本之间的变化情况,如果当前版本体积增长较大,则需要分析具体原因,看是否有优化空间。
第二个维度是依赖监控,包括 Jar、aar 依赖。其中 arr 依赖,我们可以通过 python 脚本解析输出回溯前端,接着根据版本的差异性展示依赖关系图。这样每个版本我们就能很方便的知道 dex 新增的体积发生点位在哪里,能快速排查和定位问题所在。
第三个维度是包体积规则监控,我们可以把包体积的监控抽象为无用资源、大文件、重复文件、R 文件等这些规则。
包体积规则监控可以用微信 Matrix 中的 ApkChecker 来实现。
针对资源优化的思考方式主要有六部分,第一部分是图片资源优化。第二部分是重复资源优化。第三部分是 AndResGuard 资源混淆。第四部分是 shrinkResources。第五部分是语言资源优化。第六部分是 ApkChecker。
图片资源优化分为八部分,第一部分是图片压缩。第二部分是使用 webp。第三部分是使用 SVG。第四部分是 Tint 着色器。第五部分是属性代码替代 shape。第六部分是从代码进行渲染。第七部分是多分辨率适配。第八部分是超大图缩小分辨率。
首先我们聊聊图片压缩,图片压缩目前已知最优方案是 tinypng,因此,建议采用ImgCompress[9]、 McImage[10]、TinyPngPlugin[11]、TinyPIC_Gradle_Plugin[12]工具配置 tinypng 来进行图片压缩方面优化最佳。
项目中的一些场景下,如闪屏,背景图等,无论大图小图,不带透明度的 png 图片,都可选择转换成 jpg 来进行有损压缩,能有效减小资源文件的大小。
据 Google 官方介绍,AAPT 默认使用内置的压缩算法来优化 res/drawable/ 目录下的 PNG 图片,如果使用 tinypng 二次压缩,那么可能会导致本来已经优化过的图片体积变大.因此,为了规避 AAPT 内置优化的压缩算法导致图片压缩体积更大,压缩时应禁用默认压缩算法,禁用方式如下
aaptOptions {
cruncherEnabled = false
}
vd(纯色 icon)->webp(非纯色 icon)->png(更好效果) ->jpg(若无 alpha 通道)
参考链接:http://developer.android.com/guide/appendix/media-formats.html
其二我们聊聊使用 webp,android 4.0+以下,可选择将 png 转换成 jpg 来进行有损压缩。进而有效减小资源文件的大小。
如果想要 png 或 jpg 文件的大小更小 ,那么使用pngcrush[13]、pngquant[14] 或 zopflipng[15] 等工具压缩 png 或 jpg 文件的大小是值得推荐的。
有没有比 jpg 更小的图片格式呢?webp。webp 是 android 4.0+以上版本原生支持的一种图片格式。虽然 webp 的图片大小低于 png,但我们需要比较一下 webp 对比 png 的编解码速度, 实验证明 WebP 解码时间低于 png 解码时间。
fun decodeWebP(){
var pngStart = System.currentTimeMillis()
BitmapFactory.decodeResource(resources, R.mipmap.icon_png)
Log.e(TAG, "解码 png 格式图片时间 : ${System.currentTimeMillis() - pngStart} ")
var webPStart = System.currentTimeMillis()
BitmapFactory.decodeResource(resources, R.mipmap.icon_webp)
Log.e(TAG, "解码 WebP 格式图片时间 : ${System.currentTimeMillis() - webPStart} ")
}
// 解码 png 格式图片时间 : 285
// 解码 WebP 格式图片时间 : 210
png 人肉转换 webp 推荐线上工具png-webp[16]或 Google 提供的 libWebp[17]工具,因为 libWebp 支持命令行,所以我们通过自定义插件的方式去转换就很方便了 。png 全量转换 webp 建议用WebpConvert_Gradle_Plugin[18]。
cwebp -q 75 in.png -o out.webp
但有个兼容性问题需要考虑。在 4.0 ~ 4.3.1,把 png 转成 webp 则无法显示 。如果把 png 先转成 jpg 再转成 webp 则能正常显示了,但会丢失透明度。因为 4.0 ~ 4.3.1 设备上无法显示带有透明度的 webp。
其三我们聊聊使用 SVG,SVG 又叫缩放矢量图,如果 SVG 需要使用 VectorDrawable,那么可以引用 support 库可以兼容低版本。
因为 SVG 的放大缩小,图片质量保持不变,所以使用 SVG 可以节约内存空间大小,进而减小 APK 的体积。
因此,SVG 常用于简单小图标。工作中常用 svg2android[19] 将矢量图转换成 VectorDrawable。SVG 版本兼容性如下:
在 App 瘦身最佳实践[20] 中有提到 svg 的问题和解决方案。使用AnimatedVectorDrawableCompat[21]创建矢量动画替代原有的帧动画。
参考链接:https://github.com/han1202012/SVG
其四我们聊聊Tint 着色器,自 API 21 开始,Android SDK 引入Tint 着色器,Tint 着色器就可以随意改变安卓项目中图标或者 View 背景的颜色,Tint 着色器一定程度上可以减少同一个样式不同颜色图标的数量,从而起到 Apk 瘦身的作用。Tint 着色器使用如下:
其五我们聊聊属性代码替代 shape,XML Drawable 对象生产了符合 Material Design 准则的单色图片并且 Drawable 对象(XML 中 shape)占用 APK 少量内存空间,因此,当某些图片非静态图片;框架可以在运行时改为动态绘制图片。
属性代码替代 shape体使用原理是通过加载自定义背景 shape 的 factory, 注入系统的 LayoutFactory,拦截系统的控件创建过程。在自定义控件时,扫描我们自定义控件相关的自定义属性,通过这些自定义属性,创建系统的 GradientDrawable 对象,并将该对象作为背景,设置到控件上。实现过程如下:
@Override
public View onCreateView(@Nullable View parent, String name, Context context, AttributeSet attrs) {
name = TUtil.getRealViewName(name);
View view = null;
// 先让外部layoutInflaterFactory尝试加载view
if (mOutFactory != null) {
view = mOutFactory.onCreateView(parent, name, context, attrs);
}
// 外部factory不存在或者加载不成功,尝试系统AppCompatDelegate记载view
if (view == null && appCompatDelegate != null) {
view = appCompatDelegate.createView(parent, name, context, attrs);
}
// 获取布局中的t_background相关属性
TRoundBackground bg = TUtil.getTbackgroundFromAttrs(context, attrs);
ColorStateList csl = TUtil.getTextColorFromAttr(context, attrs);
// 如果设置了T_background相关属性
if (bg != null || csl != null) {
// 自身尝试创建view
if (view == null) {
view = createViewFromTag(context, name, attrs);
}
// 尝试设置t_background相关到view中
TUtil.setViewBackground(bg, view);
TUtil.setViewTextColor(csl, view);
}
return view;
}
通过上面的渲染方式达到了自定义属性实现系统 Shape 背景的效果。
其六我们聊聊从代码进行渲染,对于一些矩形、圆形等常规图形我们可以直接通过代码进行渲染和绘制。
其七我们聊聊多分辨率适配,实际项目中我们引入的 icon 文件,如果是聊天表情图片建议使用 hdpi 格式,如果是纯色小 icon 建议使用 VD 格式,如果是背景大图建议使用 xhdpi 格式,如果是 logo 等权重比较大的图片建议使用 hdpi 和 xhdpi 格式。
如果某些图在真机中有异常建议出多套图,其余仅保 drawable-hdpi 一套即可。为了统一应用风格,我们可以减少 shape 文件。为了统一的标题栏,相同界面资源使用 include 标签。为了使用 toolbar,我们可以减少 menu 文件。为了限制灵活性,我们可以减少 layout 文件。
最后 , 我们聊聊超大图缩小分辨率 , 如果解压 Apk 发现有超出限制大小的非 alpha png 图片,那么我们考虑把超出限制的图片进行转码或缩小分辨率来优化。当然这个步骤可以通过McImage[22]实现。
说完图片资源优化,我们说一下重复资源优化,实际项目开发过程中,不同的功能迭代,经过了 N 多个人维护。可能会在不同的位置使用了同样的 icon,但命名各不一样,这样就会导致 apk 中出现很多重复发资源文件。
常见的解决方法是通过资源包中的每个 ZipEntry 的 CRC-32 checksum 来筛选出重复的资源;通过android-chunk-utils[23]修改 resources.arsc,把这些重复的资源都重定向到同一个文件上;把其它重复的资源文件从资源包中删除。
jekins 打包后,我们可以输出包含重复资源列表的 apk 大小分析结果报表,通过参考列表进行优化。
说完重复资源优化,我们说一下语言资源优化,google 给我们的 apk 提供了国际化支持,如适应不同的屏幕分辨率的 drawable 资源,还有适应不同语言的字符串资源等等。
构建工具可以移除指定语言之外的所有资源(可以删除 sdk 里面的语言资源) 。
但是在很多情况下我们只需要一些指定分辨率和语言的资源就可以了,这个时候我们可以使用 resConfigs 方法来配置。
android { //...
defaultConfig {
resConfigs "zh"
}
}
传送门: https://github.com/shwenzhang/AndResGuard
说完语言资源优化,我们说一下 AndResGuard 资源混淆工具,AndResGuard 资源混淆工具大约是在 2014 年 4 月实现,并在微信 5.4 中使用,减少了大约 1M 的空间。
AndResGuard 把资源名称,路径名称混淆压缩的方式,增加应用本身安全性的同时,压缩包体大小,同时把原 apk 包 stared 方式存储的 png 文件,改成 DEFLATED(普通压缩存储)方式,进一步压缩包体大小。
AndResGuard 资源混淆工具直接处理 apk。不依赖源码,不依赖编译过程,仅仅输入一个安装包,得到一个混淆包。
AndResGuard 资源混淆工具针对 resources.arsc,记录了资源文件的名称与路径,使用混淆后的短路径 res/s/a,可以减少文件的大小。
AndResGuard 资源混淆工具针对 metadata 签名文件,签名文件 MANIFEST.MF 与 CERT.SF 需要记录所有文件的路径以及它们的哈希值,使用短路径可以减少这两个文件的大小。
因为 AndResGuard 资源混淆工具内部可以开启 7zip 压缩资源。对于 ZIP 文件,ZIP 文件格式里面通过其索引记录了每个文件 Entry 的路径、压缩算法、CRC、文件大小等等信息。短路径的优化减少了记录文件路径的字符串大小。
7z 有着更好的压缩率,同时我们也可以强制压缩类似 resources.arsc、png、jpg 等 Android 默认不会打包压缩的文件。最后把修改后的 resources.arsc 重打包即可,微信从解压,到混淆,到重打包耗费时间,仅需 35 秒。具体效果如下图:
在终端里执行如下 sh 文件,会在当前目录生成 release 文件夹,所生成的文件在这个目录下。
git clone https://github.com/shwenzhang/AndResGuard.git
&& brew install p7zip
&& cd ../tools/res-reguard
&& java -jar ./AndResGuard-cli-1.3.15.jar(混淆工具包)
./xxx.apk(输入文件) -config ./config.xml(配置文件)
-out ./release(输出目录) -signatureType v2(签名类型)
-signature ./release.keystore(签名keystore文件路径)
testres(storepass) testres(keypass) testres(storealias)
-zipalign /Library/Android/sdk/build-tools/32.0.0/zipalign(zipalign工具路径)
其中, config.xml 文件内容如下:
关于 AndResGuard 有三点经验可以分享一下。第一点是资源混淆之白名单代码扫描调用 getIdentifier()方法的地方。
第二点是开启 7zip 压缩之后会影响图片加载速度,会对 app 启动速度有点影响。
因为安卓可以直接把图片资源加载到 mmap 里面。如果图片压缩过之后,那么图片需要再解压一次,再加载图片会变慢。
第三点是 Android 系统不会去压缩这些文件呢?主要基于以下两点原因:
一方面,压缩效果不明显:上述格式的文件大部分已经被压缩过,因此,重新做 Zip 压缩效果并不明显。比如 重新压缩 PNG 和 JPG 格式只能减少 3%~ 5% 的大小。
另一方面,基于读取时间和内存的考虑:针对于没有进行压缩的文件,系统可以使用 mmap 的方式直接读取,而不需要一次性解压并放在内存中。
第四点是 AndResGuard 会全量扫描工程文件进而拉低工程编译速度,非必要情况下不要引入 Plugin,在 release 打包环境用命令行即可。
说完 AndResGuard 资源混淆,我们说一下 shrinkResources,shrinkResources 是 Android 的编译工具链中提供了一款资源压缩的工具,可以通过 shrinkResources 工具来压缩资源。
shrinkResources 不会真正删除资源图片,而是用预置的小资源文件替代原来的资源避免 crash。
如果要启用资源压缩,可以在 build.gradle 文件中将 shrinkResources 设置为 true 可以移除无用的 resource 文件:
当 ProGuard 把部分无用代码移除的时候,这些代码所引用的资源也会被标记为无用资源,然后,系统会通过资源压缩功能将它们移除。
需要注意的是目前资源压缩器目前不会移除 values / 文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。
不想被 unused resources 或者 shrinkResources 删除的资源在 raw 目录添加添加 keep 文件。
开启后,Android 构建工具会通过 ResourceUsageAnalyzer 来检查哪些资源是无用的,当检查到无用的资源时会把该资源替换成预定义的版本。
主要是针对 .png、.9.png、.xml 提供了 TINY_PNG、TINY_9PNG、TINY_XML 这 3 个 byte 数组的预定义版本。
资源压缩工具默认是采用安全压缩模式来运行,可以通过开启严格压缩模式来达到更好的瘦身效果。
说完 AndResGuard 资源混淆,我们说一下ApkChecker[27],ApkChecker[28]使用很简单。打开项目目录:根目录/tools/apk-checker,将需要打包生成的 apk 文件、mapping 文件拷贝到该目录下;修改该目录下的 config.json 文件的配置在--apk 属性配置项修改为拷进该目录的 apk 文件名,在--mapping 属性配置项修改为拷贝进该目录的 mapping 文件名。
将 apk-check.sh 文件拖进去终端回车运行(注意因为是省略路径,终端需要 cd 相应的目录),就会自动生成一个 checker-result.html 的报告。
ApkChecker 比较好用的功能是对同一种图片监测,因为很多图片它可能是同一个,但是在不同的模块里面,或名字不一样,因为它 MD5 一样的,所以可以把它弄成一张图片即可。
注意:如果需要针对 so 相关的进行检测,需要自行在 config.json 添加相关选项:
{ "name":"-unstrippedSo", "--toolnm":"(绝对路径)../arm-linux-androideabi-as" }
{ "name":"-checkMultiSTL", "--toolnm":"(绝对路径)../arm-linux-androideabi-as" }
有些资源文件是必须要随着 app 一并发布的,对于这样的文件,可以采用压缩存储的方式,在需要资源的时候将其解压使用。android 上也有一个7z 库[30]帮助我们方便的使用 7z。
有些帧动画完全没必要,有些可以考虑换其他方式实现,或者动态下载。
项目中的语音播报文件 wav、mp3 和 aac 格式可以进行压缩优化 ,降低采样率。
项目中可能为了某些特殊需求需要用到一些特殊的字体,但应用场景很少,我们可以考虑精简字体包。小木箱推荐字体压缩神器FontZip[31]来提取特定字符的字体,在不影响用户使用的情况下,对 ttf 字体文件进行 size 精简。
本文主要说了三部分内容,第一部分内容是针对 SO 优化。第二部分内容是针对 Res 资源优化。第三部分内容是针对 Assets/Raw 资源优化。
其中针对 SO 优化从配置 abiFilters ,避免解压缩原生库 ,移除调试符号 ,设置编译器的优化 flag ,so 压缩和包体监控五个维度进行讲解。
Res 资源优化我们谈到了图片资源优化、重复资源优化和语言资源优化紧接着带大家引入了三个实用的 Res 资源优化工具 ,AndResGuard、ShrinkResources 和 ApkChecker。
而针对 Assets/Raw 资源优化总共涉及了三个方面 ,压缩资源文件、尽量避免使用帧动画、优化音频格式,降低采样率和字体压缩。
近些年来,中大厂门户 app 不断成熟,功能不断堆积和迭代,Android 打包后体积越来越大。安装包体大小不仅对用户留存、市场推广有负面影响,而且如果后续缺乏长效治理监管机制,那么包体大小会出现边治理边污染的现象。官方推荐、微信、美团、QQ 音乐、字节跳动、快手、手淘和蘑菇街等包体优化方案对咱中小企业仍然有借鉴意义。
下一篇实战论会从上而下带大家学习 Android 的包体优化高级进阶教程。我是小木箱,我们下一篇见~
优质技术方案参考:
https://developer.android.com/topic/performance/reduce-apk-size[32]
https://developer.android.com/studio/build/shrink-code?hl=zh-cn[33]
https://www.guardsquare.com/manual/configuration/examples[34]
包体积优化(上):如何减少安装包大小?[35]
包体积优化(下):资源优化的进阶实践[36]
支付宝 App 构建优化解析:Android 包大小极致压缩
美团 Android App 包瘦身优化实践[37]
美团 Android 对 so 体积优化的探索与实践
字节 抖音 Android 包体积优化探索:从 Class 字节码入手精简 Dex 体积
字节 抖音 Android 包体积优化探索:资源二进制格式的极致精简
字节 抖音 Android 包体积优化探索:基于 ReDex 的 Dex 优化落地实践
货拉拉 Android 包体积优化实践[38]
有道词典 Android 客户端包体积优化之路
百度 App Android 包体积优化实践(一)总览
百度 App Android 包体积优化实践(二)Dex 行号优化
百度 APP Android 包体积优化实践(三)资源优化
得物 App 包体积治理之路
Android 包体积优化(常规、进阶、极致)[39]
关注我获取更多知识或者投稿