Android App安裝包體積優化: 理由, 指標和可以採用的方法. ...
Android App安裝包瘦身計劃
Android App安裝包體積優化: 理由, 指標和可以採用的方法.
本文內容歸納如下圖:
為什麼要安裝包瘦身
- 安裝包需要瘦身嗎?
- 不需要嗎?
安裝包要瘦身的主要原因就是考慮應用的下載轉化率和留存率.
應用太大了, 用戶可能就不下載了, 尤其是移動網路或者流量收費的情況下.
再者, 因為手機空間問題, 用戶有時候可能需要選擇卸載一些應用, 就會先盯上那些占空間大的, 所以應用大小也會也影響留存率.
但現在什麼時代了, 到處都是WiFi, 流量包又大又便宜, 網速快到飛起, 手機硬體也在不斷升級, 更大更快更高級, 安裝包真的還需要瘦身嗎? 有人在乎嗎?
那當然還是有一定必要的, 不然這篇文章寫到這裡已經結束了.
因為就算用戶不在乎你這個應用有多少大, 決定要下載安裝了, 安裝包越大, 下載時間就會越長, 沒等下載完成就因為各種原因取消或者斷網了.
或者因為下載時間稍微有點長, 等完成以後, 用戶已經忘了要用這個app了.
現代人的耐心很有限, 不然也不會有小程式這樣輕量級的解決方案.
安裝包瘦身可以地定量, 在一定程度上改善問題.
除了下載下載轉化率和留存率, 安裝包體積優化還有一些理由:
- 預裝應用的推廣成本.
- 滿足應用市場的最大包體積限制.
- 在技術甚至業務層面, 我們可以重新審視我們的codebase, 是否需要刪除一些低價值的業務, 清理無用的代碼和資源, 進行進一步的重構和改善.
安裝包的構成
Apk安裝包本質上是一個zip壓縮文件, 解壓之後我們可以看到其中包含的內容, 通常包含:
- 一個或多個
classes.dex
文件: 代碼編譯結果. assets/
文件夾.resources.arsc
: 編譯後的二進位資源文件, 包含了所有res/values
中的XML內容, 如字元串, style等, 也包括對沒編譯進來的資源(比如layout和圖片)的路徑.res/
: 沒有被編譯進resources.arsc
文件的資源: layout, drawable等.AndroidManifest.xml
: merge後的manifest文件.META-INF/
: 包含簽名文件等.
如果有native的庫, 可能還會有lib/
文件夾.
尺寸指標
安裝包大小有幾個指標呢?
有三個:
- 文件尺寸: 這可能是最直觀的一個, 就是我們build出來的文件大小. 但是其實它是最不重要的一個指標.
- 下載尺寸: 下載app需要的大小, 最重要的指標. 因為下載市場(Google Play)會幫我們做優化, 下載尺寸會比文件的尺寸小.
一般增量安裝和全新安裝需要的大小是不一樣的. - 安裝尺寸: app安裝到手機上, 啟動解壓之後的大小, 關係到app的留存率.
安裝包分析工具
在做安裝包體積優化之前, 建議用工具看一下當前的安裝包情形, 找到當前的bottle neck, 這樣才能有的放矢.
優化之後也好做一個比較.
APK Analyzer
APK Analyzer是Android Studio自帶的工具.
使用APK Analyzer的三種方法:
Build > Analyze APK..
, 選擇.apk
文件.- 切換到Project View, 雙擊build/outputs/apk目錄下的apk文件.
- 把apk文件拖到Android Studio的編輯視窗中.
利用這個工具可以查看apk的各個組成部分以及它們各自的大小, 包括文件大小和預估的下載大小.
這個工具還可以比較兩個APK, 從而可以看出具體是哪個部分有尺寸的變化.
官方插件: Android Size Analyzer
在Android Studio中可以添加Plugin: Android Size Analyzer
.
安裝之後重啟Android Studio, 接著就可以點擊菜單Analyze > Analyze App Size
.
點擊之後會給出建議, 顯示出項目中比較大的文件, 建議轉化一些圖片到webp格式, 建議開啟代碼壓縮混淆等.
安裝包瘦身措施: Coding階段
減少代碼(native和Java代碼)
- 控制第三方庫的使用: 依賴第三方庫的時候, 儘量減小範圍, 或者使用更加友好的替代. (ProGuard會移除不用的代碼, 但是它不會移除庫的內部依賴.)
- 減少不必要的生成代碼.
- 減少枚舉的使用.
- 減小native庫: 移除debug symbols和關閉so庫壓縮.
控制資源使用
要控制資源的使用: 減少資源的數量, 減小資源的大小.
設備屏幕支持
Android有很多屏幕密度類型:
ldpi, mdpi, tvdpi, hdpi, xhdpi, xxhdpi and xxxhdpi
並不是需要每個資源都提供多密度版本的.
當一個屏幕密度下沒有提供特定的資源, Android會自動根據這個資源其他密度的版本進行縮放.
如果你的app僅需要提供縮放圖像, 你可以只在drawable-nodpi
中提供單個資源. (建議還是至少有一個xxhdpi
.)
shape的使用
很多時候我們並不需要一個圖片, 比如純色或漸變色的背景, 帶邊框, 帶圓角等.
用<shape>
尺寸就會比較小, 也不用多個密度的版本.
復用資源
這裡的復用可以是改變了顏色和旋轉方向等的復用.
比如改資源的tint, 或者把一個圖旋轉了之後再用. (thumb up變成thumb down了).
壓縮圖片
可以用一些工具對PNG和JPEG圖片進行無損壓縮: 圖片質量無損但是尺寸變小.
使用WebP格式的圖片
可以使用WebP格式的圖片, 比JPEG和PNG壓縮得更好.
Android Studio提供了轉換工具: Create WebP images
註意: Google Play只接受PNG作為launcher icon.
使用矢量圖
可以使用矢量圖來作為可伸縮的資源, 在Android中是VectorDrawable對象.
但是矢量圖的渲染需要系統的時間花銷, 所以推薦只有小的icon使用矢量圖.
逐幀動畫
不要再使用很多個圖片逐幀播放來實現一個動畫效果了.
儘量用改變屬性的動畫來節省資源使用.
另外還有一些手段比如在程式中繪製, 而不是使用圖片.
安裝包瘦身措施: Post-coding階段
Lint靜態分析
Lint是靜態分析工具, 會提示項目中的各種問題.
針對不用的資源, 可以專門這樣檢查:
在Android Studio中: Analyze -> Run Inspection by Name... -> Unused Resources
.
掃描後會對所有未使用的資源給出警告.
註意, 因為是Lint靜態檢查, 所以會有一些錯報和漏報的情況:
assets/
目錄不被掃描.- 檢測不到第三方庫中帶來的不用的資源.
- 錯報: 資源在項目中實際上用了, 但是被檢測出來了. 如果資源是被動態引用的, 比如用
getIdntifer()
方法, 拼名字使用, 會被lint錯誤地報告說沒有用到. - 漏報: 實際上沒有用到, 但是沒有檢測出來. 這是因為還有另一個沒有用到的代碼引用了這個資源.
比如有一個沒有人用的Fragment, 它的佈局文件就不會被檢測出來, 因為寫在了代碼里. (註意: 如果是不用的佈局中include了另一個佈局, 這兩個佈局都能被檢測出來.)
靜態檢查只是幫我們檢測並報告, 真正要移除這些資源還得靠我們手動刪除.
代碼壓縮和混淆
ProGuard -> R8會幫我們刪除不用的代碼, 進行名稱改寫和代碼優化.
minifyEnabled true
不用的代碼被移除了, 類和成員的名稱都被改得很短, 從而有效地減小了dex文件的大小.
這裡需要註意rules的編寫, 不要過度保護.
比較追求極致的方法會從這個方面下手, 進一步細化優化ProGuard Rules, 以達到更深度的混淆和壓縮.
shrinkResources
移除不用資源的一個有利工具shrinkResources
:
android {
// Other settings
buildTypes {
release {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
為了開啟壓縮資源, 必須先開啟壓縮代碼, 即minifyEnabled
要為true
.
這樣, 在打包的時候, 首先移除了不用的代碼, 然後Gradle才能檢測哪些資源還在被代碼引用, 從而移除不用的資源.
註意這個過程並沒有真的刪除掉資源, 只是將不用的資源替換了一個簡單版本.
跟代碼壓縮一樣, 資源壓縮也可以指定一個自定義的規則, 明確指明哪些資源要保留.
註意開啟了代碼壓縮和混淆之後, 編譯速度會變慢, 所以一般debug版本不開啟.
這種刪除方式靠譜嗎? 精確嗎? 會誤刪嗎?
常規情況下resource shrinker還挺準確的.
但是如果你的代碼(或依賴的庫)中有用到Resources.getIdentifier()
方法來獲取資源, (比如AppCompat中這麼做), 那麼說明你的代碼會根據一個動態生成的字元串來查找資源.
如:
val name = String.format("img_%1d", angle + 1)
val res = resources.getIdentifier(name, "drawable", packageName)
這個時候resource shrinker預設會表現得很保守, 會保留所有符合這個名字模式的資源, 認為它們都是可能會被用到的, 從而不會刪除它們.
而且resource shrinker還會掃描字元串, 查找符合資源格式的URLs, 比如file:///android_res/drawable//ic_plus_anim_016.png
, 從而保留這些對應的資源.
所以不用擔心, 預設情況下, 資源壓縮都是"better safe than sorry"的模式.
如果你想激進一點, 可以在keep.xml
裡面把壓縮模式指定為shrinkMode="strict"
, 這時候你就必須手動指定需要keep的動態使用的資源了.
如何查看都刪了什麼呢?
用命令行:
./gradlew clean assembleRelease --info | grep "Skipped unused resource"
註意這裡assembleRelease
可以替換成任何一個開啟了資源壓縮的type.
執行了之後會列出所有被替換成dummy file的資源.
用IDE:
在Android Studio的Build視窗下, Toggle View切換到詳細信息模式下. Build, 然後查找信息, 可以看見信息中會有一句:
Removed unused resources: Binary resource data reduced from XXXKB to YYYKB: Removed Z%
顯示資源壓縮節省出來的大小.
文件:
在app的輸出路徑(<module-name>/build/outputs/mapping/release/
)下, 和mapping.txt
一起, 還有一個resources.txt
文件.
文件的最後會列出一系列的Skipped unused resource
.
同時這個文件里也詳細地寫了資源之所以會被保留下來的理由. 關鍵字: matches string pool
. 這是前面說過的為了防止有動態使用資源的情況而採取的保守措施.
如果有空的話可以把這些Skipped unused resource
標註的文件都手動從代碼庫里刪除.
聲明支持的配置, 刪掉其他
Gradle的resource shrinker會刪除沒有被代碼用到的資源, 但是不會刪掉不同設備配置下的可替代資源
(alternative resources).
可以利用resConfigs
, 來刪除你的app並不需要的配置資源.
原理就是聲明app支持的配置, 這樣其他不支持的配置的資源就會被刪除, 從而減少apk的尺寸.
比如你包含了一個庫, 這個庫中帶有各種語言的資源, 這樣預設你的app就會包含所有這些庫中包含的語言的字元串. 你可以明確聲明app支持的語言, 那麼其他語言資源就會被刪除:
android {
defaultConfig {
...
resConfigs "en", "fr"
}
}
見Remove unused alternative resources
安裝包瘦身措施: 發佈階段
因為有的資源會提供多個版本, 所以一個用戶下載的apk中可能包含了一些他永遠也用不到的內容.
為了保證每個用戶的下載尺寸都儘量小, 我們可以改進發佈包的形式: 拆分成多個針對不同機型的apks;
如果是在Google Play發佈, 還可以用App Bundle, 由Google Play幫我們做拆分.
Build多個apks
根據屏幕密度和指令集(ABI)的不同, 可以拆分多個apk, 每種配置只下載自己需要的資源, 從而達到減小安裝包大小的目的.
Gradle就可以幫我們做這個拆分創建的工作.
具體實現見:
Build multiple APKs.
Android App Bundles
用Google Play發佈應用時可以使用Android App Bundle的格式, 這是一種新的發佈格式, 包含了應用所有的編譯代碼和資源, 把apk的生成和簽名留給Google Play來做.
Google Play會根據用戶不同的設備配置來生成優化的apk, 這樣用戶下載的就只是對應自己設備的代碼和資源. 開發者不用為不同設備準備多個apk, 用戶也得到了更小更優化的結果.
安裝包瘦身措施: 進階補充
要更加極致地壓縮安裝包, 還有一些更加複雜的手段, 比如:
- 業務層面: 動態feature, 資源動態載入.
- Dex: 加大混淆力度, 混淆四大組件和View; 去除debug信息; 優化Dex分包; Dex壓縮.
- native庫: 去除debug信息; 壓縮; 剪裁.
- 資源: 資源名混淆成短路徑; 壓縮.
另外, 還可以將對安裝包體積的分析加入到CI中, 進行自動化監控, 這樣可以得到各個版本的尺寸變化, 及時獲知是否不經意引入大的依賴, 引入過大的資源等情況.
總結
總結回顧一下本文內容, 對於安裝包瘦身的話題:
首先, 討論了我們為什麼要做這個事情 -> 為了增加app被下載安裝使用和留存的幾率. (為什麼要瘦身呢? -> 為了提升自己增加競爭力.)
其次, 介紹了安裝包的構成, 相關的指標和用於檢測的工具. (瘦身需要關註的指標和測量方式.)
重點介紹, 如何減少安裝包體積的實踐方法. (瘦身的各種手段.)
參考資料
- Reduce your app size
- Shrink, obfuscate, and optimize your app
- Configure APK Splits
- Build multiple APKs
- Screen sizes and densities
- Analyze your build with APK Analyzer
- 支付寶 App 構建優化解析:Android 包大小極致壓縮
- 美團 - Android App包瘦身優化實踐
- 極客時間: Android開發高手課, 包體積優化 (上), (下)
最後, 歡迎關註微信公眾號: 聖騎士Wind