貨拉拉Android 包體積優(yōu)化實踐

點擊上方藍字關注我,知識會給你力量

?作者簡介
?
muye,貨拉拉客戶端架構師,貨拉拉App Android端技術負責人,在Android App性能優(yōu)化、穩(wěn)定性提升等方向有豐富經驗
背景介紹
為什么要做包體積優(yōu)化?主要出于以下幾方面的考慮
下載轉化率
(1)很多應用市場流量保護限制是40M
(2)很多大型 App 一般都會有一個 Lite 版本的 App,也是出于下載轉化率方面的考慮對app性能的影響
(1)安裝時間:比如 文件拷貝、Library 解壓,并且,在編譯 ODEX 的時候,特別是對于 Android 5.0 和 6.0 系統(tǒng)來說,耗費的時間比較久,而 Android 7.0 之后有了 混合編譯,所以還可以接受。最后,App 變大后,其 簽名校驗 的時間也會變長。
(2)運行時內存:Resource 資源、Library 以及 Dex 類加載都會占用應用的一部分內存。
(3)ROM 空間:如果應用的安裝包大小為 50MB,那么啟動解壓之后很可能就已經超過 100MB 了CDN流量費用增加
安裝包體積越大,單個apk下載流量越大
優(yōu)化思路分析
apk主要由以下4個部分構成
(1)代碼相關的dex文件
(2)資源相關的resources.arsc和清單文件等
(3)so相關的lib目錄下的文件
(4)系統(tǒng)簽名文件
所以我們的優(yōu)化思路如下:
從apk整體優(yōu)化
(1)插件化
動態(tài)加載安裝包中的部分代碼、資源、so
(2)動態(tài)資源加載
動態(tài)加載安裝包中的資源、so代碼相關的dex文件優(yōu)化
(1)代碼混淆
使用更短的混淆字段來混淆原始的類名、方法名、變量名
(2)刪除未使用的代碼
完全沒使用的代碼、只使用了一小部分卻引入了整體功能的代碼
(3)刪除重復的代碼
完全重復的代碼、部分方法重復的代碼
(4)sdk優(yōu)化
重復功能的sdk(比如圖片加載庫)、只使用了部分功能卻引入了完整的sdk
(5)dex壓縮
使用更高壓縮率的算法、可以替換成常量的字節(jié)碼
(6)多dex關聯優(yōu)化
減少跨Dex調用的冗余信息
(7)字節(jié)碼優(yōu)化
方法內聯、常量內聯、優(yōu)化多余賦值指令、getter和setter方法內聯、刪除日志等資源相關的文件優(yōu)化
(1)shrinkResources
將項目中沒有使用的圖片、xml資源文件使用系統(tǒng)自帶的資源替換
(2)刪除未使用的資源
(3)刪除重復資源
(4)AndResGuard-資源混淆
使用更短的混淆名來混淆原始的資源名
(5)屬性代碼替換shape xml文件
(6)語言資源優(yōu)化
去掉多余的國際語言資源
(7)圖片資源優(yōu)化
圖片格式優(yōu)化、圖片壓縮、圖片分辨率優(yōu)化
(8)本地圖片轉網圖
編譯時將本地的圖片從apk中刪除,改成網絡動態(tài)加載的方式so相關的文件優(yōu)化
移除多余的so架構、移除調試符號
工欲善其事必先利其器,為了實現我們的優(yōu)化效果,我們引入和開發(fā)部分工具、插件來幫助我們做apk、代碼、資源的相關分析
貨拉拉Android 包體積優(yōu)化思維導圖

APK構成
APK構成

apk各個部分的詳細信息:

dex簡介
Dex 是 Android 系統(tǒng)的可執(zhí)行文件,包含 應用程序的全部操作指令以及運行時數據。因為 Dalvik 是一種針對嵌入式設備而特殊設計的 Java 虛擬機,所以 Dex 文件與標準的 Class 文件在結構設計上有著本質的區(qū)別。當 Java 程序被編譯成 class 文件之后,還需要使用 dx 工具將所有的 class 文件整合到一個 dex 文件中,這樣 dex 文件就將原來每個 class 文件中都有的共有信息合成了一體,這樣做的目的是 保證其中的每個類都能夠共享數據,這在一定程度上 降低了信息冗余,同時也使得 文件結構更加緊湊。與傳統(tǒng) jar 文件相比,Dex 文件的大小能夠縮減 50% 左右

apk打包過程
(1)簡化版

(2)詳細版

Apk分析工具
zip解壓
直接把apk后綴改成zip,然后解壓縮
解壓之后可以看到apk的各個組成部分

Android Studio自帶的Analyze APK
直接在Android Studio中點擊打開apk文件即可,打開之后可以看到各個部分的大小

還可以做apk的對比分析,可以看到新舊版本的體積對比,更直接客觀的看出新版本哪部分體積增加了及哪部分體積減少了

