Android簡單插件化

1 插件化簡介

插件化緣來java

插件化技術最初源於免安裝運行APK的想法,這個免安裝的APK就能夠理解爲插件,而支持插件的APP咱們通常叫宿主。android

插件化解決的問題數組

  1. APP的功能模塊愈來愈多,體積愈來愈大
  2. 模塊之間的耦合度高,協同開發溝通成本愈來愈大
  3. 方法數目可能超過65535,APP佔用的內存過大
  4. 應用之間的互相調用

插件化與組件化app

組件化:組件化開發就是將一個APP分紅多個模塊,每一個模塊都是一個組件,開發的過程當中咱們可讓這些組件相互依賴或者單獨調試部分組件等,可是最終發佈的時候是將這些組件合併統一成一個APK,這就是組件化開發。框架

插件化:插件化開發和組件化略有不一樣,插件化開發是將整個APP拆分紅多個模塊,這些模塊包括一個宿主和多個插件,每一個模塊都是一個APK,最終打包的時候宿主APK和插件APK分開打包。ide

經常使用插件化框架對比函數

特性 dynamic-load-apk DynamicAPK Small DroidPlugin VirtualAPK
做者 任玉剛 攜程 wequick 360 滴滴
支持四大組件 只支持Activity 只支持Activity 只支持Activity 全支持 全支持
組件須要/不須要在宿主manifest中預註冊 不須要 須要 不須要 不須要 不須要
插件能夠/不能夠依賴宿主 能夠 能夠 能夠 不能夠 能夠
支持/不支持PendingIntent 不支持 不支持 不支持 支持 支持
Android特性支持 大部分 大部分 大部分 幾乎所有 幾乎所有
兼容性適配 通常 通常 中等
插件構建 部署aapt Gradle插件 Gradle插件

2 類加載機制

2.1 Android類加載機制

咱們知道Java的類加載採用雙親委託的加載機制,有效防止類的重複加載和保護核心類的加載。Android的加載機制與Java的類加載機制相似,但也有區別。Android中的各個類加載器之間的關係以下圖所示。組件化

Android類加載機制

在Android中,在APP運行時,全部的應用自有的包含字節碼文件(.class)的dex文件被包裝成一個 Element對象,放在了一個Element[] elements數組中,每個element元素對應一個dex文件,應用尋找特定某個類時,會從前日後依次遍歷該數組,直到找到或遍歷到尾。測試

本文講的簡單插件化實現就利用該Element數組實現:新建一個Element[] newElements數組,在新建的newElements數組中實現宿主的Element[] hostElements數組和插件Element[] pluginElements數組的拼接,並用新的Element數組替換宿主原有的數組。ui

QZone dex分包修復原理

2.2 尋找Element數組

當系統經過PathClassLoader去加載應用程序的dex文件中Java類時,PathClassLoader並無重寫loadClass()方法,因此接下來由PathClassLoader的父類BaseDexClassLoader嘗試執行加載任務,然而BaseDexClassLoader也沒有重寫loadClass()方法,則依次向上調用父類加載器的loadClass()方法,父類加載器(由最頂級父類加載器開始嘗試loadClass)在各自的加載範圍內嘗試加載須要的類,失敗以後依次向下調用子類加載器的findClass()方法(也就是雙親委託機制那一套)。最終會調用到BaseDexClassLoaderfindClass()方法。

// PathClassLoader 去掉註釋後的所有代碼,只有兩個構造函數
package dalvik.system;
public class PathClassLoader extends BaseDexClassLoader {
    public PathClassLoader(String dexPath, ClassLoader parent) {
        super(dexPath, null, null, parent);
    }
    public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
        super(dexPath, null, librarySearchPath, parent);
    }
}
複製代碼
public class BaseDexClassLoader extends ClassLoader {
    // ...
	private final DexPathList pathList;
	// ...
	@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;
	}
    // ...
}
複製代碼

在該方法內,程序會調用BaseDexClassLoader類中私有成員屬性DexPathList pathListfindClass()方法。

final class DexPathList {
    // ...
    private Element[] dexElements;
    // ...
    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;
    }
    // ...
}
複製代碼

在這段代碼中,用加強for循環從前日後去遍歷dexElements數組,該數組即爲上面圖片中的說起的Element[]數組

3 項目實例

3.1 項目結構

