2014年2月25日

使用 DexClassLoader 動態載入 DEX


DEX 文件檔

  1. 虛擬器會在應用程式啟動時,動態的從儲存裝置讀取並解壓縮個別的 Classes 到記憶體中,在沒有經過適度優化的架構下,每個獨立的虛擬器行程,都會包含自己一份的相關 Classes 記憶體空間,而這些 Classes 就儲存在 dex 檔案中
  2. dex 檔案會以唯讀方案載入到記憶體中,並跨行程共享
  3. 可以把多個 Classes 檔案整合到一個 dex 檔案中
  4. 通常儲存於 apk 中,解壓縮 apk 檔後,即可以得到 classes.dex 文件檔
  5. 因為 Android 的虛擬機 (Dalvik VM) 是不認識 Java 打出 jar 的 byte code,需要通过 dex 工具來優化轉換成 Dalvik byte code 才行。在 apk 中可以看出:引入其他 Jar 的内容都被打包進了 classes.dex 

為何要動態載入 DEX 文件檔

  1. 當專案日益龐大,所產生的 dex 文件也會隨著變大。但在 Android 2.3 或以下版本中,限制 DEX 文件檔案最大為 5MB,再較新的版本則支援 8 或 16MB。如果您的 app 需支援 Android 2.3 或以下版本,且 DEX 超過 5MB,會出現 LinearAlloc exceeded capacity Problem,此時就需要將 DEX 檔案切割並動態載入
  2. DEX 檔案可跨行程共享,所以如果知道 DEX 中所含的 class 與 method 的話,其實也可以跨 app 共享 

如何產生 DEX 文件檔

  1. 先產生 JAR file:
    (1) 對專案右鍵 → Export → Java → JAR file
    (2) 選擇要輸出的 java file,並選擇儲存位置與檔名
    (3) 下一步到底,產出 JAR file
  2. 將 JAR file 優化換成 Dalvik byte code
    (1) 需使用 ADT 中所提供的 Ant 腳本來打包,版本為 Android SDK 12,可直接下載 platform-tools 與 tools 並解壓到對應目錄
    (2) 將 JAR file 拷貝到 SDK 安装目錄 android-sdk-windows\platform-tools 下,從 DOS 進入這個目錄,執行
    
    dx --dex --output=test.jar TestDex.jar  //dx --dex --output=輸出 輸入
    
    ※ 將 JAR 優化時應該重新打成 JAR (jar → dex → jar),上述指令就是把 TestDex.jar 里面的 .class 文件優化成 .dex 文件然後又打包成 test.jar
    ※ 無法直接加載 dex 文件會出現 "unable to open 'dex' file" Error
  3. 若 Android SDK Build-tools 版本為 19 的,請降版至 18.1,否則編譯時會出現
    
    Unable to execute dex: java.nio.BufferOverflowException
    
  4. 將優化過後的 JAR file 複製到手機中,接著就可以準備 LoadClass
    ※ .dex文件在 4.1 之後,Google 基於安全考量在 DexFile 函數中增加一個驗證文件歸屬權的步骤,故如果直接使用 Environment.getExternalStorageDirectory() 讀取成 file 會出現
    
    java.lang.IllegalArgumentException: Optimized data directory /storage/sdcard0 is not owned by the current user. Shared storage cannot protect your application from code injection attacks.
    
    剩至會出現 NoClassDefFoundError (本篇範例主要是提供 android 2.3 或以下系統解決 LinearAlloc 問題,故暫不考慮 android 4.X 版系統的運行結果,android 4.X 版系統可透過 getDir 來取得 file)
※ res目錄下檔案,AndroidManifest 不須打包成 dex 文件,因為打包成 apk 時就會包含 

動態載入 dex 中 class

※ 動態載入 class 有兩種方法:
(1) PathClassLoader:加載路徑必須在 /data/app 路徑下
(2) DexClassLoader:加載 sdcard 目錄下的 apk 或 jar 文件

這裡示範使用 DexClassLoader 來載入 class

//載入手機中優化過的 JAR
final File optimizedDexOutputPath = new File(Environment.getExternalStorageDirectory().toString() + File.separator + "wasync.jar");

//載入 JAR 中的  class
DexClassLoader cl = new DexClassLoader(optimizedDexOutputPath.getAbsolutePath(), 
  Environment.getExternalStorageDirectory().toString(),
  null,
  KoKoLaApplication.getInstance().getClassLoader());

Class libProviderClazz = null;

try {
 libProviderClazz = cl.loadClass("org.slf4j.LoggerFactory");  //class 完整名稱

 Method getInstance = libProviderClazz.getDeclaredMethod("getLogger", String.class);  //呼叫 class 中 method
 getInstance.setAccessible(true);  //設定 method 是否可訪問,不加會出現 access to method denied
    Object logger = getInstance.invoke(null, classname); //第一個参数為 null 表示此為靜態方法
    
 return logger;

} catch (Exception exception) {
 exception.printStackTrace();
}

應用範圍:

app 連動程式,例如:line 遊戲連動,可直接載入 line 使用者資料,不需再連網路下載使用者資訊,減少使用者等待時間

References