另外,還可以點開每個dex文件,查看里面的具體類信息

反編譯工具APKTool
APKTool主要包含三個部分:apktool、dex2jar、jd-gui,作用分別如下:
apktool
作用:資源文件獲取,可以提取出圖片文件和布局文件進行使用查看dex2jar 作用:將apk反編譯成java源碼(classes.dex轉化成jar文件)
jd-gui 作用:查看APK中classes.dex轉化成出的jar文件,即源碼文件
反編譯命令如下:
java -jar apktool_2.3.4.jar apktool d app-release.apk

反編譯后可以得到smali碼,通過jd-gui可以打開如下:

class-shark
(1)android-classshark 是一個 面向 Android 開發(fā)人員的獨立二進制檢查工具,它可以 瀏覽任何的 Android 可執(zhí)行文件,并且檢查出信息,比如類的接口、成員變量等等,此外,它還可以支持多種格式,比如說 APK、Jar、Class、So 以及所有的 Android 二進制文件如清單文件等等
(2)傳送門
https://github.com/google/android-classyshark
(3)使用方式
雙擊打開 ClassShark.jar,拖動我們的 APK 到它的工作空間即可。接下來,我們就可以看到 Apk 的分析界面了,這里我們點擊 classes 下的 classes.dex,在分析界面 左邊 可以看到該 dex 的方法數和文件大小,并且,最下面還顯示出了該 dex 中包含有 Native Call 的類

(4)點擊左上角的 Methods count 還可以切換到 方法數環(huán)形圖標統(tǒng)計界面,我們不僅可以 直觀地看到各個包下的方法數和相對大小,還可以看到各個子包下的方法數和相對大小

nimbledroid
(1)nibledroid 是美國哥倫比亞大學的博士創(chuàng)業(yè)團隊研發(fā)出來的分析 Android App 性能指標的系統(tǒng),分析的方式有靜態(tài)和動態(tài)兩種方式
(2)傳送門
https://nimbledroid.com/
(3)靜態(tài)分析:可以分析出APK安裝包中大文件排行榜,Dex 方法數和知名第三方 SDK 的方法數及占代碼整體的比例
(4)動態(tài)分析:可以給出 冷啟動時間, 列出 Block UI 的具體方法, 內存占用, 以及 Hot Methods, 從這些分析報告中, 可以 定位出具體的優(yōu)化點
ApkChecker
(1)簡介
ApkChecker是微信APM系統(tǒng)Matrix中的一個針對android安裝包的分析檢測工具
針對android安裝包的分析檢測工具,根據一系列設定好的規(guī)則檢測apk是否存在特定的問題,并輸出較為詳細的檢測結果報告,用于分析排查問題以及版本追蹤。
(2)傳送門
https://github.com/Tencent/matrix/wiki/Matrix-Android-ApkChecker
(3)怎么使用
Matrix-ApkChecker以一個jar包的形式提供使用,通過命令行執(zhí)行 java -jar ApkChecker.jar 即可運行
(4)能統(tǒng)計啥
fileSize 列出超過一定大小的文件,可按文件后綴過濾,并且按文件大小排序
--min 文件大小最小閾值,單位是KB
--order 按照文件大小升序(asc)或者降序(desc)排列
--suffix 按照文件后綴過濾,使用","作為多個文件后綴的分隔符
countMethod 統(tǒng)計方法數
group 輸出結果按照類名(class)或者包名(package)來分組
checkResProguard 檢查是否經過了資源混淆(AndResGuard)
findNonAlphaPng 發(fā)現不含alpha通道的png文件
min png文件大小最小閾值,單位是KB
checkMultiLibrary 檢查是否包含多個ABI版本的動態(tài)庫
uncompressedFile 發(fā)現未經壓縮的文件類型(即該類型的所有文件都未經壓縮)
suffix 按照文件后綴過濾,使用","作為多個文件后綴的分隔符
countR 統(tǒng)計apk中包含的R類以及R類中的field count
duplicatedFile 發(fā)現冗余的文件,按照文件大小降序排序
checkMultiSTL 檢查是否有多個動態(tài)庫靜態(tài)鏈接了STL
toolnm nm工具的路徑
unusedResources 發(fā)現apk中包含的無用資源
rTxt R.txt文件的路徑(如果在全局參數中給定了--input,則可以省略)
ignoreResources 需要忽略的資源,使用","作為多個資源名稱的分隔符
unusedAssets 發(fā)現apk中包含的無用assets文件
ignoreAssets 需要忽略的assets文件,使用","作為多個文件的分隔符
unstrippedSo 發(fā)現apk中未經裁剪的動態(tài)庫文件
(5)檢測結果示例

代碼分析工具
1、Proguard
proguard代碼混淆時會生成dump、mapping、seeds、usage4個文件,如下:

