本文述敘 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