2014年3月3日

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