以seeds.txt為例,會列出當前混淆規(guī)則下沒有被混淆的類和成員,為后續(xù)進一步混淆優(yōu)化提供指導

2、lint分析插件
Android Studio自帶lint分析插件,以分析未使用聲明為例
Analyze -> Run Inspection by Name -> unused declaration

此外還可以分析項目中未使用的class和resources資源等
3、自定義lint分析插件
除了AS自帶的lint插件,還可以自定義lint插件,自定義lint插件可以掃描以下幾類文件
(1)JavaScanner / JavaPsiScanner / UastScanner:掃描 Java 源文件
(2)XmlScanner:掃描 XML 文件
(3)ClassScanner:掃描 class 文件
(4)BinaryResourceScanner:掃描二進制資源文件
(5)ResourceFolderScanner:掃描資源文件夾
(6)GradleScanner:掃描 Gradle 腳本
(7)OtherFileScanner:掃描其他類型文件
以掃描java源文件為例,以下掃描項目中所有的Log日志代碼:

4、自定義gradle插件
(1)自定義gradle插件的三種方式
Build script:在build.gradle構建腳本中直接使用,只能在本文件內使用;
buildSrc project:新建一個名為buildSrc的Module使用,只能在本項目中使用;
Standalone project:在獨立的Module中使用,可以發(fā)布到本地或者遠程倉庫供其他項目使用。
(2)支持的語言
可以使用多種語言來實現Gradle插件,其實只要最終被編譯為JVM字節(jié)碼的都可以,常用的有Groovy、Java、Kotlin
(3)Build script示例

(4)buildSrc project和Standalone project使用
本質上實現方式一樣
首先自定義插件類implements Plugin
實現「void」 apply(Project project)方法注冊自定義的Transform類;
然后自定義類extends Transform,在transform()方法中實現自定義邏輯,例如可以自定義
ClassVisitor類訪問和修改類的相關屬性,自定義MethodVisitor類訪問和修改方法的相關屬性
5、coverage插件
(1)簡介
coverage插件是由字節(jié)跳動開源的線上無用代碼分析工具
(2)原理
由于代碼設計不合理以及keep規(guī)則限制等原因,靜態(tài)代碼檢查無法找出所有的無用代碼。
我們可以從用戶的角度去分析,對每個類插樁,執(zhí)行時將信息上報到服務器?;诖罅坑脩羯蠄螅脩魶]有用到的類可以被定義為無用類。
在抖音項目中,我們發(fā)現了1/6的無用類,不包含其引用的資源,共計3M(dex大小20M),如果能全部刪除,將減少5%包大小
(3)傳送門
https://github.com/bytedance/ByteX/blob/master/coverage/README-zh.md
6、pmd檢測重復代碼
(1)簡介
PMD是一個靜態(tài)源代碼分析器。它找到常見的編程缺陷,如未使用的變量,空的catch塊,不必要的對象創(chuàng)建等等。它主要關注Java和Apex,但支持其他六種語言。
PMD具有許多內置檢查(在PMD術語,規(guī)則中),這些檢查在規(guī)則參考中針對每種語言進行了記錄。我們還支持廣泛的API來編寫您自己的規(guī)則,您可以使用Java或作為自包含的XPath查詢來執(zhí)行。
在集成到構建過程中時,PMD最有用。
(2)支持的4種運行方式
作為Maven的目標
作為Ant任務
作為Gradle任務
從命令行
(3)傳送門
https://pmd.sourceforge.io/pmd-5.4.1/usage/cpd-usage.html
(4)檢測結果示例
使用命令行方式:
./run.sh cpd --language java --minimum-tokens 100 --files /Users/xxxx/Work/code/DeliciousFood/Classes > ~/Desktop/codeCheck.txt

7、Simian檢測重復代碼
(1)Simian是一個可跨平臺使用的重復代碼檢測工具,能夠檢測代碼片段中除了空格、注釋及換行外的內容是否完全一致,且支持的語言包括:
Java C# C++ C Objective-C JavaScript (ECMAScript) COBOL, ABAP Ruby Lisp SQL Visual Basic Groovy Swift
(2)傳送門
http://www.harukizaemon.com/simian/get_it_now.html
(3)simian檢測結果示例

