APK瘦身——更全面的方案

前阵子做了一次减包体的工作,过后觉得这项工作还是套路性挺强的,于是综合当时的经验以及广泛浏览了网上的各种大牛小牛的博客,实践后特总结了以下的一些思路和做法,不求内容最丰富,只求方案更全面 。
APK瘦身的目的
瘦身的目的最明显的一个就是:提高下载转化率 。怎么理解呢?举个例子来说,假如你的应用包12MB,有100个潜在用户想要去下载尝试使用你的应用,结果有20个用户嫌弃安装包太大而直接放弃,有20个用户在等待下载的过程中取消下载,最终只有60个用户下载安装了应用 。这时你的应用的实际的下载转化率其实是 60/100 = 60% 。
简单的总结:安装包越小,用户下载等待的时间越短,对手机配置要求的也越小,设备的体验愈佳,应用的下载转化率也就越高 。
APK包体的组成
开始瘦身前,需要先了解一下APK都主要由哪些成分组成
.dex
编写的所有的Java代码(包括各种引入的sdk代码)最终转化成在虚拟机上运行需要的字节码(和java的字节码有一定的区别)
res文件夹
存放所有资源的文件夹(除了里面raw文件夹的文件不会被编译,其他都会被编译)
.arsc
编译后的二进制资源文件
文件夹
用于保存需要保持原始文件的资源文件(这部分资源不会被编译)
lib文件夹
用于存放应用需要的库文件
.xml
程序全局配置文件
META-INF文件夹
存放几个签名校验相关的文件,用于保证APK的完整性和安全性
其他
其他一些配置生成的文件
分析现APK各成分的比例
了解清楚APK的各个组成成分后,就需要有针对性地对自己的APK各个成分做一个比例分析 。
工具1:2.2及其以上的版本
自2.2版本以来就引入了分析APK各成分的比例的功能,用法也挺简单的,主要有2种操作方法,如下:
导航栏的Build → APK…→选择APK文件的路径→选择OK打开即可

APK瘦身——更全面的方案

文章插图

APK瘦身——更全面的方案

文章插图
可以直接把APK包拖进IDE,也可以得到如下的比例分析构成图(比例会自动按从大到小排好序呈现)
APK瘦身——更全面的方案

