2014-03-10

Android DexClassLoader


本文述敘 DexClassLoader 之使用方法 ,概略內容如下 :

範例程式將在執行時下載 dex 檔案並於載入後呼叫 dex 提供的函數來開啟新的 Activity,此 Activity 將會利用 Toast 顯示一段啟動成功的訊息。

1. 製作 dex 檔案 :
將底下程式碼另存成檔案 PayloadActivity.java :
package com.example;

import android.app.Activity;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;

public class PayloadActivity extends Activity {
  public static void starter(Activity parentActivity){
    Intent tmp_intent =new Intent(parentActivity, PayloadActivity.class);
    parentActivity.startActivity(tmp_intent);
  }
  
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    Toast.makeText(this, "Instantiate PayloadAcitivty Successful", Toast.LENGTH_LONG).show();
  }
}
PayloadActivity class 提供靜態函數 starter,此函數被呼叫之後會啟動 Payload Activity。

把 PayloadActivity.java 編譯成 dex 檔案 (Mac OSX 環境)  :
首先將 java 檔案編譯成 class 檔案 (其中 path_to_sdk 指向 android sdk 之路徑):
javac ./PayloadActivity.java -cp /path_to_sdk/adt-bundle-mac-x86_64-20130917/sdk/platforms/android-10/android.jar -d .
將 class 檔案壓縮成 PayloadActivity.jar 檔案 :
jar cvfM ./PayloadActivity.jar com/
刪除執行編譯指令時產生的 com 資料夾 :
rm -rf ./com
將 jar 檔案轉成 dex 檔案 :
/path_to_sdk/adt-bundle-mac-x86_64-20130917/sdk/build-tools/android-4.3/dx --dex --output=PayloadActivity.dex.jar ./PayloadActivity.jar

將完成的 PayloadActivity.dex.jar 擺在網路空間。 ( 此範例假設可利用 url http://localhost/PayloadActivity.dex.jar 取得檔案 )

2. 程式執行時期下載 dex 檔案 :
 建立一個 com.example.dexclassloader 之 Android Application Project

主要啟動的 Activity 設定為 MainActivity

在 Eclipse 裡新創 Android Application Project,將主要 Activity ( MainActivity.java ) 替換如下 :
package com.example.dexclassloader;

import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.HttpURLConnection;
import java.net.URL;

import dalvik.system.DexClassLoader;

import android.os.Bundle;
import android.app.Activity;
import android.content.Context;
import android.util.Log;
import android.view.Menu;

public class MainActivity extends Activity {
 
 public static ClassLoader payloadClassLoader =null;
 private Smith<ClassLoader> sClassLoader =null;

 @Override
 protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  
  Thread works =new Thread(){
   @Override
   public void run(){
    //download and save the jar
    downloadDexFile("http://localhost/PayloadActivity.dex.jar", "PayloadActivity.dex.jar");
    
    //override class loader
    overrideClassLoader();
    
    //load the jar
    loadAndInvokeMethod();
   }
  };
  works.start();
 }

 @Override
 public boolean onCreateOptionsMenu(Menu menu) {
  // Inflate the menu; this adds items to the action bar if it is present.
  getMenuInflater().inflate(R.menu.main, menu);
  return true;
 }
 
 private void downloadDexFile(String url, String filename){
     try {

         URL u = new URL(url);
         HttpURLConnection c = (HttpURLConnection) u.openConnection();
         c.setRequestMethod("GET");
         c.setDoOutput(true);
         c.connect();
         
         FileOutputStream f =openFileOutput(filename, MODE_PRIVATE);
         InputStream in = c.getInputStream();

         byte[] buffer = new byte[1024];
         int len1 = 0;
         
         while ((len1 = in.read(buffer)) > 0) {
             f.write(buffer, 0, len1);
         }
         f.close();
         
     } catch (Exception e) {
         //error dialog
         Log.d("mainactivity", "error -"+e.toString());

     }
 }
 
 private void overrideClassLoader(){
  try{
         Context mBase =new Smith<Context>(this, "mBase").get();
         Object mPackageInfo =new Smith<Object>(mBase, "mPackageInfo").get();
         sClassLoader =new Smith<ClassLoader>(mPackageInfo, "mClassLoader");
 
         ClassLoader mClassLoader =sClassLoader.get();
 
         MyClassLoader cl =new MyClassLoader(mClassLoader);
         sClassLoader.set(cl);
  }catch(Exception e){
   
  }
 }
 
 private void loadAndInvokeMethod(){
  try{
         String libPath =getFilesDir()+"/PayloadActivity.dex.jar";
         File optimizedDexOutputPath = getDir("outdex", Context.MODE_PRIVATE);
         DexClassLoader dcl=new DexClassLoader(libPath, optimizedDexOutputPath.getAbsolutePath(), null, getClass().getClassLoader());
         payloadClassLoader =dcl;
 
         //LOAD CLASS
         Class<Object> payloadClass =(Class<Object>)dcl.loadClass("com.example.PayloadActivity");
         
         //LOAD METHOD
         Method starterFunc =payloadClass.getMethod("starter", Activity.class);
         
         //INVOKE METHOD
         starterFunc.invoke(null, this);
         
  }catch(Exception e){
   
  }
 }

}

