點(diǎn)贊關(guān)注奔则,不再迷路,你的支持對(duì)我意義重大!
?? Hi佳遣,我是丑丑。本文「Android 路線」| 導(dǎo)讀 —— 從零到無窮大 已收錄凡伊。這里有 Android 進(jìn)階成長路線筆記 & 博客,歡迎跟著彭丑丑一起成長窒舟。(聯(lián)系方式在 GitHub)
前言
- 隨著應(yīng)用功能模塊的增多系忙,組件化和插件化的需求日益強(qiáng)烈;
- 在這篇文章里惠豺,我將分析 實(shí)現(xiàn)插件化的基本原理银还。如果能幫上忙风宁,請(qǐng)務(wù)必點(diǎn)贊加關(guān)注,這真的對(duì)我非常重要蛹疯。
目錄
前置知識(shí)
動(dòng)態(tài)代理: 「Java 路線」| 動(dòng)態(tài)代理 & 靜態(tài)代理
so 庫加載: 「NDK 路線」| so 庫加載到卸載的全過程
資源加載: 【點(diǎn)贊催更】
1. 類加載的委派模型
Java 類加載是一種委托機(jī)制(parent delegate)戒财,即:除了頂級(jí)啟動(dòng)類加載器(bootstrap classloader)之外,每個(gè)類加載器都有一個(gè)關(guān)聯(lián)的上級(jí)類加載器(parent 字段)捺弦。當(dāng)一個(gè)類加載器準(zhǔn)備執(zhí)行類加載時(shí)饮寞,它首先會(huì)委托給上級(jí)加載器去加載,而上級(jí)加載器可能還會(huì)繼續(xù)向上委托列吼,遞歸這個(gè)過程幽崩。如果上級(jí)構(gòu)造器無法加載,才會(huì)返回由自己加載寞钥。
更多內(nèi)容:類加載: Java 虛擬機(jī)| 類加載機(jī)制
2. Android 中的類加載器
在 Java 中慌申,JVM 加載的是 .class 文件,而在 Android 中理郑,Dalvik 和 ART 加載的是 dex 文件蹄溉。這里的 dex 文件不僅僅指 .dex 后綴的文件,而是指攜帶 classed.dex 項(xiàng)的任何文件(例如:jar / zip / apk)
這一節(jié)我們就來分析 Android ART 虛擬機(jī) 中的類加載器:
ClassLoader 實(shí)現(xiàn)類 | 作用 |
---|---|
BootClassLoader | 加載 SDK 中的類 |
PathClassLoader | 加載應(yīng)用程序的類 |
DexClassLoader | 加載指定的類 |
2.1 BootClassLoader
在 Java / Android 中您炉,BootClassLoader 是委托模型中的頂級(jí)加載器柒爵,作為委托鏈的最后一個(gè)成員,它總是最先嘗試加載類的邻吭。它是 ClassLoader 的非靜態(tài)內(nèi)部類餐弱,源碼如下:
ClassLoader.java
class BootClassLoader extends ClassLoader {
public static synchronized BootClassLoader getInstance() {
單例
}
public BootClassLoader() {
沒有上級(jí)類加載器
super(null);
}
@Override
protected Class<?> findClass(String name) {
注意 ClassLoader 參數(shù):傳遞 null
return Class.classForName(name, false, null);
}
@Override
protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException {
1、檢查是否加載過
Class<?> clazz = findLoadedClass(className);
2囱晴、嘗試加載
if (clazz == null) {
clazz = findClass(className);
}
return clazz;
}
Class.java
static native Class<?> classForName(String className, boolean shouldInitialize, ClassLoader classLoader) throws ClassNotFoundException;
要點(diǎn)如下:
- 1膏蚓、BootClassLoader 是單例的,一個(gè)進(jìn)程只會(huì)有一個(gè) BootClassLoader 對(duì)象畸写,并在 JVM 啟動(dòng)的時(shí)候啟動(dòng)驮瞧;
- 2、BootClassLoader 的 parent 字段為空枯芬,沒有上級(jí)類加載器(可以通過判斷一個(gè) ClassLoader#getParent() 是否來空來判斷是否為 BootClassLoader)论笔;
- 3、BootClassLoader#findClass()千所,最終調(diào)用 native 方法狂魔,我在 第 節(jié) 再說。
2.2 BaseDexClassLoader
在 Android 中淫痰,Java 代碼的編譯產(chǎn)物是 dex 格式字節(jié)碼最楷,所以 Android 系統(tǒng)提供了 BaseDexClassLoader 類加載器,用于從 dex 文件中加載類。
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
從 DexPathList 的路徑中加載類
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
throw new ClassNotFoundException(...);
}
return c;
}
添加 dex 路徑
public void addDexPath(String dexPath, boolean isTrusted) {
pathList.addDexPath(dexPath, isTrusted);
}
添加 so 動(dòng)態(tài)庫路徑
public void addNativePath(Collection<String> libPaths) {
pathList.addNativePath(libPaths);
}
}
可以看到籽孙,BaseDexClassLoader 將 findClass() 的任務(wù)委派給 DexPathList 對(duì)象處理烈评,這個(gè) DexPathList 指定了搜索類和 so 動(dòng)態(tài)庫的路徑。
2.3 PathClassLoader & DexClassLoader
從源碼可以看出犯建,PathClassLoader & DexClassLoader 其實(shí)都沒有重寫方法讲冠,所以主要的邏輯還是在 BaseDexClassLoader。
這兩個(gè)類其實(shí)只有一點(diǎn)不同适瓦,在 Android 9.0 之前竿开,DexClassLoader 的構(gòu)造方法需要傳入第二個(gè)參數(shù)optimizedDirectory
,這個(gè)路徑是存放優(yōu)化后的 dex 文件的路徑(odex)犹菇。
不過在 Android 9.0 之后德迹,DexClassLoader 也不需要傳這個(gè)參數(shù)了。
參數(shù) | 描述 |
---|---|
dexPath | 加載 dex 文件的路徑 |
optimizedDirectory | 加載 odex 文件的路徑 |
librarySearchPath | 加載 so 庫文件的路徑 |
parent | 上級(jí)類加載器 |
DexClassLoader.java - Android 8.0
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
DexClassLoader.java - Android 9.0
public DexClassLoader(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
3. DexPathList 源碼分析
在 第 2 節(jié)里揭芍,我們提到 BaseDexClassLoader 將 findClass() 的任務(wù)委派給 DexPathList 對(duì)象處理胳搞,這一節(jié)我們就來分析 DexPathList 里的處理過程。
DexFile 是 dex 文件在內(nèi)存中的映射
Elment - dexFile
Element[] dexElements 一個(gè)app所有的class文件都在 dexElements里
4. 插件化的基本流程
4.1 如何加載插件中的類称杨?
4.1.1 生成 dex 文件
- 1肌毅、將
dx.bat
文件添加到環(huán)境變量
sdk
├─ build-tools
├── 28.0.2
├── dx.bat
dx.bat
是用于生成 dex 文件的命令,將它添加到環(huán)境變量里使用起來會(huì)方便些姑原。
2悬而、javac 命令編譯 略
3、dx 命令生成 dex 文件
dx --dex --output=「輸出文件名.dex」 「com.xurui.test.class」
- 4锭汛、將 dex 文件放置在 sdcard(外部存儲(chǔ)) 略
4.1.2 使用 DexClassLoader 加載 dex 文件
DexClassLoader dexClassLoader = new DexClassLoader("/sdcard/test.dex",
context.getCacheDir().getAbsolutePath(),
null,
context.getClassLoader());
4.1.3 執(zhí)行類加載
try {
執(zhí)行類加載
Class<?> clazz = dexClassLoader.loadClass("com.xurui.test");
...
} catch (Exception e) {
e.printStackTrace();
}
4.2 加載插件的步驟
- 1笨奠、創(chuàng)建插件的 DexClassLoader 類加載器;
- 2唤殴、獲取宿主 App 的 PathClassLoader 類加載器般婆;
- 3、合并兩個(gè)類加載器中的 dexElements朵逝,生成新的 Element[]蔚袍;
- 4、通過反射將新值賦值給宿主的 dexElements 字段配名。
1啤咽、宿主類加載器
ClassLoader appClassLoader = context.getClassLoader();
宿主 DexPathList
Object appPathList = pathListField.get(appClassLoader);
宿主 dexElements
Object[] appDexElements = (Object[]) dexElementsField.get(appPathList);
2、插件加載器
ClassLoader pluginClassLoader = new DexClassLoader(apkPath,
context.getCacheDir().getAbsolutePath(),
null,
appClassLoader);
插件 DexPathList
Object pluginPathList = pathListField.get(pluginClassLoader);
插件 dexElements
Object[] pluginDexElements = (Object[]) dexElementsField.get(pluginPathList);
3渠脉、合并 dexElements
// Object[] obj = new Object[appDexElements.length + pluginDexElements.length]; // x
Object[] newElements = (Object[]) Array.newInstance(appDexElements.getClass().getComponentType(),
appDexElements.length + pluginDexElements.length);
System.arraycopy(appDexElements, 0, newElements, 0, appDexElements.length);
System.arraycopy(pluginDexElements, 0, newElements, 0, pluginDexElements.length);
4宇整、賦值
dexElementsField.set(appPathList, newElements);
5. 啟動(dòng)插件中的四大組件
5.1 矛盾
在 第 4 節(jié) 中,我們已經(jīng)成功實(shí)現(xiàn)了插件中類的加載芋膘。但是對(duì)于四大組件來說没陡,由于插件中的組件沒有在宿主AndroidManifest.xml
中注冊涩哟,即時(shí)完成了類加載,也無法啟動(dòng)盼玄。
5.2 解決策略
解決策略是使用一個(gè)代理 Activity 作為中轉(zhuǎn),實(shí)現(xiàn)偷天換日:
- 1潜腻、在宿主 App 中注冊「ProxyActivity」;
- 2埃儿、Hook AMS 中啟動(dòng) Activity 的流程,將 「啟動(dòng) PluginActivity」修改為「啟動(dòng) ProxyActivity」融涣;
- 3童番、Hook AMS
使用動(dòng)態(tài)代理和反射機(jī)制可以實(shí)現(xiàn) Hook,而在尋找 Hook 點(diǎn)時(shí)需要遵循以下原則:
- 1威鹿、盡量 Hook 靜態(tài)變量或單例變量(不容易被改變)剃斧;
- 2、盡量 Hook public 的對(duì)象和方法(影響范圍最泻瞿恪)幼东。
5.3 實(shí)現(xiàn)步驟
提示: 以下源碼基于 Android Q - API 26。
1科雳、注冊 ProxyActivity 略
2根蟹、Hook AMS
1、獲取 singleton 對(duì)象
Class<?> amsClazz = Class.forName("android.app.ActivityManager");
Field singletonField = amsClazz.getDeclaredField("IActivityManagerSingleton");
singletonField.setAccessible(true);
Object singleton = singletonField.get(null);
2糟秘、獲取 IActivityManager 對(duì)象
Class<?> singletonClazz = Class.forName("android.util.Singleton");
Field mInstanceField = singletonClazz.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
final Object mInstance = mInstanceField.get(singleton);
3简逮、動(dòng)態(tài)代理 IActivityManager
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
Object proxyInstance = Proxy.newProxyInstance(context.getClassLoader(),
new Class[]{iActivityManagerClazz},
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
5、修改 Intent尿赚,替換 PluginActivity 到 ProxyActivity
6散庶、不修改原有執(zhí)行流程
return method.invoke(mInstance, args);
}
});
4、反射修改字段
mInstanceField.set(singleton, proxyInstance);
public static final String EXTRA_TARGET_INTENT = "target_intent";
5凌净、修改 Intent悲龟,替換 PluginActivity 到 ProxyActivity
5.1 過濾
if ("startActivity".equals(method.getName())) {
int indexOfIntent = -1;
for (int index = 0; index < args.length; index++) {
if (args[index] instanceof Intent) {
indexOfIntent = index;
break;
}
}
5.2 啟動(dòng) PluginActivity 的Intent
Intent pluginIntent = (Intent) args[indexOfIntent];
5.3 啟動(dòng) ProxyActivity 的Intent
Intent proxyIntent = new Intent();
proxyIntent.setClassName("com.xurui", "com.xurui.ProxyActivity");
args[indexOfIntent] = proxyIntent;
5.4 保存原本的 intent
proxyIntent.putExtra(EXTRA_TARGET_INTENT, pluginIntent);
}
6、不修改原有執(zhí)行流程
return method.invoke(mInstance, args);
- 3泻蚊、Hook Handler
1躲舌、創(chuàng)建 Handler.callback
Handler.Callback callback = new Handler.Callback() {
@Override
public boolean handleMessage(@NonNull Message msg) {
// msg.obj == ActivityClientRecord
switch (msg.what) {
case 100: // LAUNCH_ACTIVITY
try {
Field intentField = msg.obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
1.1 獲取 proxyIntent
Intent proxyIntent = (Intent) intentField.get(msg.obj);
1.2 替換為 pluginIntent
Intent pluginIntent = proxyIntent.getParcelableExtra(EXTRA_TARGET_INTENT);
if (null != pluginIntent) {
intentField.set(msg.obj, pluginIntent);
}
} catch (Exception e) {
e.printStackTrace();
}
}
// 不改變原有流程
return false;
}
};
2、獲取 ActivityThread 對(duì)象
Class<?> clazz = Class.forName("android.app.ActivityThread");
Field activityThreadField = clazz.getDeclaredField("sCurrentActivityThread");
activityThreadField.setAccessible(true);
Object activityThread = activityThreadField.get(null);
3性雄、獲取 mH 對(duì)象
Field mHField = clazz.getDeclaredField("mH");
mHField.setAccessible(true);
final Handler mH = (Handler) mHField.get(activityThread);
4没卸、賦值
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);
mCallbackField.set(mH, callback);
- 4、版本適配
針對(duì)每個(gè)版本的源碼秒旋,需要分別對(duì) Hook 點(diǎn)進(jìn)行適配约计。
6. 加載插件中的資源
資源加載:asset / res
通過resource訪問,其實(shí)也是通過assetmanager去訪問
2020年12月27 暫停
7. 總結(jié)
創(chuàng)作不易迁筛,你的「三連」是丑丑最大的動(dòng)力煤蚌,我們下次見!