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

2,936 阅读21分钟

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

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

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

包体积优化 · 彩蛋篇 · Android编译期PNG自动化转换WEBP

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

一、引言

Hello,我是小木箱,欢迎来到小木箱成长营系列教程,今天将分享包体积优化 · 方法论 · 揭开包体优化神秘面纱。

上一次分享我们说了三个部分内容,第一部分内容是业务问题和挑战。第二部分内容是包体优化基础知识。第三部分内容是代码优化。最后一部分内容是代码优化注意事项。

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

这一次分享,小木箱从三个维度将Android包体积优化方法论解释清楚,本文主要包括三部分,第一部分内容是关于So层优化,第二部分内容是关于资源文件优化,第三部分内容是关于Assets/Raw资源优化,最后一部分内容是总结与展望。

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

二、关于So优化

首先我们来到第一部分内容关于Native层优化,关于Native层优化有六部分可以和大家说一下,第一部分内容是配置abiFilters。第二部分内容是避免解压缩原生库.第三部分内容是移除调试符号。第四部分内容是so压缩方案.第五部分内容是so混淆方案.第六部分内容是包体监控。

2.1 配置abiFilters

首先我们聊聊第一部分内容配置abiFilters,关于so包瘦身推荐大家移除多余的so架构,一个硬件设备对应一个架构(mips、arm或者x86),只保留与设备架构相关的库文件夹(主流的架构都是arm的,mips属于小众,默认也是支持arm的so的,但x86的不支持)可以大大降低lib文件夹的大小,移除配置如下:

defaultConfig {
    ndk {
        abiFilters "armeabi"
    }
}

一般应用都不需要用到 neon指令集优化,我们只需留下 armeabi 目录就可以了。因为 armeabi 目录下的 So 可以兼容别的平台上的 So。

缺点是别的平台使用时性能上就会有所损耗,失去了对特定平台的优化。

2.2 避免解压缩原生库

然后我们聊聊第二部分内容是避免解压缩原生库,在编译应用的发布版本时,如果将android:extractNativeLibs 属性设置为 false,那么在 APK 安装时就不会提取本地库文件,这样可以减少 APK 的大小。使用android:extractNativeLibs=false 标记可防止 PackageManager 在安装过程中将 .so 文件从 APK 复制到文件系统,并具有减小应用更新的额外好处。

2.3 移除调试符号

接着我们聊聊第三部分移除调试符号,使用 Android NDK 中提供的 arm-eabi-strip 工具从原生库中移除不必要的调试符号。

使用strip手动为ndkaarch64-linux-android-strip命令移除动态库中的调试信息,配置如下:

SET_TARGET_PROPERTIES(yoga PROPERTIES LINK_FLAGS "-Wl,-s")

2.4 设置编译器的优化flag

其四,我们聊聊设置编译器的优化flag,编译器有个优化flag可以设置,分别是-Os(体积最小),-O3(性能最优)等。这里将编译器的优化flag设置为-Os,以便减少体积。

CMake

set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Os")
set(CMAKE_CXX_FLAGS "${CMAKE_C_FLAGS}")

Android.mk

LOCAL_CPPFLAGS += -Os
LOCAL_CFLAGS += -Os

除了直接删除占用体积较大的模块外,编译器优化是排下来优化空间最大的方法。设置完-Os后占用提交较大的前几个库体积对比:

2.5 Buck

其五,我们聊聊so瘦身方案,关于SO压缩方案有很多种,XZ 和7-Zip 是不错的选择,这边我推荐一款so压缩神器Buck

自从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支持三大功能。

2.6 包体监控

因为篇幅有限,SO动态化方案实战论讲解,技术方案比较多,但成熟稳定开源库却少,FaceBook提供了SoLoader适应于RN平台会改变平台构建方式,前两个东家公司有做动态化,某游戏直播公司DQ-Android-Labs源码已经开源、某同城货运公司提供了很好的技术思路,最近发现工作一年的Pika也着手做了一款SillyBoy,大家可以点赞支持一下。

最后,包体积优化之后,我们需要包体监控,目的是防止包体积大小劣化现象反复。关于包体监控主要分为三个纬度的监控。

第一个维度是大小监控,通常是记录当前版本与上一个或几个版本之间的变化情况,如果当前版本体积增长较大,则需要分析具体原因,看是否有优化空间。

