Android插件化系列第(四)篇---插件加載機(jī)制兩種方案

這篇博客說說插件的加載機(jī)制南蓬,建議閱讀Android插件化系列第(二)篇---動態(tài)加載技術(shù)之a(chǎn)pk換膚了解類的加載機(jī)制歇父。

一坏怪、相關(guān)概念

1.1、為什么需要動態(tài)加載

這個問題积糯,前面已經(jīng)介紹過掂墓,如下

Android系統(tǒng)使用了ClassLoader機(jī)制來進(jìn)行Activity等組件的加載;apk被安裝之后看成,APK文件的代碼以及資源會被系統(tǒng)存放在固定的目錄(比如/data/app/package_name/1.apk)系統(tǒng)在進(jìn)行類加載的時候君编,會自動去這一個或者幾個特定的路徑來尋找這個類;但是系統(tǒng)并不知道存在于插件中的Activity組件的信息川慌,插件可以是任意位置吃嘿,甚至是網(wǎng)絡(luò),系統(tǒng)無法提前預(yù)知梦重,因此正常情況下系統(tǒng)無法加載我們插件中的類兑燥;因此也沒有辦法創(chuàng)建Activity的對象,更不用談啟動組件了琴拧。這個時候就需要使用動態(tài)加載技術(shù)了艾蓝。

1.2于置、類的加載機(jī)制

對于android中的classloader是按照以下的流程话速,loadClass方法在加載一個類的實(shí)例的時候乳讥,會先查詢當(dāng)前ClassLoader實(shí)例是否加載過此類汹忠,有就返回赋焕;如果沒有僧界。查詢Parent是否已經(jīng)加載過此類涨共,如果已經(jīng)加載過扒吁,就直接返回Parent加載的類火鼻;如果繼承路線上的ClassLoader都沒有加載,才由Child執(zhí)行類的加載工作雕崩;這樣做的好處:首先是共享功能魁索,一些Framework層級的類一旦被頂層的ClassLoader加載過就緩存在內(nèi)存里面,以后任何地方用到都不需要重新加載盼铁。除此之外還有隔離功能粗蔚,不同繼承路線上的ClassLoader加載的類肯定不是同一個類,這樣的限制避免了用戶自己的代碼冒充核心類庫的類訪問核心類庫包可見成員的情況饶火。這也好理解支鸡,一些系統(tǒng)層級的類會在系統(tǒng)初始化的時候被加載冬念,比如java.lang.String,如果在一個應(yīng)用里面能夠簡單地用自定義的String類把這個系統(tǒng)的String類給替換掉牧挣,那將會有嚴(yán)重的安全問題

結(jié)論:
DexClassLoader可以加載jar/apk/dex急前,可以從SD卡中加載未安裝的apk;
PathClassLoader只能加載系統(tǒng)中已經(jīng)安裝過的apk瀑构;

現(xiàn)在介紹兩種插件在宿主中加載的兩種方案裆针。

二、動態(tài)加載方案

2.1寺晌、合并dexElements數(shù)組

這里的合并是指世吨,將PathClassLoader和DexClassLoader中的dexElements進(jìn)行合并,這種思路從何而來呢呻征?通常在Android中我們用上述兩個ClassLoader加載類耘婚,他們的父類是BaseDexClassLoader。在父類的構(gòu)造函數(shù)中創(chuàng)建了一個DexPathList對象陆赋,從名字看上去沐祷,估計這個類表示的是把很多個Dex文件的路徑放到一個List集合中。

BaseDexClassLoader.java

來看DexPathList的代碼

DexPathList.java


