歡迎光臨
每天分享高質量文章

你必須要懂的 APK 瘦身知識

作者 :  我是吸血鬼

鏈接:https://www.jianshu.com/p/5921e9561f5f

隨著業務複雜度的逐漸增加,代碼、資源也在不斷的增加,此時你的APP大小也在增加。從用戶層面來說,面對動輒幾十兆的APP來說在非WIFI情況下還是會猶豫要不要下載,不下載你就可能因此失去了一個用戶。從公司層面來講,流量就是錢,減少APP的大小就顯得尤為重要。從開發者層面上來講,你掌握了這個手藝也會略顯逼格滿滿。

廢話不多說了,開始正題。

1、APK結構的那些事

知己知彼,方能百戰不殆。瞭解應用程式APK的結構對於我們來說很有幫助。APK檔案由一個ZIP存檔組成,其中包含組成應用程式的所有檔案。這些檔案包括Java類檔案,資源檔案和包含編譯資源的檔案。

APK包含以下目錄:

  • META-INF/:包含CERT.SF和 CERT.RSA簽名檔案以及MANIFEST.MF 清單檔案。

  • assets/:包含應用可以使用AssetManager物件檢索的應用資源。

  • res/:包含未編譯到的資源 resources.arsc。

  • lib/:包含特定於處理器軟體層的編譯代碼。該目錄包含了每種平臺的子目錄,像armeabi,armeabi-v7a, arm64-v8a,x86,x86_64,和mips。

  • resources.arsc:包含已編譯的資源。該檔案包含res/values/ 檔案夾所有配置中的XML內容。打包工具提取此XML內容,將其編譯為二進制格式,並將內容歸檔。此內容包括語言字串和樣式,以及直接包含在resources.arsc檔案中的內容路徑 ,例如佈局檔案和圖像。

  • classes.dex:包含以Dalvik / ART虛擬機可理解的DEX檔案格式編譯的類。

  • AndroidManifest.xml:包含核心Android清單檔案。該檔案列出應用程式的名稱,版本,訪問權限和取用的庫檔案。該檔案使用Android的二進制XML格式。
    來看看淘寶APP的unzip之後的檔案目錄

一般來講APK結構中比較大的部分一般是classes.dex、lib、res、assets這些檔案或者目錄。所以接下來將會針對這四種情況進行講解。

另外,我們通過APK Analyser 可以分析 APK

2、減小 classes.dex

classes.dex 包含了所有 Java 代碼。當你編譯你的應用時,gradle 會將你的所有模塊里的 .class 檔案轉換成 .dex 檔案並將這些檔案合成一個 classes.dex 檔案。

單個的 classes.dex 檔案可以容納大約 64K 方法。如果你達到了這個限制,你必須要在你的工程中啟用 multidexing。這將會創建另一個 classes1.dex 檔案去儲存剩下的方法。所以 classes.dex 檔案數目由你的方法數而定。

減少第三庫的使用

隨著業務的頻繁變更以及複雜度的增加,我們往往會使用第三方Libaray,有時候我們可能僅僅用到了很少一部分的功能,這個時候就需要慎重考慮完全取用。從我的開發經驗上來講,寧願參照自己去實現,也不願意多引入一個第三方庫。

避免列舉

一個列舉可以為您的應用程式的classes.dex檔案添加大約1.0到1.4 KB的大小 。這些添加可以快速累積到複雜系統或共享庫。如果可能,請考慮使用@IntDef註釋,這種型別轉換保留了列舉的所有型別安全優勢。

使用ProGuard

下麵這段來自 build.gradle 檔案的代碼用於為發佈構建啟用代碼壓縮:

android {
   buildTypes {
       release {
           minifyEnabled true
           proguardFiles getDefaultProguardFile('proguard-android.txt'),
                   'proguard-rules.pro'
       }
   }
   ...
}

除了 minifyEnabled 屬性外,還有用於定義 ProGuard 規則的 proguardFiles 屬性:

getDefaultProguardFile(‘proguard-android.txt’) 方法可從 Android SDK tools/proguard/ 檔案夾獲取預設的 ProGuard 設置。
提示:要想做進一步的代碼壓縮,請嘗試使用位於同一位置的 proguard-android-optimize.txt 檔案。它包括相同的 ProGuard 規則,但還包括其他在位元組碼一級(方法內和方法間)執行分析的優化,以進一步減小 APK 大小和幫助提高其運行速度。

proguard-rules.pro 檔案用於添加自定義 ProGuard 規則。預設情況下,該檔案位於模塊根目錄(build.gradle 檔案旁)。

3、優化assets和res中的資源檔案

題外話

res/raw和assets的相同點:

兩者目錄下的檔案在打包後會原封不動的儲存在apk包中,不會被編譯成二進制。
res/raw和assets的不同點:

1、res/raw中的檔案會被映射到R.java檔案中,訪問的時候直接使用資源ID即R.id.filename;assets檔案夾下的檔案不會被映射到R.java中,訪問的時候需要AssetManager類。

2、res/raw不可以有目錄結構,而assets則可以有目錄結構,也就是assets目錄下可以再建立檔案夾。
針對不同的情況,對於資源檔案有不同的優化策略。一般來講,對於res/drawable-**ddpi中的png資源可以進行壓縮。

3.1 圖片資源優化策略

格式壓縮