第二个维度是依赖监控,包括Jar、aar 依赖。其中arr依赖,我们可以通过python脚本解析输出回溯前端,接着根据版本的差异性展示依赖关系图。这样每个版本我们就能很方便的知道dex新增的体积发生点位在哪里,能快速排查和定位问题所在。

第三个维度是包体积规则监控,我们可以把包体积的监控抽象为无用资源、大文件、重复文件、R 文件等这些规则。

包体积规则监控可以用微信Matrix中的ApkChecker来实现。

三、 针对资源文件优化

针对资源优化的思考方式主要有六部分,第一部分是图片资源优化。第二部分是重复资源优化。第三部分是AndResGuard资源混淆。第四部分是shrinkResources。第五部分是语言资源优化。第六部分是ApkChecker。

3.1 图片资源

图片资源优化分为八部分,第一部分是图片压缩。第二部分是使用webp。第三部分是使用SVG。第四部分是Tint着色器。第五部分是属性代码替代shape。第六部分是从代码进行渲染。第七部分是多分辨率适配。第八部分是超大图缩小分辨率。

3.1.1 图片压缩

首先我们聊聊图片压缩,图片压缩目前已知最优方案是tinypng,因此,建议采用ImgCompressMcImageTinyPngPluginTinyPIC_Gradle_Plugin工具配置tinypng来进行图片压缩方面优化最佳。

项目中的一些场景下,如闪屏,背景图等,无论大图小图,不带透明度的png图片,都可选择转换成jpg来进行有损压缩,能有效减小资源文件的大小。

据Google官方介绍,AAPT 默认使用内置的压缩算法来优化 res/drawable/ 目录下的 PNG 图片,如果使用tinypng二次压缩,那么可能会导致本来已经优化过的图片体积变大.因此,为了规避AAPT内置优化的压缩算法导致图片压缩体积更大,压缩时应禁用默认压缩算法,禁用方式如下

aaptOptions {
    cruncherEnabled = false
    }

3.1.2 使用webp

vd(纯色icon)->webp(非纯色icon)->png(更好效果) ->jpg(若无alpha通道)

参考链接:developer.android.com/guide/appen…

其二我们聊聊使用webp,android 4.0+以下,可选择将png转换成jpg来进行有损压缩。进而有效减小资源文件的大小。

如果想要png或jpg文件的大小更小 ,那么使用pngcrushpngquantzopflipng 等工具压缩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或Google提供的libWebp工具,因为libWebp支持命令行,所以我们通过自定义插件的方式去转换就很方便了 png全量转换webp建议用WebpConvert_Gradle_Plugin

cwebp -q 75 in.png -o out.webp

但有个兼容性问题需要考虑。在4.0 ~ 4.3.1,把png转成webp则无法显示 如果把png先转成jpg再转成webp则能正常显示了,但会丢失透明度。因为4.0 ~ 4.3.1设备上无法显示带有透明度的webp。

3.1.3 使用缩放矢量图

其三我们聊聊使用SVG,SVG又叫缩放矢量图,如果SVG需要使用VectorDrawable,那么可以引用support库可以兼容低版本。

因为SVG的放大缩小,图片质量保持不变,所以使用SVG可以节约内存空间大小,进而减小 APK 的体积。

因此,SVG常用于简单小图标。工作中常用 svg2android 将矢量图转换成 VectorDrawable。SVG版本兼容性如下:

版本矢量图支持Vector Asset Studio 支持
Android 5.0以上(API 级别 21)✔️所有 VectorDrawable 元素
Android 5.0以下(API 级别 20)及更低版本支持部分 XML 元素, 将SVG文件添加到项目中

在App瘦身最佳实践 中有提到svg的问题和解决方案。使用AnimatedVectorDrawableCompat创建矢量动画替代原有的帧动画。

3.1.4 Tint着色器

参考链接: github.com/han1202012/…

其四我们聊聊Tint着色器,自 API 21 开始,Android SDK 引入Tint着色器Tint着色器就可以随意改变安卓项目中图标或者 View 背景的颜色,Tint着色器一定程度上可以减少同一个样式不同颜色图标的数量,从而起到 Apk 瘦身的作用。Tint着色器使用如下:

3.1.5 属性代码替代shape

其五我们聊聊属性代码替代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背景的效果。

3.1.6 从代码进行渲染

