1. 程式人生 > >Android應用程式外掛化研究之DexClassLoader

Android應用程式外掛化研究之DexClassLoader

最近在研究Android應用的外掛化開發,看了好幾個相關的開源專案。外掛化都是在解決以下幾個問題:
* 如何把外掛apk中的程式碼和資源載入到當前虛擬機器。
* 如何把外掛apk中的四大元件註冊到程序中。
* 如何防止外掛apk中的資源和宿主apk中的資源引用衝突。

就這幾個問題,我開始研究外掛化開發實現的相關技術,本篇文章主要講第一點:如何載入另一個apk中的class。我們要把一個包含class檔案的jar載入到java虛擬機器中,需要使用ClassLoader這個類。Android的編譯系統中對class檔案進行了進一步的處理:最後變成 .dex檔案,.dex檔案包含在apk中,google提供了一個類來載入.dex檔案,這個類就是DexClassLoader,它繼承自ClassLoader。本篇的重點是寫一個例子來說明DexClassLoader的用法。先來熟悉下DexClassLoader。

DexClassLoader介紹

DexClassLoader是一個類載入器,可以用來從.jar和.apk檔案中載入class。可以用來載入執行沒用和應用程式一起安裝的那部分程式碼。建構函式:DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent)

dexPath:被解壓的apk路徑,不能為空。

optimizedDirectory:解壓後的.dex檔案的儲存路徑,不能為空。這個路徑強烈建議使用應用程式的私有路徑,不要放到sdcard上,否則程式碼容易被注入攻擊。

libraryPath:os庫的存放路徑,可以為空,若有os庫,必須填寫。

parent:父親載入器,一般為context.getClassLoader(),使用當前上下文的類載入器。

建立一個外掛apk工程module:apkbeloaded

我把外掛的包名命名為:com.dexclassdemo.liuguangli.apkbeloaded,我們建立兩個類:Registry和ClassToBeImportedRegistry:

package com.dexclassdemo.liuguangli.apkbeloaded;
import java.util.ArrayList;
/**
 * Created by liuguangli on 16/2/13.
*/
public class Registry {
    public static ArrayList<Class<?>> _classes = new     ArrayList<Class<?>>();
    static{
        _classes.add(ClassToBeImported.class);
        //more classes here
    }
}

這個類中個只有一個成員變數_classes,集合引用ClassToBeImported.class。
ClassToBeImported:

package com.dexclassdemo.liuguangli.apkbeloaded;
import android.util.Log;
/**
 *Created by liuguangli on 16/2/13. 
*/
public class ClassToBeImported { 
     public static ClassLoader method(){  
       Log.v("ClassToBeImported", "called method of class " + ClassToBeImported.class.getName());
      return ClassToBeImported.class.getClassLoader(); 
    }
}

我們會演示這個方法如何在宿主中被呼叫的,並且我們可以跟蹤這個類的類載入器。我們編譯這個工程得到的ask檔案為:apkbeloaded-debug.apk。

建立一個宿主apk工程

我把宿主包名命名為:dexclassloaderdemo。我們在assets目錄下建立一個目錄plugins,然後把apkbeloaded-debug.apk拷貝到該目錄下。在MainActivity中建立一個方法為:loadDexClasses

public void loadDexClassses() { 
    if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 
    Log.v("loadDexClassses", "LoadDexClasses is only available for ICS or up"); 
    } 
    String paths[] = null; 
    try { 
         paths = getAssets().list("plugins");
     } catch (IOException e) {
         e.printStackTrace(); 
     } 
    if (paths == null) { 
        Log.v("loadlDexClasses", "There was no " + paths); return; 
    } 
    Log.v("loadDexClasses", "Dex Preparing to loadDexClasses!");
    for (String file : paths) { 
        //接下來完善 
    } 
}

我們從getAssets的plugins中讀取檔案列表,然後打算逐個載入(當然本例子中只有一個)。注意,我們不能直接從asserts目錄中載入apk(asserts不是一個檔案系統目錄,只是apk的一個資源目錄),先來寫一個從assert目錄中拷貝apk到一個檔案目錄下:copyAssetsApkToFile(本例中拷貝到sdcard,實際開發中最好拷貝到私有目錄,防止被注入)。

public void copyAssetsApkToFile(Context context, String src, String des) {
     try { 
        InputStream is = context.getAssets().open(src); 
        FileOutputStream fos = new FileOutputStream(new File(des));
        byte[] buffer = new byte[1024]; 
        while (true) { 
            int len = is.read(buffer); 
            if (len == -1) { 
                break; 
            } 
            fos.write(buffer, 0, len); 
       } 
       is.close();
       fos.close(); 
     } catch (Exception e) 
     { 
        e.printStackTrace();
     } 
}

下面我們來完善核心程式碼:

for (String file : paths) { 
    File pluginDir = Environment.getExternalStorageDirectory();     
    pluginDir.mkdirs(); 
    String desDir = pluginDir.getAbsolutePath(); 
    String des = desDir + "/" + "apkbeloaded-debug.apk";
    File desFile = new File(des);
    File optimizedDirectory = this.getDir(OPT_DIR, Context.MODE_PRIVATE);
    if (!desFile.exists()){ copyAssetsApkToFile(this, "plugins/"+file, des);
    } 
    final DexClassLoader classloader = new DexClassLoader( des, optimizedDirectory.getAbsolutePath(), "data/local/tmp/natives/", ClassLoader.getSystemClassLoader()); 
    Log.v("loadDexClasses", "Searching for class : " + "com.registry.Registry");
     try {
     Class<?> classToLoad = (Class<?>) classloader.loadClass("com.dexclassdemo.liuguangli.apkbeloaded.Registry"); 
     Field classesField = classToLoad.getDeclaredField("_classes"); 
     ArrayList<Class<?>> classes = null;
     classes = (ArrayList<Class<?>>) classesField.get(null); 
    for (Class<?> cls : classes) {
     Log.v("loadDexClasses", "Class loaded " + cls.getName());
     if (cls.getName().contains("ClassToBeImported")) { 
      Method m = cls.getMethod("method"); 
      ClassLoader xb = (ClassLoader) m.invoke(null);
      if (xb.equals(ClassLoader.getSystemClassLoader())) 
       Log.v("loadDexClasses", "Same ClassLoader");
      else 
       Log.v("loadDexClasses", "Different ClassLoader");    
     Log.v("loadDexClasses", xb.toString()); 
    } 
  }
 } catch (IllegalAccessException e) { 
    e.printStackTrace(); 
 } catch (ClassNotFoundException e) { 
  e.printStackTrace(); 
 } catch (NoSuchFieldException e) { 
  e.printStackTrace(); 
 } catch (NoSuchMethodException e) { 
  e.printStackTrace(); } catch (InvocationTargetException e) { 
  e.printStackTrace(); 
 }
}