为啥要优化包体积
- 推广成本
- 下载转化率
- 运行内存
-
安装时间
体积优化思维导图.png
APK背景知识
对于APK瘦身,首先我们必须了解的知识点是APK的文件结构,那么上图:
APK文件结构.png
- Dex : 一般情况下,Android 应用在打包时通过 Android SDK 中的 dx 工具将 Java 字节码转换为 Dalvik 字节码。被DEX编译后可供Dalvik/ART虚拟机所理解的文件格式
- Res目录
res : 是 resource 的缩写,这个目录存放资源文件,会自动生成对应的 ID 并映射到 .R 文件中,访问直接使用资源 ID。 - Assets文件夹 :
存放需要打包到APK中的静态文件,assets不会自动生成对应的 ID,而是通过 AssetManager 类的接口获取。 - Native库 :
通常我们的so库都属于这个范畴。 - META-INF :
存放应用程序签名和证书的目录,签名信息可以验证 APK 文件的完整性。 - resources.arsc :
记录着资源文件和资源 ID 之间的映射关系,用来根据资源 ID 寻找资源。
由此可见:安装包的优化可以笼统的分为:资源优化、DEX文件优化两大部分
一、资源文件减包分析
优化思路 优化 -> 去重 -> 混淆
1.1 优化
在图片的格式选择上
webP压缩效果展示.png
此外 没有透明通道的PNG可以转换成jpg格式,有透明通道的png可以转成webP格式。以节省空间的占用
1.2 压缩
在Android编译过程中,下面代码中的文件格式不支持压缩:
/* these formats are already compressed, or don't compress well */
static const char* kNoCompressExt[] = {
".jpg", ".jpeg", ".png", ".gif",
".wav", ".mp2", ".mp3", ".ogg", ".aac",
".mpg", ".mpeg", ".mid", ".midi", ".smf", ".jet",
".rtttl", ".imy", ".xmf", ".mp4", ".m4a",
".m4v", ".3gp", ".3gpp", ".3g2", ".3gpp2",
".amr", ".awb", ".wma", ".wmv", ".webm", ".mkv"
};
Question : 为什么谷歌官方不支持这些文件格式的压缩?
-
1.时间与空间的收益
如果文件是没有压缩的,系统可以利用 mmap 的方式直接读取,而不需要一次性读到内存中 -
2.压缩效果不明显
如上方的注释所说大部分文件压缩效果并不明显,比如jpg、png格式的图片压缩率只有3%-5%,收益不大
1.3 去重
各工具优缺点以及准确性分析(unUsedResource)
Lint
Lint中提供了unUsedResource和unUsedId去检测无用、冗余的资源。
-
弊端
Lint作为静态代码检查工具分析的是编译前的代码,比如Lint会忽略Proguard的代码shrink,所以Lint不能检查出这些无用代码引用的无用资源。
Matrix-ApkChecker
输入apk检查 解决了Lint只能检查编译前资源的缺点
-
弊端:
类似于循环引用的资源引用方式无法被正确判定为无用资源
1.4 混淆
我们的项目中已经有了混淆的配置,但是并没有针对资源混淆的配置,资源混淆的思路就是把资源和文件的名字混淆成段路径:
R.string.name -> R.string.... res/drawable/icon -> res/s/a
Question : 为什么资源混淆可以减少APK体积?
- resource.arsc的文件格式解析
- 解析我们的apk可以发现resource.arsc与META-INF文件夹下的三个文件大小很大,原因就是他们内部保存了每个资源名称,我们在项目中有时候为了不造成冲突,就把资源名起的很长,那么这样就会导致apk的包很大,但是我们知道Android中的混淆是不会对资源文件进行混淆的,所以这时候我们就可以通过这个思路来减小包apk的大小了
shrinkResources资源压缩功能
在gradle中的android闭包中添加 shrinkResource true minifyEnabled true
如果ProGuard将无用代码移除,则代码引用的资源也被标记为无用资源,然后将其移除
-
弊端
没有从根本上处理resource.arsc文件 较为占空间的resource.arsc仍没有得到改善
仅将资源文件替换为空文件
这样实际上文件数量并没有得到改善,而且resource.arsc等文件的体积也没有任何变化
在Android编译过程中,Java Compiler会将代码中的资源引用根据R文件直接替换为常量,而R文件中的文件资源ID默认为连续的,删除某些资源会导致ID与资源无法一一对应
解决办法:可以使用资源混淆工具 AndResGurad 对打包好的apk进行处理
处理后release包大小立减1M
QQ浏览器截图20191204093020.png
- 针对资源混淆的工具 Github地址:张绍文的AndResGurad 参考文献:微信开源的资源混淆工具
二、DEX文件减包
- 2.1 删除DEX文件中data区的debugItems信息 - 方案
# 支付宝 App 构建优化解析:Android 包大小极致压缩 - 2.2 减少DEX分包以减少包体积
“define methods”是指真正在这个Dex中定义的方法,而"reference methods"指的是define methods中引用的方法
faceBook - reDex
三、 Matrix如何实现搜索APK中无用的资源文件
- 首先通过读取R.txt获取apk中声明的所有资源 写入set中;
- 通过读取smali文件中引用资源的指令 得出class中引用的资源Set;
- 通过ApkTool解析res目录下的xml文件、AndroidManifest.xml 以及 resource.arsc 得出资源之间的引用关系;
- 1.遍历DexFile,并使用Baskmali库将其编译成Smali文件
private void decodeCode() throws IOException {
for (String dexFileName : this.dexFileNameList) {
DexBackedDexFile dexFile = DexFileFactory.loadDexFile(new File(this.inputFile, dexFileName), Opcodes.forApi(15));
options = new BaksmaliOptions();
List classDefs = Ordering.natural().sortedCopy(dexFile.getClasses());
for (ClassDef classDef : classDefs) {
String[] lines = ApkUtil.disassembleClass(classDef, options);
if (lines != null)
readSmaliLines(lines);
}
}
BaksmaliOptions options;
}
- 2.遍历Smali文件,找到字符串常量
private void readSmaliLines(String[] lines) {
if (lines == null) {
return;
}
for (String line : lines) {
line = line.trim();
if (!Util.isNullOrNil(line))
if (line.startsWith("const")) {
String[] columns = line.split(",");
if (columns.length == 2) {
String resId = parseResourceId(columns[1].trim());
if ((!Util.isNullOrNil(resId)) && (this.resourceDefMap.containsKey(resId)))
this.resourceRefSet.add(this.resourceDefMap.get(resId));
}
}
else if (line.startsWith("sget")) {
String[] columns = line.split(" ");
if (columns.length == 3) {
String resourceRef = parseResourceNameFromProguard(columns[2]);
if (Util.isNullOrNil(resourceRef))
continue;
if (this.styleableMap.containsKey(resourceRef))
{
for (String attr : (Set)this.styleableMap.get(resourceRef))
this.resourceRefSet.add(this.resourceDefMap.get(attr));
}
else
this.resourceRefSet.add(resourceRef);
}
}
}
}
- 3.遍历XML、resource.arsc
private void decodeResources() throws IOException, InterruptedException, AndrolibException, XmlPullParserException {
File manifestFile = new File(inputFile, ApkConstants.MANIFEST_FILE_NAME);
File arscFile = new File(inputFile, ApkConstants.ARSC_FILE_NAME);
File resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_NAME);
if (!resDir.exists()) {
resDir = new File(inputFile, ApkConstants.RESOURCE_DIR_PROGUARD_NAME);
}
Map<String, Set<String>> fileResMap = new HashMap<>();
Set<String> valuesReferences = new HashSet<>();
ApkResourceDecoder.decodeResourcesRef(manifestFile, arscFile, resDir, fileResMap, valuesReferences);
Map<String, String> resguardMap = config.getResguardMap();
for (String resource : fileResMap.keySet()) {
Set<String> result = new HashSet<>();
for (String resName : fileResMap.get(resource)) {
if (resguardMap.containsKey(resName)) {
result.add(resguardMap.get(resName));
} else {
result.add(resName);
}
}
if (resguardMap.containsKey(resource)) {
nonValueReferences.put(resguardMap.get(resource), result);
} else {
nonValueReferences.put(resource, result);
}
}
for (String resource : valuesReferences) {
if (resguardMap.containsKey(resource)) {
resourceRefSet.add(resguardMap.get(resource));
} else {
resourceRefSet.add(resource);
}
}
for (String resource : resourceRefSet) {
readChildReference(resource);
}
for (String resource : unusedResSet) {
if (ignoreResource(resource)) {
resourceRefSet.add(resource);
ignoreChildResource(resource);
}
}
}
参考文献:
网友评论