代碼體積優(yōu)化
1、代碼優(yōu)化小建議
(1)時刻保持良好的編程習慣,去除重復或者不用的代碼,慎用第三方庫,選用體積小的第三方SDK
(2)盡量不要使用自動生成的代碼的sdk 比如butterknife和viewbinding、databinding
(3)減少ENUM的使用,避免使用枚舉 單個枚舉會使應用的 classes.dex 文件增加大約 1.0 到 1.4KB 的大小 請考慮使用 @IntDef 注釋
2、代碼混淆Proguard
(1)作用
混淆器的 作用 不僅僅是 保護代碼,它也有 精簡編譯后程序大小 的作用,其 通過縮短變量和函數名以及丟失部分無用信息等方式,能使得應用包體積減小。
i 瘦身:它可以檢測并移除未使用到的類、方法、字段以及指令、冗余代碼,并能夠對字節(jié)碼進行深度優(yōu)化。最后,它還會將類中的字段、方法、類的名稱改成簡短無意義的名字。
ii 安全:增加代碼被反編譯的難度,一定程度上保證代碼的安全。
(2)代碼混淆形式
代碼混淆的形式主要有 三種,如下所示:
i:將代碼中的各個元素,比如類、函數、變量的名字改變成無意義的名字。例如將 hasValue 轉換成單個的字母 a。這樣,反編譯閱讀的人就無法通過名字來猜測用途。
ii:重寫 代碼中的 部分邏輯,將它變成 功能上等價,但是又 難以理解 的形式。比如它會 改變循環(huán)的指令、結構體。
iii:打亂代碼的格式,比如多加一些空格或刪除空格,或者將一行代碼寫成多行,將多行代碼改成一行。
(3)Proguard踩坑經驗
i:如果項目首次混淆,可能需要全局掃描所有的類和包名,可以先全量keep,然后再逐包放開混淆
ii:EventBus的java、kotlin的onEvent的坑
iii:沒有序列化的內部屬性類也需要keep
3、sdk優(yōu)化
(1)sdk接入標準
i:不要為了某個小功能就隨意引入sdk,可以考慮源碼接入
ii:郵件通知審核sdk是否接入
(2)選擇第三方 SDK 的時候,我們可以將包大小作為選擇的指標之一,我們應該 盡可能地選擇那些比較小的庫來實現相同的功能
(3)不要選擇重復功能的sdk,如果有,可以考慮去掉其他的 Picasso、Glide、Fresco
(4)某些庫支持部分功能分離,不需要引入整個包 比如 Fresco,它將圖片加載的各個功能,如 webp、gif 功能進行了剝離,它們都處于單個的庫當中
4、刪除重復的代碼
可以使用上面的pmd和simian工具掃描出重復的代碼
5、刪除未使用的代碼
可以使用上面的coverage插件來輔助統(tǒng)計出未使用的代碼
6、dex壓縮
(1)內聯R Field
通過內聯 R Field 來進一步對代碼進行瘦身,此外,它也解決了 R Field 過多導致 MultiDex 65536 的問題。要想實現內聯 R Field,我們需要 通過 Javassist 或者 ASM 字節(jié)碼工具在構建流程中內聯 R Field
實現原理:
android 中的 R 文件,除了 styleable 類型外,所有字段都是 int 型變量/常量,且在運行期間都不會改變。所以可以在編譯時,記錄 R 中所有字段名稱及對應值,然后利用 ASM 工具遍歷所有 Class,將除 R$styleable.class 以外的所有 R.class 刪除掉,并且在引用的地方替換成對應的常量
使用工具:ThinRPlugin(美麗說團隊開源)
插件部分實現:
(2)dex壓縮--XZ Utils
i:XZ Utils 是具有高壓縮率的免費通用數據壓縮軟件,它同 7-Zip 一樣,都是 LZMA Utils 的后繼產品,內部使用了 LZMA/LZMA2 算法。LZMA 提供了高壓縮比和快速解壓縮,因此非常適合嵌入式應用
ii:缺點 壓縮 Dex 的方式,那么首次生成 ODEX 的時間可能就會超過1分鐘
iii:傳送門
https://tukaani.org/xz/
7、多dex關聯優(yōu)化-ReDex
(1)背景
Dex 的方法數就會超過65536個,因此,必須采用 mutildex 進行分包,但是此時每一個 Dex 可能會調用到其它 Dex 中的方法,這種 跨 Dex 調用的方式會造成許多冗余信息 (1)多余的 method id:跨 Dex 調用會導致當前dex保留被調用dex中的方法id,這種冗余會導致每一個dex中可以存放的class變少,最終又會導致編譯出來的dex數量增多,而dex數據的增加又會進一步加重這個問題。(2)其它跨dex調用造成的信息冗余:除了需要多記錄被調用的method id之外,還需多記錄其所屬類和當前方法的定義信息,這會造成 string_ids、type_ids、proto_ids 這幾部分信息的冗余。
(2) ReDex方案
為了減少跨 Dex 調用的情況,我們必須 盡量將有調用關系的類和方法分配到同一個 Dex 中。但是各個類相互之間的調用關系是非常復雜的,所以很難做到最優(yōu)的情況。所幸的是,ReDex 的 CrossDexDefMinimizer 類分析了類之間的調用關系,并 使用了貪心算法去計算局部的最優(yōu)解(編譯效果和dex優(yōu)化效果之間的某一個平衡點)。使用 "InterDexPass" 配置項可以把互相引用的類盡量放在同個 Dex,增加類的 pre-verify,以此提升應用的冷啟動速度
(3)ReDex的5個功能
Interdex:類重排和文件重排、Dex 分包優(yōu)化。其中對于類重排和文件重排,Google 在 Android 8.0 的時候引入了 Dexlayout,它是一個用于分析 dex 文件,并根據配置文件對其進行重新排序的庫。與 ReDex 類似,Dexlayout 通過將經常一起訪問的部分 dex 文件集中在一起,程序可以因改進文件位置從而擁有更好的內存訪問模式,以節(jié)省 RAM 并縮短啟動時間。不同于ReDex的是它使用了運行時配置信息對 Dex 文件的各個部分進行重新排序。因此,只有在應用運行之后,并在系統(tǒng)空閑維護的時候才會將 dexlayout 集成到 dex2oat 的設備進行編譯
Oatmeal:直接生成 Odex 文件
StripDebugInfo:去除 Dex 中的 Debug 信息
源碼中 access-marking 模塊:刪除 Java access 方法
源碼中 type-erasure 模塊:類型擦除。
(4)傳送門
https://fbredex.com/docs/installation
資源體積優(yōu)化
1、圖片格式優(yōu)化
(1)如果能用VectorDrawable來表示的話優(yōu)先使用VectorDrawable,如果支持WebP則優(yōu)先用WebP,而PNG主要用在展示透明或者簡單的圖片,而其它場景可以使用JPG格式。針對每種圖片格式也有各類的優(yōu)化手段和優(yōu)化工具。
(2)使用矢量圖片
可以使用矢量圖形來創(chuàng)建獨立于分辨率的圖標和其他可伸縮圖片。使用矢量圖片能夠有效的減少App中圖片所占用的大小,矢量圖形在Android中表示為VectorDrawable對象。使用VectorDrawable對象,100字節(jié)的文件可以生成屏幕大小的清晰圖像,但系統(tǒng)渲染每個VectorDrawable對象需要大量的時間,較大的圖像需要更長的時間才能出現在屏幕上。因此只有在顯示小圖像時才考慮使用矢量圖形。有關使用VectorDrawable的更多信息,請參閱 Working with Drawables。
(3)使用WebP
如果App的minSdkVersion高于14(Android 4.0+)的話,可以選用WebP格式,因為WebP在同畫質下體積更小(WebP支持透明度,壓縮比比JPEG更高但顯示效果卻不輸于JPEG,官方評測quality參數等于75均衡最佳), 可以通過PNG到WebP轉換工具來進行轉換。