其六我们聊聊从代码进行渲染,对于一些矩形、圆形等常规图形我们可以直接通过代码进行渲染和绘制。

3.1.7 多分辨率适配

其七我们聊聊多分辨率适配,实际项目中我们引入的icon文件,如果是聊天表情图片建议使用hdpi格式,如果是纯色小 icon 建议使用 VD格式,如果是背景大图建议使用 xhdpi格式,如果是logo等权重比较大的图片建议使用 hdpi和xhdpi格式。

如果某些图在真机中有异常建议出多套图,其余仅保drawable-hdpi一套即可。为了统一应用风格,我们可以减少shape文件。为了统一的标题栏,相同界面资源使用include标签。为了使用toolbar,我们可以减少menu文件。为了限制灵活性,我们可以减少layout文件。

3.1.8 超大图缩小分辨率

最后 我们聊聊超大图缩小分辨率 如果解压Apk发现有超出限制大小的非alpha png图片,那么我们考虑把超出限制的图片进行转码或缩小分辨率来优化。当然这个步骤可以通过McImage实现。

3.2 重复资源

说完图片资源优化,我们说一下重复资源优化,实际项目开发过程中,不同的功能迭代,经过了N多个人维护。可能会在不同的位置使用了同样的icon,但命名各不一样,这样就会导致apk中出现很多重复发资源文件。

常见的解决方法是通过资源包中的每个ZipEntry的CRC-32 checksum来筛选出重复的资源;通过android-chunk-utils修改resources.arsc,把这些重复的资源都重定向到同一个文件上; 把其它重复的资源文件从资源包中删除。

jekins打包后,我们可以输出包含重复资源列表的apk大小分析结果报表,通过参考列表进行优化。

3 .3 语言资源

说完重复资源优化,我们说一下语言资源优化,google给我们的apk提供了国际化支持,如适应不同的屏幕分辨率的drawable资源,还有适应不同语言的字符串资源等等。

构建工具可以移除指定语言之外的所有资源(可以删除sdk里面的语言资源) 。

但是在很多情况下我们只需要一些指定分辨率和语言的资源就可以了,这个时候我们可以使用resConfigs方法来配置。

android { //...   
defaultConfig {
     resConfigs "zh"
 }
}

3.4 AndResGuard

传送门: github.com/shwenzhang/…

3.4.1 引言

说完语言资源优化,我们说一下AndResGuard资源混淆工具,AndResGuard资源混淆工具大约是在2014年4月实现,并在微信5.4中使用,减少了大约1M的空间。

AndResGuard把资源名称,路径名称混淆压缩的方式,增加应用本身安全性的同时,压缩包体大小,同时把原apk包stared方式存储的png文件,改成DEFLATED(普通压缩存储)方式,进一步压缩包体大小。

3.4.2 方案

AndResGuard资源混淆工具直接处理apk。不依赖源码,不依赖编译过程,仅仅输入一个安装包,得到一个混淆包。

3.4.3 处理

AndResGuard资源混淆工具针对resources.arsc,记录了资源文件的名称与路径,使用混淆后的短路径 res/s/a,可以减少文件的大小。

AndResGuard资源混淆工具针对metadata签名文件,签名文件 MANIFEST.MF 与 CERT.SF 需要记录所有文件的路径以及它们的哈希值,使用短路径可以减少这两个文件的大小。

因为AndResGuard资源混淆工具内部可以开启7zip压缩资源。对于ZIP 文件,ZIP 文件格式里面通过其索引记录了每个文件 Entry 的路径、压缩算法、CRC、文件大小等等信息。短路径的优化减少了记录文件路径的字符串大小。

3.4.4 工作流程

3.4.5 7zip压缩

7z有着更好的压缩率,同时我们也可以强制压缩类似resources.arsc、png、jpg等Android默认不会打包压缩的文件。最后把修改后的resources.arsc重打包即可,微信从解压,到混淆,到重打包耗费时间,仅需35秒。具体效果如下图:

3.4.6 使用方式

在终端里执行如下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文件内容如下:

3.4.7 混淆后产物

3.4.8 经验与踩坑

关于AndResGuard有三点经验可以分享一下。第一点是资源混淆之白名单代码扫描调用getIdentifier()方法的地方。

第二点是开启7zip压缩之后会影响图片加载速度,会对app启动速度有点影响。

