Android | 類加載器與插件化

點(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í)


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 文件中加載類。

BaseDexClassLoader.java

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);
}

PathClassLoader.java

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)力煤蚌,我們下次見!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市尉桩,隨后出現(xiàn)的幾起案子筒占,更是在濱河造成了極大的恐慌,老刑警劉巖蜘犁,帶你破解...
    沈念sama閱讀 212,383評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翰苫,死亡現(xiàn)場離奇詭異,居然都是意外死亡这橙,警方通過查閱死者的電腦和手機(jī)奏窑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,522評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來屈扎,“玉大人埃唯,你說我怎么就攤上這事∮コ浚” “怎么了墨叛?”我有些...
    開封第一講書人閱讀 157,852評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長并村。 經(jīng)常有香客問我巍实,道長,這世上最難降的妖魔是什么哩牍? 我笑而不...
    開封第一講書人閱讀 56,621評(píng)論 1 284
  • 正文 為了忘掉前任棚潦,我火速辦了婚禮,結(jié)果婚禮上膝昆,老公的妹妹穿的比我還像新娘丸边。我一直安慰自己,他們只是感情好荚孵,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,741評(píng)論 6 386
  • 文/花漫 我一把揭開白布妹窖。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上揪罕,一...
    開封第一講書人閱讀 49,929評(píng)論 1 290
  • 那天郊愧,我揣著相機(jī)與錄音玷犹,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的嫉沽。 我是一名探鬼主播,決...
    沈念sama閱讀 39,076評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼俏竞,長吁一口氣:“原來是場噩夢啊……” “哼绸硕!你這毒婦竟也來了堂竟?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,803評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤玻佩,失蹤者是張志新(化名)和其女友劉穎出嘹,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體咬崔,經(jīng)...
    沈念sama閱讀 44,265評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡疚漆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,582評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了刁赦。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,716評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闻镶,死狀恐怖甚脉,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情铆农,我是刑警寧澤牺氨,帶...
    沈念sama閱讀 34,395評(píng)論 4 333
  • 正文 年R本政府宣布,位于F島的核電站墩剖,受9級(jí)特大地震影響猴凹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜岭皂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,039評(píng)論 3 316
  • 文/蒙蒙 一郊霎、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧爷绘,春花似錦书劝、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,798評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至陶因,卻和暖如春骡苞,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背楷扬。 一陣腳步聲響...
    開封第一講書人閱讀 32,027評(píng)論 1 266
  • 我被黑心中介騙來泰國打工解幽, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人毅否。 一個(gè)月前我還...
    沈念sama閱讀 46,488評(píng)論 2 361
  • 正文 我出身青樓亚铁,卻偏偏與公主長得像,于是被迫代替她去往敵國和親螟加。 傳聞我的和親對(duì)象是個(gè)殘疾皇子徘溢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,612評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容

  • 上篇文章講到了apk的分包吞琐,通過multidex構(gòu)建出包含多個(gè)dex文件的apk,從而解決65536的方法數(shù)限制問...
    the_q閱讀 8,140評(píng)論 1 15
  • 插件化概述 ??插件化技術(shù)最初源于免安裝運(yùn)行apk的想法然爆,這個(gè)免安裝的apk可以理解為插件站粟。支持插件化的app可以...
    曾想念_fce1閱讀 1,671評(píng)論 1 2
  • NDK中so的加載分析 libcore/ojluni/src/main/java/java/lang/System...
    約你一起偷西瓜閱讀 1,339評(píng)論 1 1
  • Android插件化原理探究 一、簡介 android動(dòng)態(tài)加載插件機(jī)制一直以來就是探索的熱門領(lǐng)域曾雕,各種動(dòng)態(tài)加載框架...
    寒瀟2018閱讀 821評(píng)論 0 2
  • 久違的晴天奴烙,家長會(huì)。 家長大會(huì)開好到教室時(shí)剖张,離放學(xué)已經(jīng)沒多少時(shí)間了切诀。班主任說已經(jīng)安排了三個(gè)家長分享經(jīng)驗(yàn)。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,515評(píng)論 16 22