顯示具有 Android DEX 標籤的文章。 顯示所有文章
顯示具有 Android DEX 標籤的文章。 顯示所有文章

2014/03/03

DexClassLoader 應用 - 整合服務平台


原理:使用 DexClassLoader 動態載入 apk 中 dex file,並產生 classes 然後執行其 method
※ 此方法必須先知道 apk 中 dex file 有哪些 classes 與 method

Code 解說

目的:將編譯好的 classB 之 apk 放到 SD 卡目錄下,在使用 classA 動態啟動(載入) classB
class A

String path = Environment.getExternalStorageDirectory() + File.separator;  //File.separator 表示"/"符號,系統自動產生對應的 "\" 或 "/"
String filename = "TestClassLoadBDex.apk";

String optimizedDirectory = path + File.separator + "dex_temp"  ;
//為解決 .dex文件在 4.1 之後不能存在 SD 卡下,否則會出現 IllegalArgumentException 問題,這裡透過 getDir 來取得 file
File file = getDir("dex", 0) ;
DexClassLoader cl = new DexClassLoader(path + filename, file.getAbsolutePath(),null, getClassLoader());

Class libProviderClazz = null;

try {
 libProviderClazz = cl.loadClass("com.example.testclassloadb.MainActivity");  //class 完整名稱

 Method onCreateMethod = libProviderClazz.getDeclaredMethod("onCreate", Bundle.class);  //呼叫 class 中 method
 onCreateMethod.setAccessible(true);  //設定 method 是否可訪問
 onCreateMethod.invoke(this, savedInstanceState); //第一個参数為,一般 method 傳入 context,靜態 method 傳入 null
    
     Method changeTitle = libProviderClazz.getDeclaredMethod("changeTitle", String.class);  //呼叫 class 中 method
     changeTitle.setAccessible(true);  //設定 method 是否可訪問
 changeTitle.invoke(this, "class A call change title"); //第一個参数為,一般 method 傳入 context,靜態 method 傳入 null
    

} catch (Exception exception) {
 exception.printStackTrace();
}
class B (此部分代碼為錯誤示範)

@Override
protected void onCreate(Bundle savedInstanceState) {
 super.onCreate(savedInstanceState);
 setContentView(R.layout.activity_main);
 
 tv_title = (TextView) findViewById(R.id.tv_title);
 btn_showToast = (Button) findViewById(R.id.btn_showToast);
 
 Log.i("ruby", "path="+this.getFilesDir());
 
 btn_showToast.setOnClickListener(new OnClickListener() {
  
  @Override
  public void onClick(View v) {
   Toast.makeText(MainActivity.this, "in ClassLoadB", Toast.LENGTH_SHORT).show();
  }
 });
 
}

private void changeTitle(String str){
 tv_title.setText(str);
}

執行步驟:

  1. 將 classB 編譯成 apk 檔案
  2. 將 apk 放置手機 SD 卡跟目錄下 (這裡示範位置在根目錄,要自訂的請自行修改路徑)
  3. 執行 classA

執行結果:

輸入完以上代碼後執行,發現雖然成功執行了,但是 classA 並沒有載入 classB 的 apk,也沒有執行預定的 method (onCreate 與 changeTitle)
其實不安装apk,直接動態加載 dex 中的類別,是屬於透過反射的方式手動的創建 Activity
這種方式跟系统創建的 Activity 是不一樣的,没有系统創建之 Activity 的特性,而且 R 也找不到(無法透過 layout 來畫 UI),其實他並不是啟動 Apk 中的 Activity,而是在 classA Activity 上劃出 Apk 中 Activity 的 View

解決辦法:

由上述內容可知,其實只要手動產生一個 activity 並傳入 classB 即可正常執行 classB 及其 method
首先,增加一個 setActivity 的 method
class B

public void setActivity(Activity paramActivity) {
    Log.i("ruby", "setActivity..." + paramActivity);
    this.activity = paramActivity;
}
接著,在 classB OnCreate method 中修改 view 的呈現
※ classB 載入的 view 不可使用 resources 只能動態產生,因為載入 classB 時無法做 resources 連結
classB

boolean b = false;
TextView t = new TextView(this.activity);  
t.setText("class B"); 

if (savedInstanceState != null) {
 b = savedInstanceState.getBoolean("KEY_START_FROM_OTHER_ACTIVITY", false);
 if (b) {
  this.activity.setContentView(t);
 }
}
if (!b) {
 super.onCreate(savedInstanceState);
 setContentView(t);
}
最後,在 classA 呼叫 classB 的 onCreate method 時,先呼叫 setActivity method
classA

Constructor localConstructor = libProviderClazz.getConstructor(new Class[] {});
Object instance = localConstructor.newInstance(new Object[] {});
Method localMethodSetActivity = libProviderClazz.getDeclaredMethod(
  "setActivity", new Class[] { Activity.class });
localMethodSetActivity.setAccessible(true);
localMethodSetActivity.invoke(instance, new Object[] { this });

應用範圍:

app 整合平台,例如:game center classB 就像是遊戲大廳,classA 就像是裡面的各個小遊戲,讓不同公司開發出的小遊戲可以直接存取底層的遊戲大廳(classB)

getDeclaredMethod 與 getMethod 差異

  1. getDeclaredMethod:取得本身 class 中的所有方法,包含 public、protected 和 private 方法
  2. getMethod:取得 class 中的所有共有方法,包括自己的 public 方法、繼承來的方法、實作介面的 method 

References

2014/02/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