2、圖片壓縮
(1)png格式圖片可以在tinyPng網站上壓縮,或者使用pngcrush、pngquant或zopflipng等工具壓縮
而不會丟失圖像質量。所有這些工具都可以減少PNG文件大小,同時保持圖像質量。
pngcrush工具特別有效:此工具在PNG過濾器和zlib(Deflate)參數上迭代,使用過濾器和參數的每個組合來壓縮圖像。然后選擇產生最小壓縮輸出的配置
(2)JPEG文件,可以使用packJPG或guetzli等工具將JPEG文件壓縮的更小,這些工具能夠在保持圖片質量不變的情況下,把圖片文件壓縮的更小。guetzli工具更是能夠在圖片質量不變的情況下,將文件大小降低35%
3、開啟資源壓縮shrinkResources
(1)Android的編譯工具鏈中提供了一款資源壓縮的工具,可以通過該工具來壓縮資源,如果要啟用資源壓縮,可以在build.gradle文件中將shrinkResources true
(2)需要注意的是,Android構建工具是通過ResourceUsageAnalyzer來檢查哪些資源是無用的,
當檢查到無用的資源時會把該資源替換成預定義的版本。主要是針對 .png、.9.png、.xml 提供了 TINY_PNG、TINY_9PNG、TINY_XML 這 3 個 byte 數組的預定義版本。資源壓縮工具默認是采用 安全壓縮模式 來運行,可以通過開啟 嚴格壓縮模式 來達到 更好的瘦身效果

4、語言資源優(yōu)化
語言資源優(yōu)化 讓構建工具移除指定語言之外的所有資源(可以刪除sdk里面的語言資源) resConfigs "zh", "zh-rCN"
5、圖片分辨率優(yōu)化
根據項目實際需要,大部分圖片可以只保留一套xxhdpi圖片
6、屬性代碼替代shape xml
(1)背景
項目中為了滿足ui需求,使用了大量android shape來生成各式各樣的背景。這些背景大多數只有圓角,描邊,填充色等信息不一樣,但是種類繁多,無法兼容,需要我們使用大量的xml文件來生產多種多樣的背景,目前項目中多達數百個
(2)解決思路
i:自定義HllRoundBackground類來構建一個android 原生GradientDrawable來表示背景shape
ii:自定義屬性來表示常用的android shape屬性,包含圓角,填充色,描邊,根據狀態(tài)改變填充顏色,描邊顏色,字體顏色,以及漸變色屬性
iii:繼承android原生LayoutInflater.Factory2類,用它來生成View,并檢查該View上是否有自定義屬性,如果有嘗試生成背景,并設置到該View上。把該Factory注入到系統(tǒng)中,必要的時候,由我們代替系統(tǒng)的LayoutInflater創(chuàng)建View
(3)代碼設計方案