使用TinyPng或者Guetzli進行壓縮。Guetzli的使用可以參見我之前寫的博文https://www.jianshu.com/p/565e944bb594

使用WebP檔案格式

定位Android 3.2(API級別13)或更高級別時 ,您也可以使用WebP檔案格式來製作圖像,而不是使用PNG或JPEG檔案。WebP格式提供有損壓縮(如JPEG)以及透明度(如PNG),但可以提供比JPEG或PNG更好的壓縮。

Android 4.0 (API level 14) 支持有損壓縮的WebP格式,Android 4.3 (API level 18) 開始支持無損透明WebP圖像。

看下圖:

壓縮效率極高,僅為PNG格式的12%。驚喜不驚喜。。。

使用矢量圖形

您可以使用矢量圖形來創建與解析度無關的圖標和其他可伸縮媒體。使用這些圖形可以大大減少您的APK足跡。矢量圖像在Android中表示為VectorDrawable物件。通過一個VectorDrawable物件,一個100位元組的檔案可以生成一個與屏幕尺寸一致的清晰圖像。

但是,系統渲染每個 VectorDrawable物件需要很長時間,而較大的圖像需要更長的時間才能顯示在屏幕上。因此,只有在顯示小圖像時才考慮使用這些矢量圖形。

其它策略

有時候我們可能對一張圖片進行重覆利用,比如一張圖片僅僅是整體顏色的變換可以使用setColorFilter或者tint。儘量減少使用幀動畫,那可是一堆圖片呀。

3.2 壓縮資源

要啟用資源壓縮,請在 build.gradle 檔案中將 shrinkResources 屬性設置為 true。

android {
   ...
   buildTypes {
       release {
           shrinkResources true
           minifyEnabled true
           proguardFiles getDefaultProguardFile('proguard-android.txt'),
                   'proguard-rules.pro'
       }
   }
}

資源壓縮器目前不會移除 values/ 檔案夾中定義的資源(例如字串、尺寸、樣式和顏色)。這是因為 Android 資源打包工具 (AAPT) 不允許 Gradle 插件為資源指定預定義版本。
同時,我們也可以指定哪些資源可以保留下來。

例如,將下邊的代碼儲存在 res/raw/keep.xml。構建不會將該檔案打包到 APK 之中。


<resources xmlns:tools="http://schemas.android.com/tools"
   tools:keep="@layout/l_used*_c,@layout/l_used_a,@layout/l_used_b*"
   tools:discard="@layout/unused2" />

resources有以下屬性:

  • tools:keep 指出哪些資源會保留

  • tools:discard 指定哪些資源需要剔除

  • tools:shrinkMode 資源壓縮樣式,有兩種取值strict和safe,預設為safe

safe和strict的優化策略:

safe可以簡單理解為安全樣式,它會盡最大努力檢查代碼中可能會使用到的資源進行保留,避免運行時錯誤。

如果你的代碼呼叫 Resources.getIdentifier(),這就表示你的代碼將根據動態生成的字串查詢資源名稱。當你執行這一呼叫時,預設情況下資源壓縮器會採取防禦性行為,將所有具有匹配名稱格式的資源標記為可能已使用,無法移除。

String name = String.format("img_%1d", angle + 1);
res = getResources().getIdentifier(name, "drawable", getPackageName());

img_ 前綴的資源標記為已使用。

在strict樣式下,img_前綴的資源會做未使用的處理,因此你需要使用tools:keep手動進行已使用標識。

移除未使用的備用資源

我們知道google給我們的apk提供了國際化支持,如適應不同的屏幕解析度的drawable資源,還有適應不同語言的字串資源等等,但是在很多情況下我們只需要一些指定解析度和語言的資源就可以了,這個時候我們可以使用resConfigs方法來配置。

defaultConfig {
   // 對於國際化支持只打包中文資源,
   resConfigs "zh-rCN"
}

4、lib中資源優化

這裡我們主要講一下lib中動態鏈接庫的優化策略,也就是SO檔案。如果你有NDK的開發經驗可能會更容易理解一些。

為了支持不同指令集的情況,應用可能會包含armeabi、armeabi-v7a、x86的SO檔案等。

目前主流的機型都是支持armeabi-v7a的,並且armeabi-v7a兼容armeabi。所以在一般的開發中我們只需要使用armeabi-v7a 進行ABI支持。

有些SO庫可以採用網絡下載,把負擔放到用戶安裝完應用之後。對於哪些SO檔案可以放到網絡中加載,還需要看具體業務情況。

題外話,如果運行時找不到SO的話,會導致應用崩潰。

java.lang.UnsatisfiedLinkError: Couldn't load stlport_shared
 from loader dalvik.system.PathClassLoader: findLibrary returned null
at java.lang.Runtime.loadLibrary(Runtime.java:365)
at java.lang.System.loadLibrary(System.java:535)
at com.your.app.NativeClass.(Native.java:16)
... 63 more
Caused by: java.lang.UnsatisfiedLinkError: Library stlport_shared not found
at java.lang.Runtime.loadLibrary(Runtime.java:461)
at java.lang.System.loadLibrary(System.java:557)
at com.your.app.NativeClass.(Native.java:16)
... 5 more

我們也是有辦法應對的,可以參見這個開源專案ReLinker

另外關於SO的優化我會單獨拿出來講一講。

 

赞(0)

分享創造快樂