類在build之后就會變成一個dex文件攒岛,而這個文件的路徑就存放在dexElements赖临。所以自然就會想到,我們把宿主和插件的dex都放到這里面灾锯,這樣系統(tǒng)就會幫我們加載了兢榨。

 /**
     * 創(chuàng)建DexClassLoader,不能用DexClassLoader,因?yàn)镈exClassLoader只能加載安裝過的
     */
    public DexClassLoader createDexClassLoader(Activity pActivity) {
        String cachePath = pActivity.getCacheDir().getAbsolutePath();
        String apkPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/chajian_demo.apk";
        return new DexClassLoader(apkPath, cachePath, cachePath, getClassLoader());
    }
  */
    public static void injectClassLoader(DexClassLoader loader,Context context){
        //獲取宿主的ClassLoader
        PathClassLoader pathLoader = (PathClassLoader)context.getClassLoader();
        try {
            //獲取宿主pathList
            Object hostPathList = getPathList(pathLoader);
            //獲取插件pathList
            Object pluginPathList = getPathList(loader);
            //獲取宿主ClassLoader中的dex數(shù)組
            Object hostDexElements = getDexElements(hostPathList);
            //獲取插件CassLoader中的dex數(shù)組
            Object pluginDexElements = getDexElements(pluginPathList);
            //獲取合并后的pathList
            Object sumDexElements = combineArray(hostDexElements, pluginDexElements);
            //將合并的pathList設(shè)置到本應(yīng)用的ClassLoader
            setField(hostPathList, suZhuPathList.getClass(), "dexElements", sumDexElements);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

   private static Object getPathList(Object baseDexClassLoader)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException, ClassNotFoundException {
            return getField(baseDexClassLoader, Class.forName("dalvik.system.BaseDexClassLoader"), "pathList");
    }
    private static Object getDexElements(Object paramObject)
            throws IllegalArgumentException, NoSuchFieldException, IllegalAccessException {
        return getField(paramObject, paramObject.getClass(), "dexElements");
    }
   private static Object getField(Object obj, Class<?> cl, String field)
            throws NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
        //反射需要獲取的字段
        Field localField = cl.getDeclaredField(field);
        localField.setAccessible(true);
        return localField.get(obj);
    }

上面的代碼演示了怎么合并系統(tǒng)默認(rèn)加載器PathClassLoader和動態(tài)加載器DexClassLoader中的dexElements數(shù)組,這種方案還是比較簡單的顺饮,現(xiàn)在看麻煩一些的吵聪。

2.1、替換LoadedApk中的mClassLoader

LoadedApk是什么兼雄? LoadedApk對象是APK文件在內(nèi)存中的表示吟逝。 Apk文件的相關(guān)信息,諸如Apk文件的代碼和資源君旦,甚至代碼里面的Activity,Service等組件的信息我們都可以通過此對象獲取嘲碱。

Paste_Image.png

為什么想到要替換LoadedApk中的mClassLoader金砍,這個答案也是看源碼的,在Activity的啟動過程中麦锯,會獲取LoadedApk對象恕稠。

    public final LoadedApk getPackageInfo(String packageName, CompatibilityInfo compatInfo,
            int flags, int userId) {
        final boolean differentUser = (UserHandle.myUserId() != userId);
        synchronized (mResourcesManager) {
            WeakReference<LoadedApk> ref;
            if (differentUser) {
                ref = null;
            } else if ((flags & Context.CONTEXT_INCLUDE_CODE) != 0) {
                ref = mPackages.get(packageName);
            } else {
                ref = mResourcePackages.get(packageName);
            }

            LoadedApk packageInfo = ref != null ? ref.get() : null;
           
            if (packageInfo != null && (packageInfo.mResources == null
                    || packageInfo.mResources.getAssets().isUpToDate())) {
                if (packageInfo.isSecurityViolation()
                        && (flags&Context.CONTEXT_IGNORE_SECURITY) == 0) {
                    throw new SecurityException(
                            "Requesting code from " + packageName
                            + " to be run in process "
                            + mBoundApplication.processName
                            + "/" + mBoundApplication.appInfo.uid);
                }
                return packageInfo;
            }
        }

首先判斷了是不是同一個userId,如果是同一個user扶欣,嘗試獲取緩存數(shù)據(jù)鹅巍;如果沒有命中緩存數(shù)據(jù)千扶,才通過LoadedApk的構(gòu)造函數(shù)創(chuàng)建了LoadedApk對象;因此當(dāng)我們拿到這一份緩存數(shù)據(jù)骆捧,修改里面的ClassLoader澎羞,自己控制類加載的過程,這樣加載插件中的Activity類的問題就解決了敛苇。

public class HookLoadedApk {

    public static Map<String, Object> sLoadedApk = new HashMap<String, Object>();

    public static void hookLoadedApkInActivityThread(File apkFile) throws Exception {

        // 1妆绞、獲取到當(dāng)前的ActivityThread對象
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
        currentActivityThreadMethod.setAccessible(true);
        Object currentActivityThread = currentActivityThreadMethod.invoke(null);

        //2、 獲取 mPackages 靜態(tài)成員變量, 這里緩存了dex包的信息
        Field mPackagesField = activityThreadClass.getDeclaredField("mPackages");
        mPackagesField.setAccessible(true);
        Map mPackages = (Map) mPackagesField.get(currentActivityThread);

        // 方法簽名:public final LoadedApk getPackageInfoNoCheck(ApplicationInfo ai,CompatibilityInfo compatInfo)
        //3枫攀、獲取getPackageInfoNoCheck方法
        Class<?> compatibilityInfoClass = Class.forName("android.content.res.CompatibilityInfo");
        Method getPackageInfoNoCheckMethod = activityThreadClass.getDeclaredMethod("getPackageInfoNoCheck",
                ApplicationInfo.class, compatibilityInfoClass);

        Field defaultCompatibilityInfoField = compatibilityInfoClass.getDeclaredField("DEFAULT_COMPATIBILITY_INFO");
        defaultCompatibilityInfoField.setAccessible(true);

        Object defaultCompatibilityInfo = defaultCompatibilityInfoField.get(null);

        //4括饶、獲取applicationInfo信息
        ApplicationInfo applicationInfo = generateApplicationInfo(apkFile);

        Object loadedApk = getPackageInfoNoCheckMethod.invoke(currentActivityThread, applicationInfo, defaultCompatibilityInfo);

        String odexPath = Utils.getPluginOptDexDir(applicationInfo.packageName).getPath();

        String libDir = Utils.getPluginLibDir(applicationInfo.packageName).getPath();

        //5、創(chuàng)建DexClassLoader
        ClassLoader classLoader = new DexClassLoader(apkFile.getPath(), odexPath, libDir, ClassLoader.getSystemClassLoader());
        Field mClassLoaderField = loadedApk.getClass().getDeclaredField("mClassLoader");
        mClassLoaderField.setAccessible(true);

        //6来涨、替換掉loadedApk
        mClassLoaderField.set(loadedApk, classLoader);

        // 由于是弱引用, 為了防止被GC图焰,我們必須在某個地方存一份
        sLoadedApk.put(applicationInfo.packageName, loadedApk);

        WeakReference weakReference = new WeakReference(loadedApk);

        mPackages.put(applicationInfo.packageName, weakReference);
    }


    /**
     * 反射generateApplicationInfo方法,得到ApplicationInfo對象
     *
     * generateApplicationInfo方法簽名:
     * public static ApplicationInfo generateApplicationInfo(Package p, int flags,PackageUserState state, int userId) 
     * 
     * 這個方法需要Package參數(shù)和PackageUserState參數(shù)
     * 
     * 
     */
    public static ApplicationInfo generateApplicationInfo(File apkFile) throws Exception{

        // 獲取PackageParser類
        Class<?> packageParserClass = Class.forName("android.content.pm.PackageParser");
       // 獲取PackageParser$Package類
        Class<?> packageParser$PackageClass = Class.forName("android.content.pm.PackageParser$Package");
        Class<?> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
       
        Method generateApplicationInfoMethod = packageParserClass.getDeclaredMethod("generateApplicationInfo",
                packageParser$PackageClass,
                int.class,
                packageUserStateClass);
        
        // 創(chuàng)建出一個PackageParser對象供使用
        Object packageParser = packageParserClass.newInstance();
      
        // 調(diào)用 PackageParser.parsePackage 解析apk的信息
        Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);

        // 得到第一個參數(shù) :PackageParser.Package 對象
        Object packageObj = parsePackageMethod.invoke(packageParser, apkFile, 0);

        //得到第三個參數(shù):PackageUserState對象
        Object defaultPackageUserState = packageUserStateClass.newInstance();

        // 反射generateApplicationInfo得到ApplicationInfo對象
        ApplicationInfo applicationInfo = (ApplicationInfo) generateApplicationInfoMethod.invoke(packageParser,
                packageObj, 0, defaultPackageUserState);
        String apkPath = apkFile.getPath();

        applicationInfo.sourceDir = apkPath;
        applicationInfo.publicSourceDir = apkPath;

        return applicationInfo;
    }
}

這種方法參考了weishu蹦掐,比較復(fù)雜技羔,因?yàn)锳ctivityThread對于LoadedApk有緩存機(jī)制,我們才有機(jī)可乘笤闯,把自定義的ClassLoader的插件信息添加進(jìn)mPackages中堕阔,從而完成了插件的加載。關(guān)于這兩種方案颗味,不能說哪一種更好超陆,雖然第一種方案易理解,代碼少浦马,但是有一個問題时呀,一旦插件之間甚至插件與宿主之間使用的類庫有沖突,就會崩潰晶默,DroidPlugin采用的就是第二種方案谨娜,Small采用的是第一種方案,合并dexElements數(shù)組磺陡。第二種方案也有缺點(diǎn)趴梢,除了Hook過程復(fù)雜外,每一個版本的apk解析都有差別币他,使用的PackageParser的兼容性就比較差坞靶,根據(jù)不同版本來分別Hook。詳細(xì)的可以參考weishu蝴悉,解釋的比我更清楚彰阴。