觀察 MainActivity 的 onCreate 函數可知程式啟動時將從 url ( http://localhost/Payload.dex.jar ) 下載檔案 (downloadDexFile 函數),並將檔案暫存在裝置記憶體裡。

完成後使用 DexClassLoader 載入 dex 檔案並執行檔案裡提供的 starter 函數 (loadAndInvokeMethod 函數)。
下一節解說函數 overrideClassLoader 的功能。

3. 從 dex 檔案啟動 Activity (修改 ClassLoader) :

藉由 DexClassLoader 載入的程式在啟動 Activity 時 (Activity Class) 不會搜尋 DexClassLoader 所載入的 dex 檔案,因此會出現找不到 Class 的 Exception。為解決這個問題,透過修改 ClassLoader ,我們可以讓程式先搜尋 dex 檔案,若找不到目標則再搜尋預設的 jar 檔案。

底下是覆寫 loadClass 函數部分的程式碼 :

package com.example.dexclassloader;

public class MyClassLoader extends ClassLoader {
 public MyClassLoader(ClassLoader parent){
  super(parent);
 }
 
 @Override
 public Class<?> loadClass(String className) throws ClassNotFoundException{
  if (MainActivity.payloadClassLoader !=null){
   try{
                Class<?> c = MainActivity.payloadClassLoader.loadClass(className);
                if (c != null){
                  return c;
                }
   } catch (ClassNotFoundException e) {
   }
  }
  
  return super.loadClass(className);
 }
}

此外 overrideClassLoader 函數中的 Smith Class 可以幫助我們取得 ClassLoader,其程式碼可以參考這裡

4. AndroidManifest.xml

雖然可以透過 dex 檔案更新並且啟動 activity,但注意的是此 activity 在安裝 apk 之前,必須先定義在 apk 的 AndroidManifest.xml 檔案裡,AndroidManifest.xml 檔案內容如下 :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.dexclassloader"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk
        android:minSdkVersion="8"
        android:targetSdkVersion="18" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name="com.example.dexclassloader.MainActivity"
            android:label="@string/app_name" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity android:name="com.example.PayloadActivity" android:label="@string/app_name" />
    </application>
    
  <uses-permission android:name="android.permission.INTERNET" />
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />

</manifest

ref:http://stackoverflow.com/questions/6857807/is-it-possible-to-dynamically-load-a-library-at-runtime-from-an-android-applicat
ref:http://stackoverflow.com/questions/2519760/android-how-to-use-dexclassloader-to-dynamically-replace-an-activity-or-service