為啥要優(yōu)化包體積
- 推廣成本
- 下載轉化率
- 運行內存
-
安裝時間
體積優(yōu)化思維導圖.png
APK背景知識
對于APK瘦身,首先我們必須了解的知識點是APK的文件結構刁愿,那么上圖:
- Dex : 一般情況下,Android 應用在打包時通過 Android SDK 中的 dx 工具將 Java 字節(jié)碼轉換為 Dalvik 字節(jié)碼拖云。被DEX編譯后可供Dalvik/ART虛擬機所理解的文件格式
- Res目錄
res : 是 resource 的縮寫吴超,這個目錄存放資源文件奴烙,會自動生成對應的 ID 并映射到 .R 文件中庄呈,訪問直接使用資源 ID蜕煌。 - Assets文件夾 :
存放需要打包到APK中的靜態(tài)文件,assets不會自動生成對應的 ID诬留,而是通過 AssetManager 類的接口獲取斜纪。 - Native庫 :
通常我們的so庫都屬于這個范疇。 - META-INF :
存放應用程序簽名和證書的目錄文兑,簽名信息可以驗證 APK 文件的完整性盒刚。 - resources.arsc :
記錄著資源文件和資源 ID 之間的映射關系,用來根據(jù)資源 ID 尋找資源绿贞。
由此可見:安裝包的優(yōu)化可以籠統(tǒng)的分為:資源優(yōu)化因块、DEX文件優(yōu)化兩大部分
一、資源文件減包分析
優(yōu)化思路 優(yōu)化 -> 去重 -> 混淆
1.1 優(yōu)化
在圖片的格式選擇上
此外 沒有透明通道的PNG可以轉換成jpg格式樟蠕,有透明通道的png可以轉成webP格式贮聂。以節(jié)省空間的占用
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.時間與空間的收益
如果文件是沒有壓縮的,系統(tǒng)可以利用 mmap 的方式直接讀取歼冰,而不需要一次性讀到內存中2.壓縮效果不明顯
如上方的注釋所說大部分文件壓縮效果并不明顯靡狞,比如jpg、png格式的圖片壓縮率只有3%-5%隔嫡,收益不大
1.3 去重
各工具優(yōu)缺點以及準確性分析(unUsedResource)
Lint
Lint中提供了unUsedResource和unUsedId去檢測無用甸怕、冗余的資源甘穿。
-
弊端
Lint作為靜態(tài)代碼檢查工具分析的是編譯前的代碼,比如Lint會忽略Proguard的代碼shrink梢杭,所以Lint不能檢查出這些無用代碼引用的無用資源温兼。
Matrix-ApkChecker
輸入apk檢查 解決了Lint只能檢查編譯前資源的缺點
-
弊端:
類似于循環(huán)引用的資源引用方式無法被正確判定為無用資源
1.4 混淆
我們的項目中已經(jīng)有了混淆的配置,但是并沒有針對資源混淆的配置武契,資源混淆的思路就是把資源和文件的名字混淆成段路徑:
R.string.name -> R.string.... res/drawable/icon -> res/s/a
Question : 為什么資源混淆可以減少APK體積募判?
- resource.arsc的文件格式解析
- 解析我們的apk可以發(fā)現(xiàn)resource.arsc與META-INF文件夾下的三個文件大小很大,原因就是他們內部保存了每個資源名稱咒唆,我們在項目中有時候為了不造成沖突届垫,就把資源名起的很長,那么這樣就會導致apk的包很大全释,但是我們知道Android中的混淆是不會對資源文件進行混淆的装处,所以這時候我們就可以通過這個思路來減小包apk的大小了
shrinkResources資源壓縮功能
在gradle中的android閉包中添加 shrinkResource true minifyEnabled true
如果ProGuard將無用代碼移除,則代碼引用的資源也被標記為無用資源浸船,然后將其移除
-
弊端
沒有從根本上處理resource.arsc文件 較為占空間的resource.arsc仍沒有得到改善
僅將資源文件替換為空文件
這樣實際上文件數(shù)量并沒有得到改善妄迁,而且resource.arsc等文件的體積也沒有任何變化
在Android編譯過程中,Java Compiler會將代碼中的資源引用根據(jù)R文件直接替換為常量糟袁,而R文件中的文件資源ID默認為連續(xù)的判族,刪除某些資源會導致ID與資源無法一一對應
解決辦法:可以使用資源混淆工具 AndResGurad 對打包好的apk進行處理
處理后release包大小立減1M
- 針對資源混淆的工具 Github地址:張紹文的AndResGurad 參考文獻:微信開源的資源混淆工具
二、DEX文件減包
- 2.1 刪除DEX文件中data區(qū)的debugItems信息 - 方案
# 支付寶 App 構建優(yōu)化解析:Android 包大小極致壓縮 - 2.2 減少DEX分包以減少包體積
“define methods”是指真正在這個Dex中定義的方法项戴,而"reference methods"指的是define methods中引用的方法
faceBook - reDex
三形帮、 Matrix如何實現(xiàn)搜索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);
}
}
}
參考文獻: