Android中插件化實現的原理,宿主app運行插件中的類 (一)

https://blog.csdn.net/lin20044140410/article/details/104584877

什麼是插件化?

直白點說,就是去運行沒有安裝的apk,把這些沒有安裝過的apk理解爲插件,把運行這些插件的apk稱爲宿主.

爲什麼需要插件化?

由於宿主可以在運行時動態去加載和運行插件,這就可以把apk中不常用的功能模塊做成插件,減小了apk包的大小,實現了apk功能的動態擴展.

插件化和組件化的區別:

組件化,是把app分成多個模塊,獨立開發,根據需求,利用gradle配置模塊間的依賴關係,但是最終發佈時模塊和app會打包成一個apk.

插件化,也是把app分成多個模塊,這些模塊中有一個宿主,多個插件,最終打包時宿主apk和插件apk可以分開打包,插件apk也可以在需要時由網絡下載,然後加載運行.

插件化實現的思路:

1)加載插件的類

2)加載插件的資源

3)啓動插件中的組件(四大組件)

這篇文章先來實現第一個問題:加載插件的類,這裏插件的類,是普通的類,不是四大組件,四大組件的加載運行會放在後面的文章來討論.

加載類就要用到類加載器,java中jvm加載的class文件,android中的虛擬機加載的dex文件(就是多個class文件的合併),這裏主要看android中如何加載dex文件的.

android中的類加載分兩種:系統類加載器,自定義的類加載器,他們的關係如圖:

BootClassLoader 用於加載framework層的class文件

PathClassLoader 用於加載應用程序中class文件,這裏也包括應用中依賴的庫文件,具體形式可以是apk,jar,zip中的dex文件.

DexClassLoader 用於加載制定的dex文件.

在一個自動生成的app中,MainActivity.java 這個類是由PathClassLoader來加載的, 而android.app.Activity.java這個framework中類時由BootClassLoader加載的.

從源碼看,DexClassLoader,PathClassLoader除了構造函數外,沒有實現任何代碼,類加載的工作都是由父類BaseDexClassLoader時完成的.

這裏有一點需要注意的是:PathCalssLoader的parent類加載器是BootClassLoader,並不是其繼承關係的父類.

在分析類加載流程之前,先看下類的生命週期:

加載 是 類加載過程中的一個階段,在加載階段,虛擬機要完成3件事情:

1)通過類的全限定名,獲取類的二進制字節流

2)將這個字節流代表的靜態存儲結構,轉化爲方法區的運行時數據結構

3)在內存中生成一個代表這個類的java.lang.class對象,作爲方法區這個類的各種數據的訪問入口,這個Class對象是比較特殊的,雖然它是對象,卻是放在方法區的.

下面就通過源碼看下加載一個類的過程,從loadClass方法的實現看起,DexClassLoader,PathClassLoader及父類BaseDexClassLoader,一直往上查找,這個方法的實現在ClassLoader.java中.

   protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    c = findClass(name);
                }
            }
            return c;
    }

這裏的實現就是雙親委派模型, 

1,首先去查找這個類是不是已經加載過,是就直接返回,否則,

2, 遞歸的去請求parent加載器去完成加載,在遞歸過程中,走到BootClassLoader後,就不會再請求其parent來加載,而是通過其findClass來加載,如果無法完成這個加載請求,就返回null.

3,如果都沒有加載成功,才由自己來完成加載findClass, 

這個雙親委託機制,讓類的加載有了一種優先級的層次關係,比如我們Activity這個類,不管是誰請求加載這個類,最終都是委託給這個模型最頂端的BootClassLoader來加載,所以activity在各種類加載環境中都是同一個類,如果沒有這種機制,自己去定義類加載器,然後自己定義一個android.app.Activity類,那麼系統中就會出現多個不同的Activity類,那就亂了.

從上面雙親委派模型的加載流程,如果要自己加載類,時調用findClass方法,下面看BaseDexClassLoader.java中的這個方法的實現:

(因爲DexClassLoader,PathClassLoader都沒定義這個方法,所以實際調用的是BaseDexClassLoader.java中的)

  public BaseDexClassLoader(ByteBuffer[] dexFiles, ClassLoader parent) {
        // TODO We should support giving this a library search path maybe.
        super(parent);
        this.pathList = new DexPathList(this, dexFiles);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
        Class c = pathList.findClass(name, suppressedExceptions);
        if (c == null) {
            ClassNotFoundException cnfe = new ClassNotFoundException(
                    "Didn't find class \"" + name + "\" on path: " + pathList);
            for (Throwable t : suppressedExceptions) {
                cnfe.addSuppressed(t);
            }
            throw cnfe;
        }
        return c;
    }

它又是通過DexPathList來實現的.

DexPathList.java

    public Class<?> findClass(String name, List<Throwable> suppressed) {
        for (Element element : dexElements) {
            Class<?> clazz = element.findClass(name, definingContext, suppressed);
            if (clazz != null) {
                return clazz;
            }
        }

        if (dexElementsSuppressedExceptions != null) {
            suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
        }
        return null;
    }

dexElements是一個數組,其中的元素 Element裏面包含了dex文件,如果app中有多個dex文件,這個數組就會有多個Element元素,每個Element元素中包含一個dex文件,

DexPathList#Element.java

        public Class<?> findClass(String name, ClassLoader definingContext,
                List<Throwable> suppressed) {
            return dexFile != null ? dexFile.loadClassBinaryName(name, definingContext, suppressed)
                    : null;
        }

最終是通過Element中的dexfile來獲取類的二進制字節流.

在回頭去看看dexElements這個數組怎麼來的?

        // save dexPath for BaseDexClassLoader
        this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,
                                           suppressedExceptions, definingContext, isTrusted);

從構造函數的回溯,可以看到dexElements中元素,來自與第一個參數dexpath,這個路徑通常就是apk的路徑,或者jar,zip,dex文件的路徑,具體的值是有創建類加載器傳入的參數確定.

分析到這裏,應該能明白一點:如果要讓app能加載插件的class文件,只要把插件的dex文件,也存入dexElement數組就OK 了.

通過代碼驗證,使用DexClassLoader,PathClassLoader能不能成功加載插件的類.

先定義一個插件module,新建測試類:

package com.test.plugintest;

import android.util.Log;

public class PlugInDemo {
    public static void testPlugIn() {
        Log.d("PlugIn","from plugin PlugInDemo.");
    }
}

把這個類打包成一個dex文件,具體做法是使用sdk下dx工具,把class文件打包成dex,

D:\NDKDemo\plugintest\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes>
dx --dex --output=plugIn.dex com\test\plugintest\PlugInDemo.class
D:\NDKDemo\plugintest\build\intermediates\javac\debug\compileDebugJavaWithJavac\classes>

執行上面的命令,會在當前目錄下生成一個plugIn.dex文件,這裏有一點值得注意,執行dx命令的位置最好在classes這個目錄,然後指定具體class文件時,使用全類名

把plugIn.dex拷貝到sdcard目錄下,

    public void loadClassPlugIn() {
        DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugIn.dex",
                MainActivity.this.getCacheDir().getAbsolutePath(),
                null,
                MainActivity.this.getClassLoader());
//        PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugIn.dex",
//                MainActivity.this.getClassLoader());
        try {
            Class<?> clazz = dexClassLoader.loadClass("com.test.plugintest.PlugInDemo");
            Method method = clazz.getMethod("testPlugIn");
            method.invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

在宿主App的MainActivity中,可以通過上面的ClassLoader加載PlugInDemo.java類,通過反射調用其中方法。

通過這個測試,只是去驗證通過自定義類加載器可以去加載執行一個插件中的類.但是這個插件的類並沒有合併到app中.

下面就來實現把插件的類,合併到宿主app中,實現步驟:

1,創建插件的DexClassLoader類加載器,然後反射拿到插件的dexElements數組

2,獲取宿主的PathClassLoader類加載器,然後反射拿到宿主的dexElements數組

3,把宿主和插件的兩個dexElements數組合並

DexPathList.java
   private Element[] dexElements;
BaseDexClassLoader.java
    private final DexPathList pathList;

dexElements在DexPathList中,而DexPathList在BaseDexClassLoader中,所以先獲取類加載器,然後反射獲取到DexPathList,在反射拿到dexElements.


    public void loadPlugInModule() {
        try {
//BastDexClassLoader,DexPathList,宿主和插件可公用
            Class<?> baseDexClassLoader = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoader.getDeclaredField("pathList");
            pathListField.setAccessible(true);

            Class<?>  dexPathListClassLoader = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClassLoader.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);
            //創建插件的DexClassLoader類加載器,然後反射拿到插件的dexElements數組
            DexClassLoader plugInDexClassLoader = new DexClassLoader(
                    "/sdcard/plugintest-debug.apk",
                    mContext.getCacheDir().getAbsolutePath(),
                    null,
                    mContext.getClassLoader());
            //獲取插件類加載器中DexPathList實例對象
            Object plugInDexPathList =  pathListField.get(plugInDexClassLoader);
            //拿到插件的dexElements數組
            Object[] plugInDexElements = (Object[]) dexElementsField.get(plugInDexPathList);

//獲取宿主的PathClassLoader類加載器,然後反射拿到宿主的dexElements數組
            PathClassLoader hostPathClassLoader = (PathClassLoader) mContext.getClassLoader();
            //獲取宿主類加載器中DexPathList實例對象
            Object hostDexPathList =  pathListField.get(hostPathClassLoader);
            //拿到宿主的dexElements數組
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostDexPathList);

            //創建新的DexElements數組
            Object[] dexElements = (Object[]) Array.newInstance(hostDexElements.getClass().getComponentType(),
                    hostDexElements.length + plugInDexElements.length);
            //合併宿主和插件的dexElements數組
            System.arraycopy(hostDexElements,0, dexElements, 0, hostDexElements.length);
            System.arraycopy(plugInDexElements, 0, dexElements, hostDexElements.length, plugInDexElements.length);

            //用合併後的數組替換宿主的原dexElements數組
            dexElementsField.set(hostDexPathList, dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

在將插件apk中class加載後,就可以通過反射訪問其中的class屬性了.

    public void loadClassPlugIn() {
//        DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/plugIn.dex",
//                MainActivity.this.getCacheDir().getAbsolutePath(),
//                null,
//                MainActivity.this.getClassLoader());
////        PathClassLoader pathClassLoader = new PathClassLoader("/sdcard/plugIn.dex",
////                MainActivity.this.getClassLoader());

        try {
            Class<?> clazz = Class.forName("com.test.plugintest.PlugInDemo");
            Method method = clazz.getMethod("testPlugIn");
            method.invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章