文章插图
可以看出,在其中一般占比比较大的一般都是dex文件,res文件夹,文件夹,lib文件夹以及.arsc文件 。所以接下来的工作就是有针对性地让这些文件和文件夹尽量地变小 。在开始下一步之前,要简要介绍另外一个工具 。
工具2:
是美国哥伦比亚大学的博士创业团队研发出来的分析 app性能指标的系统,分析的方式有静态和动态两种方式,其中静态分析可以分析出APK安装包中大文件排行榜,各种知名SDK的大小以及占代码整体的比例,各种类型文件的大小以及占排行,各种知名SDK的方法数以及占所有dex中方法数的比例 。这个外国网站的工具目前比 自带的功能更加全面和强大 。这里仅作简要介绍,读者若有兴趣可自行去研究 。
有针对性地对各部分做缩减工作 一、减.dex文件——压缩代码
代码压缩通过提供,会检测和移除封装应用中未使用的类、字段、方法和属性,包括自带代码库中的未使用项(这使其成为以变通方式解决 64k 引用限制的有用工具) 。还可优化字节码,移除未使用的代码指令,以及用短名称混淆其余的类、字段和方法 。混淆过的代码可令您的 APK 难以被逆向工程 。
要启用通过实现的代码压缩,要在对应(一般情况下是主,build.文件第一行为apply : ‘com..’)的build. 文件相应的构建类型中添加true 。
注意,会拖慢构建速度,因此应该尽可能避免在调试版本构建中使用它 。另:会在使用Run 时停用。
如下为构建版本的build.的示例片段:
android {buildTypes {release {minifyEnabled trueproguardFiles getDefaultProguardFile(‘proguard-android.txt'),'proguard-rules.pro'}}...}
其中的 属性:用于定义规则
le(‘-.txt’) 为获取默认的 规则文件,它位于 SDK tools// 文件夹 。(提示:要想做进一步的代码压缩,可尝试使用位于同一位置的 --.txt 文件 。它包括相同的规则,除此之外还包括其他在字节码一级(方法内和方法间)执行分析的优化,以进一步减小 APK 大小和帮助提高其运行速度 。)
-rules.pro 文件用于添加自定义的规则 。默认情况下,该文件位于根目录(build. 文件旁)
每次构建时都会输出下列文件,这些文件保存在 /build////
自定义混淆规则
对于某些情况,默认配置文件 (-.txt) 足以满足需要,会移除所有(并且只会移除)未使用的代码 。不过,难以对许多情况进行正确分析,可能会移除应用真正需要的代码 。举例来说,它可能错误移除代码的情况包括:
要修正错误并强制保留特定代码,可以在配置文件中添加一行 keep 代码 。例如:
-keepclass
还可以在想保留的代码添加 @Keep 注解 。在类上添加 @Keep 可原样保留整个类 。在方法或字段上添加它可完整保留方法/字段(及其名称)以及类名称 。(请注意,只有在使用注解支持库时,才能使用此注解 。)
有关自定义-rules.pro文件的更多信息,可以参考 手册.这里问题排查列出了一些常见的问题 。本文不讲解这部分知识 。
解码混淆过的堆叠追踪
本来本章主要讲解压缩代码的,一般都不会涉及到如何解码混淆过的堆叠追踪的 。考虑到网上很多参考资料只是讲了如何压缩混淆代码却没有告诉我们如何去解码混淆过的堆叠追踪以便快速定位出现崩溃的代码的 。所以当你发现刚好差某样东西的时候,这恰恰就是你的机会了 。
在压缩代码后,读取堆叠追踪变得困难(即使并非不可行),因为方法名称经过了混淆处理 。幸运的是,每次运行都会创建一个 .txt 文件,其中显示了与混淆过的名称对应的原始类名称、方法名称和字段名称 。将该文件保存在应用的 /build//// 目录中 。
请注意,每次使用创建发布构建时都会覆盖 .txt 文件,因此每次发布新版本时都要必须小心地保存一个副本 。通过为每个发布构建保留一个 .txt 文件副本,就可以在用户提交的已混淆堆叠追踪来自旧版本应用时对问题进行调试 。
如果是在 Play 上发布应用,可以上传每个 APK 版本的 .txt 文件 。Play 将根据用户报告的问题对收到的堆叠追踪进行去混淆处理,以便你在Play中进行检查 。
如果要自行将混淆过的堆叠追踪转换成可读的堆叠追踪,请使用脚本(在上为 .bat;在 Mac/Linux 上为 .sh) 。它位于 /tools// 目录中 。该脚本利用 .txt 文件和您的堆叠追踪生成新的可读堆叠追踪 。使用工具的语法如下:
.bat|.sh [-] .txt []
例如:
.bat - .txt .txt
如果不指定堆叠追踪文件,工具会从标准输入读取
其实,最简单的方法还是自己先查看混淆后的堆叠追踪文件,根据自己的经验找出最后导致崩溃的追踪信息,然后拿这个追踪信息去.txt文件中直接按查找键查找 。例如,假如我最后出错的代码定位的信息为com.go.cj.b.c(b.java 1234),那我们可以拿com.go.cj.b去.txt文件中直接按查找键查找 。
对于压缩代码,以上是工具能为我们做的,但是我们在平时的开发中养成一些习惯,也会有利于我们的代码缩减,一些良好的习惯建议如下:
二、减res文件夹
res文件夹里面主要就是包括各种布局文件,value文件,图片文件,原生文件 。这一块的处理方案主要采用2种方式,一是压缩资源,二是删除未使用的资源 。
1. 压缩资源
压缩资源又分2种思路,一是对资源进行压缩,二是使用更小的资源来替换当前的资源 。
2. 移除无用的资源
这里的移除无用的资源,主要是指2个方面,一是在工程里面直接删除没有使用的资源,二是不打包没有使用的资源 。
2.1 在工程里面直接删除没有使用的资源
这一点主要是使用lint检查并清除冗余资源 。如果你的资源是通过资源名称使用的( name,,)方法去获取到资源的id来使用资源(这种方式是通过反射的方法根据资源名称去获取资源的id),而不是直接通过R文件自动生成的id来使用资源的,lint会检测判定你这个文件并没有被使用,而作为未使用的文件列出来 。这时候就不能使用一键删除的功能,需要确认后自己手动删除 。例如我要使用.png这个资源,一般情况下我们是通过R..去使用的,如下代码
Resources res = context.getResources();Drawable doodleDrawable = res.getDrawable(R.drawable.doodle);
但是,有些时候我们需要在代码中动态地根据资源名称去使用资源,这时候就要用到()去获取到资源的id,然后再使用这个id去获取资源,示例代码如下:
Resources res = context.getResources();int doodleDrawableId = res.getIdentifier("doodle", "drawable", context.getPackageName());Drawable doodleDrawable = res.getDrawable(doodleDrawableId);
这种情况就不能使用一键删除的功能,需要确认后自己手动删除 。
具体使用步骤:
选中项目右键 =>=> Runby Name => 输入
2.2 不打包不需要使用的资源
这个方面也有2个可行的思想,一是利用 开启 的 进行构建打包,这时候没有被使用的资源将不会打进包里 。二是不打包未使用(不需要)的替代资源 。
(1) 利用 开启 的
的使用
需要和Code一起使用 。在代码中删除所有未使用的代码后,才可以知道哪些资源APK程序仍然使用,你必须先删除未使用的代码,才会成为无用的,从而被清除掉 。Code 部分在上面的减.dex文件——压缩代码 一节中已经讲过了 。下面为 的使用具体步骤:
添加 true属性在你的 build.文件中,相应代码块如下:
android {buildTypes {release {shrinkResources trueminifyEnabled trueproguardFiles getDefaultProguardFile('proguard-android.txt'),'proguard-rules.pro'}}}
目前还不支持移除定义在/目录下的资源文件(,,,),因为 AssetTool(AAPT)不允许 指定预定义的版本资源[issue 70869]
指定要忽略的资源文件或者一定要删除的资源文件
如果我们希望保留或丢弃特定的资源,需要在项目中创建一个XML文件,并使用标签,并使用tools:keep属性明确指定每个资源保留,或者使用tools:属性明确指定这个资源将要被舍弃移除 。两个属性都可以使用逗号(,)分隔符声明资源名称列表 。也可以使用* 作为匹配符,匹配名称 。
例如,XML文件,命名为keep.xml,这个文件需要保存在:res/raw/keep.xml,这样build的时候该文件才不会被打包到APK里面,示例代码块如下:

启用严格的检测
也通过搜索代码中是否包含资源名或者具有匹配符相同的名称来判断是否在build的时候删除资源 。所以,通常情况下,可以准确地确定资源使用 。但是如果你在代码中使用.()显式通过资源名称动态获取指定资源的Id,在默认情况下,这样资源具有匹配名称的格式为潜在的使用,无法去除 。
例如,下面的代码将导致所有img_前缀的资源都无法去除 。
String name = String.format("img_", angle + 1);res = getResources().getIdentifier(name, "drawable", getPackageName());
这是因为,在处理该资源文件时候的方式,遇到被判断为潜在使用的情况下,默认的值为 safe 。这时候就需要在上面的keep.xml文件中指定 为(只会保留有明确引用的资源,以及处理被 tools:keep 和 tools: 标注的资源) 。示例代码块如下:

但是这里面还有另外一种特殊情况就是,当上面代码中的文件名name没有显式地出现“img_”这个字符的时候,不需要指定 为模式构建的时候也能删除具有前缀为“img_”字符名称的资源 。也就是说,当我把name写在其他非代码类的文件中,然后在代码中读取的时候,这时候通过搜索代码是扫描不出相关的资源名称“img_”字符来的,所以这个时候具有前缀为“img_”字符名称的资源都会被删除了 。所以要特别注意这种特殊情况 。
(2) 不打包未使用(不需要)的替代资源
只删除你在代码中未使用资源,这意味着它不会删除不同的设备配置的可替代资源 。如果有必要,可以使用的属性删除替代资源文件 。
例如:我们项目中适配10种国家语言,而项目依赖了v7、v4等其他包里面包含20种国家语言,那么我们可以通过 删除剩余的可替代资源文件,这对于我们APK大小可减少不少 。
以下代码说明了如何限制语言资源:
android {defaultConfig {...resConfigs "en", "fr"}}
以上 属性只指定了英语和法语这2个语言的资源会被打进包里 。未指定的语言的任何资源都被删除 。当然如果不设置属性,默认会把所有的语言资源都会打进包里 。
同样的思想我们可以运用在图片资源上,但是我们不能做到几种屏幕密度的资源打在同一个包里面,另外的几种不要了 。我们只能做到打包一个只有1种屏幕密度图片资源的包 。就是我们下面要讲的使用APK 构建不同替代资源的APK 。
【APK瘦身——更全面的方案】(3)使用APK 构建不同替代资源的多个APK
APK 比起使用,能让应用程序更有效地构建一些形式的多个apk 。注意:这里的多个APK和使用构建出来的多个APK是不同的 。使用构建出来的APK是只含有不同的单套资源但功能用途一样的APK,而构建出来的APK是含有同样的资源但却是功能用途不一样的APK 。
APK 多APK只支持以下类型:
正是由于构建出来的APK只含有单套可替换资源,所以它的适用情景就是我们要根据用户的手机去提供基于不同屏幕分辨率(,mhdpi等),so库版本的单个APK,并且应用市场支持发布这种多个APK的功能(即要求应用市场能根据用户的手机的屏幕分辨率,CPU的架构而为用户选择对应的版本的APK提供下载) 。而目前只有支持这种 APK 发布功能,所以你的应用如果不是在上面发布的话,这种拆分多个APK发布的做法,你就了解一下就行了 。下面继续介绍
按屏幕密度拆分,配置代码如下:
android {...splits {density {enable trueexclude "ldpi", "tvdpi", "xxxhdpi"compatibleScreens 'small', 'normal', 'large', 'xlarge'}}
: 启用屏幕密度拆分机制
: 默认情况下,不设置这个属性所有屏幕密度都包括在内,如果设置,则显式声明移除一些密度 。
: 表示要包括哪些屏幕密度
reset( ): 重置屏幕密度列表为只包含一个空字符串 (这能够实现,在与一起使用时可以表示使用哪一个屏幕密度,而不是要忽略哪一些屏幕密度)
:表示兼容屏幕的列表 。这将会注入到中匹配的 节点 。这个设置是可选的 。
构建完成后可以在out/apk/目录下看到多个版本的APK 。
按 ABI 拆分,配置代码如下:
android {...splits {abi {enable truereset()include 'x86', 'armeabi-v7a', 'mips'universalApk true}}}
: 启用ABI拆分机制
: 不使用这个属性默认情况下所有ABI都包括在内,可以指明移除一些ABI 。
:指明要包含哪些ABI
reset():重置ABI列表为只包含一个空字符串(这可以实现,在与一起使用来可以表示要使用哪一个ABI,而不是要忽略哪一些ABI)
:指示是否打包一个通用版本(包含所有的ABI) 。默认值为 false 。
只支持这2种类型的分类,更多的多版本支持的知识是下一节的内容 。
(5)使用多版本的APK发布
APK 是一个在 Play,可以发布不同的应用程序,分别针对不同的设备配置特征 。每个APK是一个完整的、独立的应用程序版本,但他们分享在 Play相同的应用程序清单,必须共享相同的包名和与签名 。Play 会自动给你匹配相应的APK,这样我们的APK 就可以是分不同版本构建需要资源文件,从而减小APK的大小 。
APK 支持以下:
更多相关信息请参考 APK,这里不详细叙述 。
三、减.arsc文件
简单介绍下.arsc文件来源与作用:除了和res/raw资源被原装不动地打包进APK之外,其它的资源都会被编译或者处理 。除了资源之外,其它的资源都会被赋予一个资源ID 。打包工具负责编译和打包资源,编译完成之后,会生成一个.arsc文件和一个R.java,前者保存的是一个资源索引表,后者定义了各个资源ID常量,供在代码中索引资源 。
所有的png文件是以STORE的方式存储到apk里的,关于zip里的STORE和,详见:Zip (file )
通俗的说,当文件是的方式存储到zip,表示这个文件并没有经过压缩,如果是Defl:N的方式,表示通过 的方式压缩存储到zip 。
现在业内有一个开源的插件针对以上原理进行了一定的压缩,就是下面要讲的
微信资源压缩插件:
其原理就是:
其地址为(里面有详细的接入流程):资源混淆工具使用说明
四、减文件夹
目录可以存放各种文件,正常情况下,一般只存放以下几种文件:字体文件、WEB页面、配置文件、图片文件 。
上述几种文件除了配置文件之外,我们都可以进行适当的压缩处理:
字体文件:可以使用字体资源文件编辑神器进行压缩,其压缩方式其实就是通过删除不需要的字符从而减少APK的大小 。
WEB页面:可以考虑使用7zip压缩工具对该文件进行压缩,在正式使用的时候解压
图片文件:可以使用进行图片压缩
五、减lib文件夹
lib目录用于存放通过C或C++编写编译生成的so文件(库/JNI开发) 。
因为目前市场上主流的架构还主要是arm架构,所以如果不是必要的话,可以考虑不支持x86和mips架构,但这并不意味着CPU是x86或mips架构的手机就不能正常安装使用APK了,因为放在arm目录下的so库是可以兼容到其他架构的;
另外arm架构中的eabi-v7a相比于eabi只是在图形渲染方面有了很大的改进,所以如果so库对图形渲染没有很高的要求的话,完全可以把so库只存放在arm eabi目录中,这样可以大大减小APK的体积 。
lib瘦身主要是减小对 CPU 架构的支持,配置起来很简单,在 build. 使用配置需要用到的 CPU 架构,并将不需要兼容的 so 文件从项目中移除即可 。
示例代码块如下:
defaultConfig {ndk{// 设置支持的so库abiFilters 'armeabi', 'x86'}}
总结
以上所述的方法,并不需要全部都应用,而是应该根据实际情况来选用其中适合实际情况的最有效的方法 。根据笔者使用过的实际情况来说,以下几种方式对于减少包体是效果最明显的且施行起来也不难的:
欢迎大家斧正、交流、补充!