Android應用程序插件化研究之DexClassLoader

來自: http://www.liuguangli.win/archives/366

最近在研究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:父親加載器,一般為ClassLoader.getSystemClassLoader()。

創建一個插件apk工程module:apkbeloaded

我把插件的包名命名為:com.dexclassdemo.liuguangli.apkbeloaded,

我們創建兩個類:Registry和ClassToBeImported

Registry:

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
    

    } }</pre>

    這個類中個只有一個成員變量_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();
    

    } }</pre>

    我們會演示這個方法如何在宿主中被調用的,并且我們可以跟蹤這個類的類加載器。我們編譯這個工程得到的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) {
       //接下來完善
     }
    

    }</pre>

    我們從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();
     }
    

    }</pre>

    下面我們來完善核心代碼:

    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();
         }
    }</pre> 

Demo源碼

下載源碼: https://github.com/liuguangli/DexClassLoaderDemo

</div>

 本文由用戶 BeverlyHarv 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!