(4)使用示例
<TextView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="20元"
app:hll_corners_radius = "6dp"
app:hll_stroke_width = "1dp"
app:hll_solid_normal_color = "@color/white"
app:hll_solid_selected_color = "@color/color_0dff6600"
app:hll_stroke_normal_color = "@color/gray_15_percent"
app:hll_stroke_selected_color = "@color/color_ff6600"/>
7、資源混淆-AndResGuard
(1) AndResGuard方案
直接處理apk. 不依賴源碼,不依賴編譯過程,僅僅輸入一個安裝包,得到一個混淆包

(2)AndResGuard處理流程
i:resources.arsc:它記錄了資源文件的名稱與路徑,使用混淆后的短路徑 res/s/a,可以減少文件的大小。
ii:metadata 簽名文件:簽名文件 MANIFEST.MF 與 CERT.SF 需要記錄所有文件的路徑以及它們的哈希值,使用短路徑可以減少這兩個文件的大小。
iii:ZIP 文件:ZIP 文件格式里面通過其索引記錄了每個文件 Entry 的路徑、壓縮算法、CRC、文件大小等等信息。短路徑的優(yōu)化減少了記錄文件路徑的字符串大小
AndResGuard工作流程圖

(3)傳送門
https://github.com/shwenzhang/AndResGuard
(4)與7z極限壓縮

(5)AndResGuard混淆后的資源名

(6)AndResGuard踩坑經驗
i:資源混淆之白名單
代碼掃描調用getIdentifier()方法的地方
ii:開啟7zip壓縮之后會影響圖片加載速度,會對app啟動速度有點影響
8、刪除重復資源
可以使用上面的ApkChecker工具掃描出apk中重復的資源
9、刪除未使用資源
(1)可以使用上面的ApkChecker工具掃描出apk中沒有使用的資源
(2)也可以使用Android Studio自帶的lint插件掃描項目中沒有使用的資源
Analyze -> Run Inspection by Name -> unused Resources
其他包體積優(yōu)化方案
1、資源動態(tài)加載方案
(1)原理
把一些使用頻率相對低一些的資源不打包進apk,需要的時候在下載到本地進行使用(這些資源可能包括動畫文件、字體文件、so庫、zip壓縮包等)
(2)資源動態(tài)加載架構圖

(3)部分類UML設計

(4)資源動態(tài)配置示例

(5)動態(tài)so加載
i:正常so加載流程
安裝app的時候,PMS會把指定架構的so庫,拷貝到 data/data/[包名]/lib 下面
啟動app的時候,會把系統(tǒng)的so文件夾,以及 安裝包的so文件夾位置 給 BaseDexClassLoader 中的屬性DexPathList 下面屬性的 nativeLibraryDirectories 和 systemNativeLibraryDirectories 兩個File集合
調用及使用 調用:System.loadLibrary("xxx")
ii:動態(tài)加載so方案
System.loadLibrary()和System.load()最后都會調用DexPathList 的 findLibrary(), 通過 DexPathList 中的 nativeLibraryDirectories 和systemNativeLibraryDirectories兩個文件夾集合,生成一個NativeLibraryElement[],然后從這里面找對應的so,返回全路徑
hook了DexPathList 中的 nativeLibraryDirectories,在這個文件夾集合中又添加一個自定義的文件夾
流程圖如下:

2、本地圖片轉網圖
(1)原理
編譯時 (1)上傳圖片 (2)刪除圖片源文件 (3)保存鏈接信息
運行時 (1)解析鏈接信息 (2)Hook Android Drawable圖片加載流程 (3)自定義Drawable,觸發(fā)網絡圖片下載,還原系統(tǒng)的Drawable圖片繪制流程
(2)aapt流程圖

(3)本地圖片轉網圖流程圖

(4)編譯時刪除本地圖片

(5)運行時加載圖片

3、字節(jié)碼優(yōu)化Bytex
(1)Bytex簡介
ByteX字節(jié)跳動團隊開發(fā)的一個基于gradle transform api和ASM的字節(jié)碼插件平臺(或許,你可以把它當成一個有無限個插頭的插座?)
目前集成了若干個字節(jié)碼插件,每個插件完全獨立,既可以脫離ByteX這個宿主而獨立存在,又可以自動集成到宿主和其它插件一起整合為一個單獨的Transform。插件和插件之間,宿主和插件之間的代碼是完全解耦的(有點像組件化),這使得ByteX在代碼上擁有很好的可拓展性,新插件的開發(fā)將會變得更加簡單高效