因为安卓可以直接把图片资源加载到 mmap 里面。如果图片压缩过之后,那么图片需要再解压一次,再加载图片会变慢。

第三点是Android 系统不会去压缩这些文件呢?主要基于以下两点原因:

一方面,压缩效果不明显:上述格式的文件大部分已经被压缩过,因此,重新做 Zip 压缩效果并不明显。比如 重新压缩 PNG 和 JPG 格式只能减少 3%~5% 的大小。

另一方面,基于读取时间和内存的考虑:针对于没有进行压缩的文件,系统可以使用 mmap的方式直接读取,而不需要一次性解压并放在内存中。

第四点是AndResGuard会全量扫描工程文件进而拉低工程编译速度,非必要情况下不要引入Plugin,在release打包环境用命令行即可。

3.5 ShrinkResources

3.5.1 引言

说完AndResGuard资源混淆,我们说一下shrinkResources,shrinkResources是Android的编译工具链中提供了一款资源压缩的工具,可以通过shrinkResources工具来压缩资源。

3.5.2 注意事项

shrinkResources不会真正删除资源图片,而是用预置的小资源文件替代原来的资源避免crash。

如果要启用资源压缩,可以在build.gradle文件中将shrinkResources设置为true可以移除无用的 resource 文件:

image.png

当 ProGuard 把部分无用代码移除的时候,这些代码所引用的资源也会被标记为无用资源,然后,系统会通过资源压缩功能将它们移除。

需要注意的是目前资源压缩器目前不会移除 values / 文件夹中定义的资源(例如字符串、尺寸、样式和颜色)。

不想被unused resources或者shrinkResources删除的资源在raw目录添加添加keep文件。

<?xml version="1.0" encoding="utf-8"?>
<resources xmlns:tools="http://schemas.android.com/tools"
    tools:keep="
           @layout/fragment_personal,
           @drawable/material_*,
           @string/*,
           @style/*,
           @anim/tiny_*"/>

开启后,Android 构建工具会通过 ResourceUsageAnalyzer 来检查哪些资源是无用的,当检查到无用的资源时会把该资源替换成预定义的版本。

主要是针对 .png、.9.png、.xml 提供了 TINY_PNG、TINY_9PNG、TINY_XML 这 3 个 byte 数组的预定义版本。

资源压缩工具默认是采用安全压缩模式来运行,可以通过开启严格压缩模式来达到更好的瘦身效果。

3.6 ApkChecker

3.6.1 使用方式

说完AndResGuard资源混淆,我们说一下ApkCheckerApkChecker使用很简单。打开项目目录:根目录/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" } 

3.6.2 输出报告

四、 针对Assets/Raw资源优化

4.1 压缩资源文件

有些资源文件是必须要随着app一并发布的,对于这样的文件,可以采用压缩存储的方式,在需要资源的时候将其解压使用。android上也有一个7z库帮助我们方便的使用7z。

4.2 尽量避免使用帧动画

有些帧动画完全没必要,有些可以考虑换其他方式实现,或者动态下载。

4.3 优化音频格式,降低采样率

项目中的语音播报文件wav、mp3和aac格式可以进行压缩优化 ,降低采样率。

4.4 字体压缩

项目中可能为了某些特殊需求需要用到一些特殊的字体,但应用场景很少,我们可以考虑精简字体包。小木箱推荐字体压缩神器FontZip来提取特定字符的字体,在不影响用户使用的情况下,对ttf字体文件进行size精简。

五、 总结与展望

本文主要说了三部分内容,第一部分内容是针对SO优化。第二部分内容是针对Res资源优化。第三部分内容是针对Assets/Raw资源优化。

其中针对SO优化从配置abiFilters ,避免解压缩原生库 ,移除调试符号 ,设置编译器的优化flag ,so压缩和包体监控五个维度进行讲解。

Res资源优化我们谈到了图片资源优化、重复资源优化和语言资源优化紧接着带大家引入了三个实用的Res资源优化工具 ,AndResGuard、ShrinkResources和ApkChecker。

而针对Assets/Raw资源优化总共涉及了三个方面 ,压缩资源文件、尽量避免使用帧动画、优化音频格式,降低采样率和字体压缩。

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

下一篇实战论会从上而下带大家学习Android的包体优化高级进阶教程。我是小木箱,我们下一篇见~

优质技术方案参考