Please accept mybest wishes for your happiness and success !

參考博客

http://weishu.me/2016/04/05/understand-plugin-framework-classloader/

http://blog.csdn.net/yulong0809/article/details/59113935

http://www.wjdiankong.cn/android%E4%B8%AD%E6%8F%92%E4%BB%B6%E5%BC%80%E5%8F%91%E7%AF%87%E4%B9%8B-%E5%8A%A8%E6%80%81%E5%8A%A0%E8%BD%BDactivity%E5%85%8D%E5%AE%89%E8%A3%85%E8%BF%90%E8%A1%8C%E7%A8%8B%E5%BA%8F/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市拍冠,隨后出現(xiàn)的幾起案子尿这,更是在濱河造成了極大的恐慌簇抵,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,122評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件射众,死亡現(xiàn)場離奇詭異碟摆,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)责球,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,070評論 3 395
  • 文/潘曉璐 我一進(jìn)店門焦履,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人雏逾,你說我怎么就攤上這事嘉裤。” “怎么了栖博?”我有些...
    開封第一講書人閱讀 164,491評論 0 354
  • 文/不壞的土叔 我叫張陵屑宠,是天一觀的道長。 經(jīng)常有香客問我仇让,道長典奉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,636評論 1 293
  • 正文 為了忘掉前任丧叽,我火速辦了婚禮卫玖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘踊淳。我一直安慰自己假瞬,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,676評論 6 392
  • 文/花漫 我一把揭開白布迂尝。 她就那樣靜靜地躺著脱茉,像睡著了一般。 火紅的嫁衣襯著肌膚如雪垄开。 梳的紋絲不亂的頭發(fā)上琴许,一...
    開封第一講書人閱讀 51,541評論 1 305
  • 那天,我揣著相機(jī)與錄音溉躲,去河邊找鬼榜田。 笑死,一個胖子當(dāng)著我的面吹牛锻梳,可吹牛的內(nèi)容都是我干的箭券。 我是一名探鬼主播,決...
    沈念sama閱讀 40,292評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼唱蒸,長吁一口氣:“原來是場噩夢啊……” “哼邦鲫!你這毒婦竟也來了灸叼?” 一聲冷哼從身側(cè)響起神汹,我...
    開封第一講書人閱讀 39,211評論 0 276
  • 序言:老撾萬榮一對情侶失蹤庆捺,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后屁魏,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體滔以,經(jīng)...
    沈念sama閱讀 45,655評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,846評論 3 336
  • 正文 我和宋清朗相戀三年氓拼,在試婚紗的時候發(fā)現(xiàn)自己被綠了你画。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,965評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡桃漾,死狀恐怖坏匪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情撬统,我是刑警寧澤适滓,帶...
    沈念sama閱讀 35,684評論 5 347
  • 正文 年R本政府宣布,位于F島的核電站恋追,受9級特大地震影響凭迹,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜苦囱,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,295評論 3 329
  • 文/蒙蒙 一嗅绸、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧撕彤,春花似錦鱼鸠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,894評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至睦裳,卻和暖如春造锅,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背廉邑。 一陣腳步聲響...
    開封第一講書人閱讀 33,012評論 1 269
  • 我被黑心中介騙來泰國打工哥蔚, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人蛛蒙。 一個月前我還...
    沈念sama閱讀 48,126評論 3 370
  • 正文 我出身青樓糙箍,卻偏偏與公主長得像,于是被迫代替她去往敵國和親牵祟。 傳聞我的和親對象是個殘疾皇子深夯,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,914評論 2 355

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