項目結構

3.2 生成插件APK

3.2.1 插件MainActivity類

插件中的MainActivity.java未額外處理,以下:

package com.tongbo.plugin;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}
複製代碼

3.2.2 插件Test類

Test類用於以後插件引入以後的測試,以下:

package com.tongbo.plugin;

import android.util.Log;

public class Test {
    public static void print() {
        Log.e("TB", "Test class from com.tongbo.plugin");
    }
}
複製代碼

3.2.3 ★生成插件APK並上傳★

  • 運行plugin模塊;
  • plugin\build\outputs\apk\debug中找到plugin-debug.apk,並上傳到模擬器的/sdcard/路徑。

上傳插件APK

3.3 宿主載入插件

3.3.1 宿主MainActivity類

  • onCreate()回調中,調用LoadUtil.loadClass(this)實現插件的加載(詳見3.3.2);

  • 在宿主的主頁面,添加一個按鈕,並在onClick屬性綁定toStartPlugin(View view)方法,並在方法中反射調用插件的類的方法Test.print()

package com.tongbo.pluginbasic;

import androidx.appcompat.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import java.lang.reflect.Method;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        LoadUtil.loadClass(this);
    }

    public void toStartPlugin(View view) {
        Log.e("TB", "btn is clicked.");
        try {
            Class<?> clazz = Class.forName("com.tongbo.plugin.Test");
            Method method = clazz.getMethod("print");
            method.invoke(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

3.3.2 宿主LoadUtil類

LoadUtil類中的loadClass(Context context)方法是核心的插件載入方法,主要分爲如下幾個步驟:

  • 第一步,反射獲得dalvik.system.BaseDexClassLoaderpathList字段以及dalvik.system.DexPathListdexElements字段並設置訪問權限;
  • 第二步,反射得到宿主的dexElements字段的值;
  • 第三步,反射得到插件的dexElements字段的值;
  • 第四步,利用Array.newInstance()方法建立新的Element[],在新建立的Element[]中利用System.arraycopy()方法完成數組的拼接;
  • 第五步,設置宿主的dexElements字段的值爲新建的Element[],完成插件加載。
package com.tongbo.pluginbasic;

import android.content.Context;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;

public class LoadUtil {

    private final static String pluginApkPath = "/sdcard/plugin-debug.apk";

    public static void loadClass(Context context) {

        try {
            //TODO:to get 'pathList' field and 'dexElements' field by reflection.
            //private final DexPathList pathList;
            Class<?> baseDexClassLoaderClass = Class.forName("dalvik.system.BaseDexClassLoader");
            Field pathListField = baseDexClassLoaderClass.getDeclaredField("pathList");
            pathListField.setAccessible(true);
            //private Element[] dexElements;
            Class<?> dexPathListClass = Class.forName("dalvik.system.DexPathList");
            Field dexElementsField = dexPathListClass.getDeclaredField("dexElements");
            dexElementsField.setAccessible(true);

            //TODO:to get the value of host's dexElements field.
            PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
            Object hostPathList = pathListField.get(pathClassLoader);
            Object[] hostDexElements = (Object[]) dexElementsField.get(hostPathList);

            //TODO:to get the value of plugin's dexElements field.
            DexClassLoader dexClassLoader = new DexClassLoader(
                    pluginApkPath,
                    context.getCacheDir().getAbsolutePath(),
                    null,
                    context.getClassLoader()
            );
            Object pluginPathList = pathListField.get(dexClassLoader);
            Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);

            //TODO:to add host's dexElements and plugin's dexElements together in a newly created Element array.
            Object[] dexElements = (Object[]) Array.newInstance(
                    pluginDexElements.getClass().getComponentType(),
                    hostDexElements.length + pluginDexElements.length
            );
            System.arraycopy(
                    hostDexElements,
                    0, dexElements,
                    0,
                    hostDexElements.length
            );
            System.arraycopy(
                    pluginDexElements,
                    0,
                    dexElements,
                    hostDexElements.length,
                    pluginDexElements.length
            );

            //TODO:to set the newly created Element array to the host's dexElements field and done.
            dexElementsField.set(hostPathList, dexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
複製代碼

3.4 測試

將宿主APP運行起來,點擊按鈕,控制檯將打印以下(成功):

測試
相關文章
相關標籤/搜索