Try   HackMD

Android 64K 方法 & MultiDex

OverView of Content

64K 方法

當 APP 開發到一定版本以後,必可避免的會遇到 APK 64K 方法數的問題,這個跟 Dex 虛擬機有
關係,這裡也會說明解決方案

64K 其實是方便記憶,其實限制數目為 2 Byte => 0xFFFF 也就是 65536

64K 方法 - 錯誤訊息

若 APP 中建構的方法數量 64K,則會在編譯的時候出現錯誤,導致 build 失敗,錯誤訊息如下

  1. 舊版本錯誤訊息
    ​​​​# 舊版本錯誤訊息 ​​​​Conversion to Dalvik format failed: ​​​​Unable to execute dex: method ID not in [0, 0xffff]: 65536
  2. 新版錯誤訊息
    ​​​​# 新版本錯誤訊息 ​​​​trouble writing output: ​​​​Too many field references: 131000; max is 65536. ​​​​You may try using --multi-dex option.

64K 限制原因

  • Android APK 的看作是一個 壓縮檔,裡面包含許多的 dex 可執行檔

    • dex 檔案
      dex 檔案簡單來說就是 .class 檔案透過 javac 編譯過後的 .java 檔案壓縮而成,Dex 檔案也是專門給 Dalvik 虛擬機執行的檔案

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

  • 64K 方法限制是因為 Dalvik 可執行檔限制,單一 .dex 檔案最多能參考的方法數為 65536 個 (也就是 2 Byte 的大小)

MultiDex

Android 5.0 之前 (API level 21)

  • 在 Android 5.0 之前,全部的 dex 檔案都是透過 Dalvik 虛擬機來運行的,當方法數量超過 64K 時我們需要拆分 classes.dex 檔案 (將一個 classes.dex 檔拆成 classes.dex、classes2.dex、classes3.dex)
  • 在啟動時會先加載 classed.dex 檔案 (classed.dex 檔案也稱為 Primary dex),之後才會加載其他 dex 檔案 (其他的稱為 Secondary dex)

Android 5.0 之前 - MultiDex 注意事項

  1. 應用啟動後會,Dalvik 虛擬機會執行 dexopt 的操作 (優化 dex 檔案),產生 ODEX 檔案,這個過程 複雜、耗時,若 dex 檔案過大則可能導致 ANR

  2. 由於 Dalvik 的線性記憶體分配器 LinearAlloc 的限制,使用 MultiDex 的應用在出現很大的記憶體分配時容易導致 App 當機,根本原因是 Dalvik 虛擬機用來載入類別的堆積記憶體大小被強制寫入

    • 堆積記憶體大小
    Android 版本 堆積記憶體大小
    Android 2.3 以下 5M
    Android 2.3 8M
    Android 4.0 16M
    • Android 5.0 以上使用 ART,就不存在 LinearAlloc 限制問題 (除非你手動用 ClassLoader 加載)
  • 由於存在 Primary、Secondary 兩種 dex 分類,所以還需區分那些類必須放置在 Primary dex (像是啟動 Application 類就要放置),若 ClassLoader 加載不到則會出現 NoClassDefFoundError 錯誤

Android 5.0 之後

  • 從 Android 5.0 開始,Android 使用了 ART 虛擬機來代替 Dalvik 虛擬機,但並非捨棄了 dex 檔案,ART 虛擬機仍可載入 .dex 檔案
  • ART 的特色是,在 APP 安裝期間會執行 預編譯 動作,掃描 APK 中的 全部的 .dex 檔案,並將它們編譯成 單一個 .oat 檔案,在應用執行期間去載入 .oat 檔案而不是 .dex
    • ART 不能在運行期間載入 dex 檔案 ?
      ART 預設會先跑 oat 檔,但能是支持運行期加載 dex 檔案的,熱修復有其中一個方案就是採用自己透過 ClassLoader 加載 dex 檔案做修復

Android Module

  • 每個 Android Module 都會對應到不同的 dex 檔案,有幾個 Module 就有幾個 dex 檔,通過這個點可以把主 dex 放置到主 Module 上,其他工具類放到別的 Module

    Image Not Showing Possible Reasons
    • The image file may be corrupted
    • The server hosting the image is unavailable
    • The image path is incorrect
    • The image format is not supported
    Learn More →

解決方案

有個廢話就是~ 盡量減少方法,不讓其觸及 64K 的上限,這麼說是有原因的

因為在 APP 啟動時載入的 classes.dex 檔案若過多、過大,會導致啟動速度降低,並且可能導致 OOM 等等問題 (上面有提過)

gradle proguard 設定

  • 可以透過 Gradle 的 proguard 設定,能套過分析位元組碼,壓縮並移除沒有使用到的函數,可以有效減少方法數量

專案開啟 MultiDex 設定

  1. 1 gradle#defaultConfig 設定 multiDexEnabled true,2 並使用 multidex Lib

    ​​​​android { ​​​​ compileSdk 31 ​​​​ defaultConfig { ​​​​ applicationId "com.alien.myapplication" ​​​​ minSdk 23 ​​​​ targetSdk 31 ​​​​ versionCode 1 ​​​​ versionName "1.0" ​​​​ ​​​​ // 設定 multiDexEnabled ​​​​ multiDexEnabled true ​​​​ } ​​​​} ​​​​dependencies { ​​​​ implementation 'com.android.support:multidex:1.0.3' ​​​​ ​​​​ // ... 省略其他 ​​​​}
  2. 自訂一個 Application,並對其設定(有兩種方案),1 繼承 MultiDexApplication、2 透過 MultiDex#install 初始化

    ​​​​// 方案一 ​​​​public class MyApplication extends MultiDexApplication { ​​​​ @Override ​​​​ protected void attachBaseContext(Context base) { ​​​​ super.attachBaseContext(base); ​​​​ // 方案二 ​​​​ MultiDex.install(this); ​​​​ } ​​​​}
    • 為何使用在 attachBaseContext 方法
      因為 attachBaseContext 方法在 onCreate 方法前執行
  3. 在 AndroidManifest.xml 設定自定義的 Application

    ​​​​<!-- AndroidManifest.xml --> ​​​​<application ​​​​ android:allowBackup="true" ​​​​ android:icon="@mipmap/ic_launcher" ​​​​ android:label="@string/app_name" ​​​​ android:roundIcon="@mipmap/ic_launcher_round" ​​​​ android:supportsRtl="true" ​​​​ android:theme="@style/Theme.MyApplication" ​​​​ android:name=".MyApplication"> ​​​​ <activity ​​​​ android:name=".MainActivity" ​​​​ android:exported="true"> ​​​​ <intent-filter> ​​​​ <action android:name="android.intent.action.MAIN" /> ​​​​ <category android:name="android.intent.category.LAUNCHER" /> ​​​​ </intent-filter> ​​​​ </activity> ​​​​</application>

Appendix & FAQ

tags: Android 進階