(2)傳送門
ByteX/README_zh.md at master · bytedance/ByteX
(3)字節(jié)碼優(yōu)化功能
i:優(yōu)化多余賦值指令 (field-assign-opt-plugin)
編譯期間去除代碼中不必要或者重復的賦值(默認值)代碼,在虛擬機實例化時分配的內存中默認會給予默認值,所以代碼中的默認值是多余的,如下:
private boolean aBoolean = false;
private byte aByte = 0;
private short aShort = 0;
private char aChar = '\u0000';
private int anInt = 0;
private float aFloat = 0f;
private double aDouble = 0d;
private long aLong = 0l;
ii:刪除某些方法調用 (method-call-opt-plugin)
比如我們的調試日志Log.d()只是在開發(fā)調試階段使用,發(fā)布包完全不需要此類代碼
iii:常量內聯(const-inline-plugin)
編譯期間內聯并優(yōu)化掉項目中的編譯期間常量字段,插件將對編譯期常量的運算(對應GETFIELD指令)進行內聯操作(對應LDC指令),然后將對應的字段進行刪除優(yōu)化。插件會對可能的反射的代碼進行分析,對于直接使用反射方式獲取運行時常量字段進行忽略優(yōu)化處理。
4、so包瘦身
(1)移除多余的so架構
defaultConfig {
ndk {
abiFilters "armeabi"
}
}
i:一般應用都不需要用到 neon 指令集,我們只需留下 armeabi 目錄就可以了。因為 armeabi 目錄下的 So 可以兼容別的平臺上的 So
ii:缺點:別的平臺使用時性能上就會有所損耗,失去了對特定平臺的優(yōu)化
(2)移除調試符號
使用 Android NDK 中提供的 arm-eabi-strip 工具從原生庫中移除不必要的調試符號
5、Buck-刪除 Native Library 中無用的導出 symbol
(1)Buck作用
分析代碼中的 JNI 方法以及不同 Library 庫的方法調用,然后找出無用的 symbol 并刪除,這樣 Linker 在編譯的時候也會把 symbol 對應的無用代碼給刪除。在 Buck 有 NativeRelinker 這個類,它就實現了這個功能,其 類似于 Native Library 的 ProGuard Shrinking 功能
(2)使用
刪除 Native Library 中無用的導出 symbol 使用facebook的Buck庫,Buck有 NativeRelinker 這個類,可以刪除 Native Library 中無用的導出 symbol,其 類似于 Native Library 的 ProGuard Shrinking 功能。
(3)傳送門
https://github.com/facebook/buck
插件化
1、DL 動態(tài)加載框架 ( 2014 年底)
基于代理的方式實現插件框架,當啟動插件組件時,首先啟動一個代理組件,然后通過這個代理組件來構建,啟動插件組件
支持的功能
(1)plugin無需安裝即可由宿主調起。
(2)支持用R訪問plugin資源
(3)plugin支持Activity和FragmentActivity(未來還將支持其他組件)
(4)基本無反射調用
(5)插件安裝后仍可獨立運行從而便于調試
(6)支持3種plugin對host的調用模式:
無調用(但仍然可以用反射調用)。
部分調用,host可公開部分接口供plugin調用。這前兩種模式適用于plugin開發(fā)者無法獲得host代碼的情況。
完全調用,plugin可以完全調用host內容。這種模式適用于plugin開發(fā)者能獲得host代碼的情況。
(7)只需引入DL的一個jar包即可高效開發(fā)插件,DL的工作過程對開發(fā)者完全透明
傳送門:
https://github.com/singwhatiwanna/dynamic-load-apk
2、DroidPlugin ( 2015 年 8 月)
360 手機助手實現的一種插件化框架,它可以直接運行第三方的獨立 APK 文件,完全不需要對 APK 進行修改或安裝。一種新的插件機制,一種免安裝的運行機制,是一個沙箱
功能:
(1)插件APK完全不需做任何修改,可以獨立安裝運行、也可以做插件運行。要以插件模式運行某個APK,你「無需」重新編譯、無需知道其源碼。
(2)插件的四大組件完全不需要在Host程序中注冊,支持Service、Activity、BroadcastReceiver、ContentProvider四大組件
(3)插件之間、Host程序與插件之間會互相認為對方已經"安裝"在系統(tǒng)上了。
(4)API低侵入性:極少的API。HOST程序只是需要一行代碼即可集成Droid Plugin
(5)超強隔離:插件之間、插件與Host之間完全的代碼級別的隔離:不能互相調用對方的代碼。通訊只能使用Android系統(tǒng)級別的通訊方法。
(6)支持所有系統(tǒng)API
(7)資源完全隔離:插件之間、與Host之間實現了資源完全隔離,不會出現資源竄用的情況。
(8)實現了進程管理,插件的空進程會被及時回收,占用內存低。
(9)插件的靜態(tài)廣播會被當作動態(tài)處理,如果插件沒有運行(即沒有插件進程運行),其靜態(tài)廣播也永遠不會被觸發(fā)
缺點:
(1)無法在插件中發(fā)送具有自定義資源的Notification,例如:a. 帶自定義RemoteLayout的Notification b. 圖標通過R.drawable.XXX指定的通知(插件系統(tǒng)會自動將其轉化為Bitmap)
(2)無法在插件中注冊一些具有特殊Intent Filter的Service、Activity、BroadcastReceiver、ContentProvider等組件以供Android系統(tǒng)、已經安裝的其他APP調用。
(3)缺乏對Native層的Hook,對某些帶native代碼的apk支持不好,可能無法運行。比如一部分游戲無法當作插件運行。
傳送門:
https://github.com/DroidPluginTeam/DroidPlugin
3、Small ( 2015 年底)
實現原理:(1)動態(tài)加載類(2)資源分段(3)動態(tài)代理注冊
傳送門:
https://github.com/wequick/Small/wiki/Android
4、VirtualAPK (2017年 6 月)
VirtualAPK 是滴滴開源的一套插件化框架,支持幾乎所有的 Android 特性,四大組件方面
VirtualAPK架構圖

