為什么要優化包體積
- 下載轉化率:安裝包越小,轉化率越高;
- 推廣成本:渠道推廣成本和廠商預裝的單價
- 應用市場:App Store和Google Play對安裝包大小都有限制;
應用性能:
- 安裝時間:文件拷貝、Library 解壓、編譯 ODEX、簽名校驗
- 運行內存:Resource 資源、Library 以及 Dex 類加載這些都會占用不少的內存
- ROM空間:閃存空間不足,非常容易出現寫入放大的情況
包體積優化
APK分析
- 使用 ApkTool 反編譯工具分析 APK;
- 使用AS 2.2之后提供的Analyze APK;
- 使用 nimbledroid 進行 APK 性能分析
Proguard
- 混淆之后,默認會在工程目錄 app/build/outputs/mapping/release 下生成一個 mapping.txt 文件,這就是 混淆規則;
1. 作用:
- 瘦身:它可以檢測并移除未使用到的類、方法、字段以及指令、冗余代碼,并能夠對字節碼進行深度優化。最后,它還會將類中的字段、方法、類的名稱改成簡短無意義的名字。
- 安全:增加代碼被反編譯的難度,一定程度上保證代碼的安全。
2. 功能
- 壓縮(Shrinking): 默認開啟,以減小應用體積,移除未被使用的類和成員
-dontshrink 關閉壓縮
- 優化(Optimization): 默認開啟,在 字節碼級別執行優化,讓應用 運行的更快
-dontoptimize 關閉優化
-optimizationpasses n 表示proguard對代碼進行迭代優化的次數,Android一般為5
- 混淆(Obfuscation): 默認開啟,增大反編譯難度,類和類成員會被隨機命名
-dontobfuscate 關閉混淆
3. 優化細節
1)、優化了 Gson 庫的使用。
2)、把類都標記為 final。
3)、把枚舉類型簡化為常量。
4)、把一些類都垂直合并進當前類的結構中。
5)、把一些類都水平合并進當前類的結構中。
6)、移除 write-only 字段。
7)、把類標記為私有的。
8)、把字段的值跨方法地進行傳遞。
9)、把一些方法標記為私有、靜態或 final。
10)、解除方法的 synchronized 標記。
11)、移除沒有使用的方法參數。
4. 配置
buildTypes {
release {
// 1、是否進行混淆
minifyEnabled true
// 2、開啟zipAlign可以讓安裝包中的資源按4字節對齊,這樣可以減少應用在運行時的內存消耗
zipAlignEnabled true
// 3、移除無用的resource文件:當ProGuard 把部分無用代碼移除的時候,
// 這些代碼所引用的資源也會被標記為無用資源,然后
// 系統通過資源壓縮功能將它們移除。
// 需要注意的是目前資源壓縮器目前不會移除values/文件夾中
// 定義的資源(例如字符串、尺寸、樣式和顏色)
// 開啟后,Android構建工具會通過ResourceUsageAnalyzer來檢查
// 哪些資源是無用的,當檢查到無用的資源時會把該資源替換
// 成預定義的版本。主要是針對.png、.9.png、.xml提供了
// TINY_PNG、TINY_9PNG、TINY_XML這3個byte數組的預定義版本。
// 資源壓縮工具默認是采用安全壓縮模式來運行,可以通過開啟嚴格壓縮模式來達到更好的瘦身效果。
shrinkResources true
// 4、混淆文件的位置,其中 proguard-android.txt 為sdk默認的混淆配置,
// 它的位置位于android-sdk/tools/proguard/proguard-android.txt,
// 此外,proguard-android-optimize.txt 也為sdk默認的混淆配置,
// 但是它默認打開了優化開關。并且,我們可在配置混淆文件將android.util.Log置為無效代碼,
// 以去除apk中打印日志的代碼。而 proguard-rules.pro 是該模塊下的混淆配置。
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
5. 混淆的基本規則
每個module創建時都會創建一個混淆文件proguard-rules.pro
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in C:\sdk\android-sdk-windows/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the proguardFiles
# directive in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# 代碼混淆壓縮比,在0~7之間
-optimizationpasses 5
# 去除編譯時警告
-ignorewarnings
#不壓縮輸入的類文件
-dontshrink
#不優化輸入的類文件
-dontoptimize
# 不混淆輸入的類文件
#-dontobfuscate
# 混合時不使用大小寫混合,混合后的類名為小寫
-dontusemixedcaseclassnames
# 指定不去忽略非公共庫的類
-dontskipnonpubliclibraryclasses
# 指定不去忽略非公共庫的類成員
-dontskipnonpubliclibraryclassmembers
#把混淆類中的方法名也混淆了
-useuniqueclassmembernames
#優化時允許訪問并修改有修飾符的類和類的成員
-allowaccessmodification
#以下是打印出關鍵的流程日志
# 不做預校驗,preverify是proguard的四個步驟之一,Android不需要preverify,去掉這一步能夠加快混淆速度。
-dontpreverify
#混淆時是否記錄日志
-verbose
#apk包內所有class的內部結構
-dump class_files.txt
#未混淆的類和成員
-printseeds seeds.txt
#列出從apk中刪除的代碼
-printusage unsed.txt
#混淆前后的映射
-printmapping mapping.txt
# 避免混淆泛型
-keepattributes Signature
#google推薦算法
-optimizations !code/simplification/arithmetic,!code/simplification/cast,!field/*,!class/merging/*
# 避免混淆Annotation注解、內部類、泛型、匿名類
-keepattributes *Annotation*,InnerClasses,Signature,EnclosingMethod
#js調用java方法
-keepattributes *JavascriptInterface*
#將文件來源重命名為“SourceFile”字符串
-renamesourcefileattribute SourceFile
# 保留行號
-keepattributes SourceFile,LineNumberTable
# 處理support包
-dontnote android.support.**
-dontwarn android.support.**
# 保留繼承的
-keep public class * extends android.support.v4.**
-keep public class * extends android.support.v7.**
-keep public class * extends android.support.annotation.**
# 保留R下面的資源
-keep class **.R$* {*;}
#反射中使用的元素,需要保證類名,方法名,屬性名不變,否則混淆后會反射不了
# 保留四大組件,自定義的Application等這些類不被混淆
#四大組件必須在AndroidManifest中注冊,混淆后類名發生更改
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Appliction
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.app.backup.BackupAgentHelper
-keep public class * extends android.preference.Preference
-keep public class * extends android.view.View
-keep public class com.android.vending.licensing.ILicensingService
# 保持測試相關的代碼
-dontnote junit.framework.**
-dontnote junit.runner.**
-dontwarn android.test.**
-dontwarn android.support.test.**
-dontwarn org.junit.**
# 保留在Activity中的方法參數是view的方法,
# 這樣以來我們在layout中寫的onClick就不會被影響
-keepclassmembers class * extends android.app.Activity{
public void *(android.view.View);
}
# 對于帶有回調函數的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}
# 保留本地native方法不被混淆
-keepclasseswithmembernames class * {
native <methods>;
}
# 保留枚舉類不被混淆
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
# 保留Parcelable序列化類不被混淆
-keep class * implements android.os.Parcelable {
public static final android.os.Parcelable$Creator *;
}
#保持所有實現 Serializable 接口的類成員
-keepclassmembers class * implements java.io.Serializable {
static final long serialVersionUID;
private static final java.io.ObjectStreamField[] serialPersistentFields;
private void writeObject(java.io.ObjectOutputStream);
private void readObject(java.io.ObjectInputStream);
java.lang.Object writeReplace();
java.lang.Object readResolve();
}
#assume no side effects:刪除android.util.Log輸出的日志
-assumenosideeffects class android.util.Log {
public static *** v(...);
public static *** d(...);
public static *** i(...);
public static *** w(...);
public static *** e(...);
}
#保留Keep注解的類名和方法
-keep,allowobfuscation @interface android.support.annotation.Keep
-keep @android.support.annotation.Keep class *
-keepclassmembers class * {
@android.support.annotation.Keep *;
}
#Fragment不需要在AndroidManifest.xml中注冊,需要額外保護下
-keep public class * extends android.support.v4.app.Fragment
-keep public class * extends android.app.Fragment
# webView處理,項目中沒有使用到webView忽略即可
-keepclassmembers class fqcn.of.javascript.interface.for.webview {
public *;
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.WebView, java.lang.String, android.graphics.Bitmap);
public boolean *(android.webkit.WebView, java.lang.String);
}
-keepclassmembers class * extends android.webkit.webViewClient {
public void *(android.webkit.webView, jav.lang.String);
}
# 對于帶有回調函數的onXXEvent、**On*Listener的,不能被混淆
-keepclassmembers class * {
void *(**On*Event);
void *(**On*Listener);
}
- 在 AndroidMainfest 中的類默認不會被混淆,所以四大組件和 Application 的子類和 Framework 層下所有的類默認不會進行混淆,
并且自定義的 View 默認也不會被混淆。因此,我們不需要手動在 proguard-rules.pro 中去添加;
優化方式
業務梳理:
- 刪除無用或者低價值的業務,永遠都是最有效的性能優化方式;
開發模式升級:
- 如果所有的功能都不能移除,那可能需要倒逼開發模式的轉變,更多地采用小程序、H5 這樣開發模式;
代碼
- 對于大部分應用來說,Dex 都是包體積中的大頭。
- 而且 Dex 的數量對用戶安裝時間也是一個非常大的挑戰
- 在不砍功能的前提下,我們看看有哪些方法可以減少這部分空間。
1. ProGuard
- 要仔細檢查最終合并的 ProGuard 配置文件,是不是存在過度 keep 的現象。
- 可以通過下面的方法輸出 ProGuard 的最終配置: -printconfiguration configuration.txt
- 一般來說,應用都會 keep 住四大組件以及 View 的部分方法,這樣是為了在代碼以及 XML 布局中可以引用到它們
-keep public class * extends android.app.Activity
-keep public class * extends android.app.Application
-keep public class * extends android.app.Service
-keep public class * extends android.content.BroadcastReceiver
-keep public class * extends android.content.ContentProvider
-keep public class * extends android.view.View
- 事實上,我們完全可以把非 exported 的四大組件以及 View 混淆,但是需要完成下面幾個工作:
- XML替換: 在代碼混淆之后,需要同時修改 AndroidManifest 以及資源 XML 中引用的名稱。
- 代碼替換: 需要遍歷其他已經混淆好的代碼,將變量或者方法體中定義的字符串也同時修改。
- 推薦使用 ASM
- 餓了么曾經開源過一個可以實現四大組件和 View 混淆的組件Mess,可供參考;
- 要注意的是,代碼中不能出現經過運算得到的類名,這種情況會導致替換失敗。
// 情況一:變量
public String activityName = "com.sample.TestActivity";
// 情況二:方法體
startActivity(new Intent(this, "com.sample.TestActivity"));
// 情況三:通過運算得到,不支持
startActivity(new Intent(this, "com.sample" + ".TestActivity"));
- Android Studio 3.0 推出了新 Dex 編譯器 D8 與新混淆工具 R8
1)、Dex的編譯時間更短。
2)、.dex文件更小。
3)、D8 編譯的 .dex 文件擁有更好的運行時性能。
4)、包含 Java 8 語言支持的處理。
- 開啟D8: gradle.properties 文件中 android.enableD8 = true
- Android Studio 3.1 或之后的版本 D8 將會被作為默認的 Dex 編譯器。
- R8 是 Proguard 壓縮與優化部分的替代品,
- Android Studio 3.4 或 Android Gradle 插件 3.4.0 及其更高版本,R8 會作為默認編譯器。
- 否則gradle.properties 中配置支持R8:
android.enableR8=true
android.enableR8.libraries=true
- 去掉 Debug 信息或者去掉行號
- 通過相同的 ProGuard 規則生成一個 Debug 包和 Release 包,大小卻又差異,就是差在DebugItem;
- DebugItem包含:
- 調試的信息。函數的參數變量和所有的局部變量。
- 排查問題的信息。所有的指令集行號和源文件行號的對應關系。
- 占dex的5.5%左右
- ProGuard 配置中一般我們也會通過下面的方式保留行號信息: -keepattributes SourceFile, LineNumberTable
- debugItem 能直接去掉嗎,顯然不能,如果去掉了,那所有上報的 crash 信息就會沒有行號,所有的行號都會變成 -1,會被噴的找不到北。
- 下面是支付寶的兩個方案:
- 方案1:行號查找離線化
讓本來存放在 App 中的行號對應關系提前抽離出來存放在服務端,crash 上報的時候通過提前
抽離的行號表進行行號反解,解決 crash 信息上報無行號,無法定位的問題,主要步驟如下:
1. 修改 proguard:利用 proguard 來刪除 debugItem (去掉 -keep lineNumberTable),在刪除行號表之前 dump 出一個臨時的 dex。
2. 修改 dexdump:把臨時的 dex 中的行號表關系 dump 成一個 dexpcmapping 文件(指令集行號和源文件行號映射關系),并存至服務端。
3. hook app runtime 的 crash handler,把 crash 時的指令集行號上報到反解平臺。
4. 反解平臺通過上報指令集行號和提前準備好 dexpcmapping 文件反解出正確的行號。
保留一小塊 debugItem,讓系統查找行號的時候指令集行號和源文件行號保持一致,
這樣就什么都不用做,任何監控上報的行號都直接變成了指令集行號,只需修改 dex 文件
- 使用RxDex:(支付寶參考的是 Facebook 的一個開源編譯工具ReDex)
{
"redex" : {
"passes" : [
"StripDebugInfoPass",
"RegAllocPass"
]
},
"StripDebugInfoPass" : {
"drop_all_dbg_info" : false,
"drop_local_variables" : true,
"drop_line_numbers" : false,
"drop_src_files" : false,
"use_whitelist" : false,
"cls_whitelist" : [],
"method_whitelist" : [],
"drop_prologue_end" : true,
"drop_epilogue_begin" : true,
"drop_all_dbg_info_if_empty" : true
},
"RegAllocPass" : {
"live_range_splitting": false
}
}
2. Dex 分包
跨 Dex 調用造成冗余信息:
- 例如:Class A 與 Class B 分別編譯到不同的 Dex 中,由于 method a 調用了 method b,所以在 classesA.dex 中也需要加上 method b 的 id
- 造成method id 爆表:
- 每個 Dex 的 method id 需要小于 65536,因為 method id 的大量冗余導致每個 Dex 真正可以放的 Class 變少,這是造成最終編譯的Dex 數量增多。
- 造成信息冗余
- 為了進一步減少 Dex 的數量,我們希望每個 Dex 的方法數都是滿的,即分配了 65536 個方法。
- 如何實現 Dex 信息有效率提升呢?
- 需要將有調用關系的類和方法分配到同一個 Dex 中,即減少跨 Dex 的調用的情況;
- ReDex 在分析類調用關系后,使用的是貪心算法計算局部最優值,具體算法可查看CrossDexDefMinimizer。
- 通過Redex實現,配置如下
{
"redex" : {
"passes" : [
"StripDebugInfoPass",
"InterDexPass",
"RegAllocPass"
]
},
"StripDebugInfoPass" : {
"drop_all_dbg_info" : false,
"drop_local_variables" : true,
"drop_line_numbers" : false,
"drop_src_files" : false,
"use_whitelist" : false,
"cls_whitelist" : [],
"method_whitelist" : [],
"drop_prologue_end" : true,
"drop_epilogue_begin" : true,
"drop_all_dbg_info_if_empty" : true
},
"InterDexPass" : {
"minimize_cross_dex_refs": true,
"minimize_cross_dex_refs_method_ref_weight": 100,
"minimize_cross_dex_refs_field_ref_weight": 90,
"minimize_cross_dex_refs_type_ref_weight": 100,
"minimize_cross_dex_refs_string_ref_weight": 90
},
"RegAllocPass" : {
"live_range_splitting": false
},
"string_sort_mode" : "class_order",
"bytecode_sort_mode" : "class_order"
}
3. Dex 壓縮
- Facebook App 的 classes.dex 只是一個殼,真正的代碼都放到 assets 下面。它們把所有的 Dex 都合并成同一個 secondary.dex.jar.xzs 文件,并通過 XZ 壓縮。
- XZ 壓縮算法和 7-Zip 一樣,內部使用的都是 LZMA 算法。對于 Dex 格式來說,XZ 的壓縮率可以比 Zip 高 30% 左右。
- 存在的問題:
- 首次啟動解壓:應用首次啟動的時候,需要將 secondary.dex.jar.xzs 解壓縮,根據上圖的配置信息,應該一共有 11 個 Dex。
- ODEX 文件生成:Facebook 為了解決這個問題,使用了 ReDex 另外一個超級硬核的方法,那就是oatmeal
Native Library
- 各種三方庫導致APK 中 Native Library 的體積越來越大
- 去除 Debug 信息
- 使用 c++_shared
- Library 壓縮
- 跟 Dex 壓縮一樣,Library 優化最有效果的方法也是使用 XZ 或者 7-Zip 壓縮。
- 只需要加載少數啟動過程相關的 Library,其他的 Library 我們都在首次啟動時解壓。
- Facebook 有一個 So 加載的開源庫SoLoader,它可以跟這套方案配合使用
- Library 合并與裁剪
- Facebook 中的編譯構建工具Buck也有兩個比較硬核的高科技
- Library 合并。在 Android 4.3 之前,進程加載的 Library 數量是有限制的。在編譯過程,我們可以自動將部分 Library 合并成一個。
- 具體思路你可以參考文章《Android native library merging》以及Demo。
- Library 裁剪。Buck 里面有一個relinker的功能,原理就是分析代碼中 JNI 方法以及不同 Library 的方法調用,找到沒有無用的導出 symbol,將它們刪掉。
這樣 linker 在編譯的時候也會把對應的無用代碼同時刪掉,這個方法相當于實現了 Library 的 ProGuard Shrinking 功能。
- So 移除方案
- 目前,Android 一共 支持7種不同類型的 CPU 架構,其中x86架構目前沒有真機,只是模擬器會用的這個架構的so庫,那么生產包就可以去掉x86,x86_64的so文件;
- 在 build.gradle 中配置這個 abiFiliters 去設置 App 支持的 So 架構
//生產環境
release {
resValue "string", "app_name", "@string/app_name_release"
ndk {
abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
//有些觀點是只留下armeabi即可,armeabi 目錄下的 So 可以兼容別的平臺上的 So,
//但是,這樣 別的平臺使用時性能上就會有所損耗,失去了對特定平臺的優化,而且近期國內的應用市場也開始要求適配64位的應用了
}
}
//開發環境
debug {
resValue "string", "app_name", "@string/app_name_debug"
ndk {
rootProject.ext.ndkAbis.each { abi ->
abiFilter(abi)
}
}
}
包體積監控
- 大小監控
- 如果某個版本體積增長過大,需要分析具體原因,是否有優化空間。
- 依賴監控
- 添加開源庫時需要注意其大小,可以對其進行功能剝離,只引入部分需要的代碼
- 規則監控: 例如無用資源、大文件、重復文件、R 文件等,參考微信Matrix的ApkChecker
資源優化
使用 AndResGuard 工具
- 資源混淆
- ProGuard 的核心優化主要有三個:Shrink、Optimize 和 Obfuscate,也就是裁剪、優化和混淆。
- resources.arsc: 因為資源索引文件 resources.arsc 需要記錄資源文件的名稱與路徑,使用混淆后的短路徑 res/s/a,可以減少整個文件的大小
- metadata 簽名文件:簽名文件 MF 與 SF都需要記錄所有文件的路徑以及它們的哈希值,使用短路徑可以減少這兩個文件的大小
- ZIP 文件索引:ZIP 文件格式里面也需要記錄每個文件 Entry 的路徑、壓縮算法、CRC、文件大小等信息。使用短路徑,本身就可以減少記錄文件路徑的字符串大小;
- 極限壓縮
- 更高的壓縮率。雖然我們使用的還是 Zip 算法,但是利用了 7-Zip 的大字典優化,APK 的整體壓縮率可以提升 3% 左右。
- 壓縮更多的文件。Android 編譯過程中,下面這些格式的文件會指定不壓縮;在 AndResGuard 中,支持針對 resources.arsc、PNG、JPG 以及 GIF 等文件的強制壓縮。
/* 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"
};
- 為什么 Android 系統會專門選擇不去壓縮這些文件呢?
- 壓縮效果并不明顯。這些格式的文件大部分本身已經壓縮過
- 讀取時間與內存的考慮。如果文件是沒有壓縮的,系統可以利用 mmap 的方式直接讀取,而不需要一次性解壓并放在內存中
- Android 6.0 之后 AndroidManifest 支持不壓縮 Library 文件,這樣安裝 APK 的時候也不需要把 Library 文件解壓出來,系統可以直接 mmap 安裝包中的 Library 文件。
android:extractNativeLibs=“true”
apply plugin: 'AndResGuard'
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.tencent.mm:AndResGuard-gradle-plugin:1.2.18'
}
}
andResGuard {
// mappingFile = file("./resource_mapping.txt")
mappingFile = null
use7zip = true
useSign = true
// 打開這個開關,會keep住所有資源的原始路徑,只混淆資源的名字
keepRoot = false
// 設置這個值,會把arsc name列混淆成相同的名字,減少string常量池的大小
fixedResName = "arg"
// 打開這個開關會合并所有哈希值相同的資源,但請不要過度依賴這個功能去除去冗余資源
mergeDuplicatedRes = true
whiteList = [
// for your icon
"R.drawable.icon",
// for fabric
"R.string.com.crashlytics.*",
// for google-services
"R.string.google_app_id",
"R.string.gcm_defaultSenderId",
"R.string.default_web_client_id",
"R.string.ga_trackingId",
"R.string.firebase_database_url",
"R.string.google_api_key",
"R.string.google_crash_reporting_api_key"
]
compressFilePattern = [
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
]
sevenzip {
artifact = 'com.tencent.mm:SevenZip:1.2.18'
//path = "/usr/local/bin/7za"
}
/**
* 可選: 如果不設置則會默認覆蓋assemble輸出的apk
**/
// finalApkBackupPath = "${project.rootDir}/final.apk"
/**
* 可選: 指定v1簽名時生成jar文件的摘要算法
* 默認值為“SHA-1”
**/
// digestalg = "SHA-256"
}
進階的優化方法
- 資源合并: 所有的資源文件都合并成同一個大文件
- 事實上,大部分的換膚方案也是采用這個思路,這個大資源文件就相當于一套皮膚。因此我們完全可以把這套方案推廣開來,但是實現起來還是需要解決不少問題的。
- 資源的解析。我們需要模擬系統實現資源文件的解析,例如把 PNG、JPG 以及 XML 文件轉換為 Bitmap 或者 Drawable,這樣獲取資源的方法需要改成我們自定義的方法。
// 系統默認的方式
Drawable drawable = getResouces().getDrawable(R.drawable.loading);
// 新的獲取方式
Drawable drawable = CustomResManager.getDrawable(R.drawable.loading);
2. 資源的管理??紤]到內存和啟動時間,所有的資源也是用時加載,我們只需要使用 mmap 來加載“Big resource File”。
同時我們還要實現自己的資源緩存池 ResourceCache,釋放不再使用的資源文件,這部分內容你可以參考類似 Glide 圖片庫的實現。
- 無用資源
- Lint: 使用Lint這個靜態代碼掃描工具,它里面就支持 Unused Resources 掃描。
- shrinkResources:資源壓縮, 需要配合 ProGurad 的“minifyEnabled”功能同時使用
//如果 ProGuard 把部分無用代碼移除,這些代碼所引用的資源也會被標記為無用資源,然后通過資源壓縮功能將它們移除
//沒有真正刪除,而是替換成空文件,防止resources.arsc 和 R.java 文件的資源 ID 會改變
android {
...
buildTypes {
release {
shrinkResources true
minifyEnabled true
}
}
}
- 對于 Assets 資源,代碼中會有各種各樣的引用方式,如果想準確地識別出無用的 Assets 并不是那么容易。在 Matrix 中嘗試提供了一套簡單的實現,可以參考UnusedAssetsTask;
- 避免產生 Java access 方法
- 在開發過程中需要注意在可能產生 access 方法的情況下適當調整,比如去掉 private,改為 package 可見性。
- 使用 ASM 在編譯時刪除生成的 access 方法。
- 建議直接使用 ByteX 的 access_inline 插件
- 除了 access_inlie 之外,在 ByteX 中還有 四個 很實用的代碼優化 Gradle 插件可以幫助我們有效減小 Dex 文件的大小
1、編譯期間 內聯常量字段:const_inline。
2、編譯期間 移除多余賦值代碼:field_assign_opt。
3、編譯期間 移除 Log 代碼:method_call_opt。
4、編譯期間 內聯 Get / Set 方法:getter-setter-inline-plugin。
- 重復資源優化
- 在 Android 構建工具執行 package${flavorName}Task 之前通過修改 Compiled Resources 來實現重復資源的去除
- 首先,通過資源包中的每個ZipEntry的CRC-32 checksum來篩選出重復的資源。
- 然后,通過android-chunk-utils修改resources.arsc,把這些重復的資源都重定向到同一個文件上。
- 最后,把其它重復的資源文件從資源包中刪除,僅保留第一份資源。
實現代碼如下:
variantData.outputs.each {
def apFile = it.packageAndroidArtifactTask.getResourceFile();
it.packageAndroidArtifactTask.doFirst {
def arscFile = new File(apFile.parentFile, "resources.arsc");
JarUtil.extractZipEntry(apFile, "resources.arsc", arscFile);
def HashMap<String, ArrayList<DuplicatedEntry>> duplicatedResources = findDuplicatedResources(apFile);
removeZipEntry(apFile, "resources.arsc");
if (arscFile.exists()) {
FileInputStream arscStream = null;
ResourceFile resourceFile = null;
try {
arscStream = new FileInputStream(arscFile);
resourceFile = ResourceFile.fromInputStream(arscStream);
List<Chunk> chunks = resourceFile.getChunks();
HashMap<String, String> toBeReplacedResourceMap = new HashMap<String, String>(1024);
// 處理arsc并刪除重復資源
Iterator<Map.Entry<String, ArrayList<DuplicatedEntry>>> iterator = duplicatedResources.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<String, ArrayList<DuplicatedEntry>> duplicatedEntry = iterator.next();
// 保留第一個資源,其他資源刪除掉
for (def index = 1; index < duplicatedEntry.value.size(); ++index) {
removeZipEntry(apFile, duplicatedEntry.value.get(index).name);
toBeReplacedResourceMap.put(duplicatedEntry.value.get(index).name, duplicatedEntry.value.get(0).name);
}
}
for (def index = 0; index < chunks.size(); ++index) {
Chunk chunk = chunks.get(index);
if (chunk instanceof ResourceTableChunk) {
ResourceTableChunk resourceTableChunk = (ResourceTableChunk) chunk;
StringPoolChunk stringPoolChunk = resourceTableChunk.getStringPool();
for (def i = 0; i < stringPoolChunk.stringCount; ++i) {
def key = stringPoolChunk.getString(i);
if (toBeReplacedResourceMap.containsKey(key)) {
stringPoolChunk.setString(i, toBeReplacedResourceMap.get(key));
}
}
}
}
} catch (IOException ignore) {
} catch (FileNotFoundException ignore) {
} finally {
if (arscStream != null) {
IOUtils.closeQuietly(arscStream);
}
arscFile.delete();
arscFile << resourceFile.toByteArray();
addZipEntry(apFile, arscFile);
}
}
}
}
- 圖片壓縮
- 通過https://tingpng.com/網站
- McImage
- TinyPngPlugin
- TinyPIC_Gradle_Plugin
- 需要注意的是,在 Android 的構建流程中,AAPT 會使用內置的壓縮算法來優化 res/drawable/ 目錄下的 PNG 圖片,
但這可能會導致本來已經優化過的圖片體積變大,因此,可以通過在 build.gradle 中 設置 cruncherEnabled 來禁止 AAPT 來優化 PNG 圖片
aaptOptions {
cruncherEnabled = false
}
- 圖片格式的選擇:VD(純色icon)->WebP(非純色icon)->Png(更好效果) ->jpg(若無alpha通道)
- R Field 的內聯優化
- 通過內聯 R Field 來進一步對代碼進行瘦身,此外,它也解決了 R Field 過多導致 MultiDex 65536 的問題。
要想實現內聯 R Field,我們需要 通過 Javassist 或者 ASM 字節碼工具在構建流程中內聯 R Field;
- 可以使用或參考蘑菇街的ThinR gradle plugin
- 資源文件最少化配置
- 根據 App 目前所支持的語言版本去選用合適的語言資源,例如使用了 AppCompat,如果不做任何配置的話,
最終 APK 包中會包含 AppCompat 中所有已翻譯語言字符串,無論應用的其余部分是否翻譯為同一語言。
對此,我們可以 通過 resConfig 來配置使用哪些語言,從而讓構建工具移除指定語言之外的所有資源。
同理,也可以使用 resConfigs 去配置你應用需要的圖片資源文件類,如 "xhdpi"、"xxhdpi" 等等
android {
...
defaultConfig {
...
resConfigs "zh", "zh-rCN"
resConfigs "nodpi", "hdpi", "xhdpi", "xxhdpi", "xxxhdpi"
}
...
}
//還可以利用 Density Splits 來選擇應用應兼容的屏幕尺寸大小
android {
...
splits {
density {
enable true
exclude "ldpi", "tvdpi", "xxxhdpi"
compatibleScreens 'small', 'normal', 'large', 'xlarge'
}
}
...
}
- 資源在線化
- 將一些圖片資源放在服務器,然后 結合圖片預加載 的技術手段,既可以滿足產品的需要,同時可以減小包大小。
- 統一應用風格
- 設定統一的 字體、尺寸、顏色和按鈕按壓效果、分割線 shape、selector 背景 等
- 插件化
- 使用插件化的手段 對代碼結構進行調整,如果我們 App 當中的每一個功能都是一個插件,并且都是可以從服務器下發下來的,那 App 的包體積肯定會小很多。
參考文章
我是今陽,如果想要進階和了解更多的干貨,歡迎關注微信公眾號 “今陽說” 接收我的最新文章