傳送門:
https://github.com/didi/VirtualAPK/blob/master/README.md
5、RePlugin (2017 年 7 月) RePlugin是一套完整的、穩(wěn)定的、適合全面使用的,占坑類插件化方案,由360手機衛(wèi)士的RePlugin Team研發(fā),也是業(yè)內首個提出”全面插件化“(全面特性、全面兼容、全面使用)的方案
RePlugin架構圖

優(yōu)點:
「極其靈活」:主程序無需升級(無需在Manifest中預埋組件),即可支持新增的四大組件,甚至全新的插件 「非常穩(wěn)定」:Hook點「僅有一處(ClassLoader),無任何Binder Hook」!如此可做到其「崩潰率僅為“萬分之一”,并完美兼容市面上近乎所有的Android ROM」 「特性豐富」:支持近乎所有在“單品”開發(fā)時的特性。「包括靜態(tài)Receiver、Task-Affinity坑位、自定義Theme、進程坑位、AppCompat、DataBinding等」 「易于集成」:無論插件還是主程序,「只需“數行”就能完成接入」 「管理成熟」:擁有成熟穩(wěn)定的“插件管理方案”,支持插件安裝、升級、卸載、版本管理,甚至包括進程通訊、協議版本、安全校驗等 「數億支撐」:有360手機衛(wèi)士龐大的「數億」用戶做支撐,「三年多的殘酷驗證」,確保App用到的方案是最穩(wěn)定、最適合使用的
傳送門:
https://github.com/Qihoo360/RePlugin/blob/dev/README_CN.md
6、Shadow
騰訊自主研發(fā)的Android插件框架,經過線上億級用戶量檢驗,號稱“零hook”
Shadow主要具有以下特點:
(1)復用獨立安裝App的源碼:插件App的源碼原本就是可以正常安裝運行的。
(2)零反射無Hack實現插件技術:從理論上就已經確定無需對任何系統(tǒng)做兼容開發(fā),更無任何隱藏API調用和Google限制非公開SDK接口訪問的策略完全不沖突。
(3)全動態(tài)插件框架:一次性實現完美的插件框架很難,但Shadow將這些實現全部動態(tài)化起來,使插件框架的代碼成為了插件的一部分。插件的迭代不再受宿主打包了舊版本插件框架所限制。
(4)宿主增量極小:得益于全動態(tài)實現,真正合入宿主程序的代碼量極小(15KB,160方法數左右)。(5)Kotlin實現:core.loader,core.transform核心代碼完全用Kotlin實現,代碼簡潔易維護
傳送門
https://github.com/Tencent/Shadow
總結
以上是我們目前在Apk包體積優(yōu)化方面做的一些嘗試和積累,可以根據自身情況取舍使用
通過上述優(yōu)化措施,貨拉拉32位包體積從82.69M減少到了33.86M,減少了60%


由于自身業(yè)務特點,我們暫時沒有使用插件化框架;
最后,保持好的開發(fā)習慣,砍掉不必要的功能才是保證包體積持續(xù)優(yōu)化的超級大招
向大家推薦下我的網站 https://xuyisheng.top/ 點擊原文一鍵直達
專注 Android-Kotlin-Flutter 歡迎大家訪問
往期推薦
更文不易,點個“三連”支持一下??
