插件化資源加載

插件化框架實(shí)現(xiàn):基于kotlin的插件化框架

Android 資源訪問(wèn)

AndroidResource.png
  • android通過(guò)aapt將資源編譯成R.java索引席噩,在代碼中通過(guò)R文件來(lái)引用具體資源
  • 在構(gòu)建成apk時(shí)通過(guò)對(duì)應(yīng)資源和其R文件的id生成resource.arsc跷叉,在app啟動(dòng)時(shí)會(huì)通過(guò)AssetManager的addAssetPath()方法添加系統(tǒng)資源和apk資源者铜,并構(gòu)造Resource提供給Context上下文進(jìn)行使用阳柔,所以真正加載資源是通過(guò)AssetManger去加載

AssetManager的創(chuàng)建時(shí)機(jī)

  • 從Context的getResource()跟蹤到ContextImpl構(gòu)造器:
private ContextImpl(ContextImpl container, ActivityThread mainThread,
          LoadedApk packageInfo, IBinder activityToken, UserHandle user, boolean restricted,
          Display display, Configuration overrideConfiguration) {

    // ...

    mResourcesManager = ResourcesManager.getInstance();

    // ...

    Resources resources = packageInfo.getResources(mainThread);
    if (resources != null) {
       if (activityToken != null
               || displayId != Display.DEFAULT_DISPLAY
               || overrideConfiguration != null
               || (compatInfo != null && compatInfo.applicationScale
                       != resources.getCompatibilityInfo().applicationScale)) {
           resources = mResourcesManager.getTopLevelResources(packageInfo.getResDir(),
                   packageInfo.getSplitResDirs(), packageInfo.getOverlayDirs(),
                   packageInfo.getApplicationInfo().sharedLibraryFiles, displayId,
                   overrideConfiguration, compatInfo, activityToken);
       }
    }
    mResources = resources;
}
  • 根據(jù)代碼追蹤到ResourcesManager的getTopLevelResources()方法:
public Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {
  
        ResourcesKey key = new ResourcesKey(resDir, displayId, overrideConfiguration, scale, token);
        Resources r;
        synchronized (this) {
            // Resources is app scale dependent.
            if (false) {
                Slog.w(TAG, "getTopLevelResources: " + resDir + " / " + scale);
            }
            WeakReference<Resources> wr = mActiveResources.get(key);
            r = wr != null ? wr.get() : null;
             //if (r != null) Slog.i(TAG, "isUpToDate " + resDir + ": " + r.getAssets().isUpToDate());
             if (r != null && r.getAssets().isUpToDate()) {
                 if (false) {
                     Slog.w(TAG, "Returning cached resources " + r + " " + resDir
                             + ": appScale=" + r.getCompatibilityInfo().applicationScale);
                 }
                 return r;
             }
         }

        // ... 

        // 創(chuàng)建AssertManager
        AssetManager assets = new AssetManager();

        // ... 
        // assets.addAssetPath(resDir)
            
        r = new Resources(assets, dm, config, compatInfo, token);
        mActiveResources.put(key, new WeakReference<Resources>(r));
        return r;
}
  • 在這里可以知道AssetManager是在第一次new ContextImpl時(shí)創(chuàng)建的势木,并構(gòu)造并緩存Resource提供給后續(xù)Context

Instant Run 資源替換部分

  • Part 1. 創(chuàng)建一個(gè)新的 AssetManager,并通過(guò)反射調(diào)用 addAssetPath 添加 /sdcard 上的新資源包屏歹。
AssetManager newAssetManager = AssetManager.class.getConstructor().newInstance();
Method mAddAssetPath = AssetManager.class.getDeclaredMethod("addAssetPath", String.class);
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager,externalResourceFile)) == 0) {
    throw new IllegalStateException("Could not create newAssetManager");
}
  • Part 2. 反射得到 Activities 中 AssetManager 的引用處,全部換成剛才新構(gòu)建的newAssetManager
for (Activity activity : activities) {
    Resources resources = activity.getResources();
    try {
        Field mAssets = Resources.class.getDeclaredField("mAssets");
        mAssets.setAccessible(true);
        mAssets.set(resources, newAssetManager);
    } catch (Throwable ignore) {
        Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
        mResourcesImpl.setAccessible(true);
        Object resourceImpl = mResourcesImpl.get(resources);
        Field implAssets = resourceImpl.getClass().
        getDeclaredField("mAssets");
        implAssets.setAccessible(true);
        implAssets.set(resourceImpl, newAssetManager);
    }
}
  • Part 3. 得到 Resources 的弱引用集合圃庭,把他們的 AssetManager 成員替換成newAssetManager
Collection<WeakReference<Resources>> references;
if (SDK_INT >= KITKAT) {
    // Find the singleton instance of ResourcesManager
    Class<?> resourcesManagerClass = Class.forName("android.app.ResourcesManager");
    Method mGetInstance = resourcesManagerClass.getDeclaredMethod("getInstance");
    mGetInstance.setAccessible(true);
    Object resourcesManager = mGetInstance.invoke(null);
    try {
        Field fMActiveResources = resourcesManagerClass.getDeclaredField("mActiveResources");
        fMActiveResources.setAccessible(true);
        @SuppressWarnings("unchecked")
        ArrayMap<?, WeakReference<Resources>> arrayMap = (ArrayMap<?, WeakReference<Resources>>)fMActiveResources.get(resourcesManager);
        references = arrayMap.values();
    } catch (NoSuchFieldException ignore) {
        Field mResourceReferences = resourcesManagerClass.getDeclaredField("mResourceReferences");
        mResourceReferences.setAccessible(true);
        //noinspection unchecked
        references = (Collection<WeakReference<Resources>>)
        mResourceReferences.get(resourcesManager);
    }
} else {
    Class<?> activityThread = Class.forName("android.app.ActivityThread");
    Field fMActiveResources = activityThread.getDeclaredField("mActiveResources");
    fMActiveResources.setAccessible(true);
    Object thread = getActivityThread(context, activityThread);
    @SuppressWarnings("unchecked")
    HashMap<?, WeakReference<Resources>> map = (HashMap<?, WeakReference<Resources>>) fMActiveResources.get(thread);
    references = map.values();
}

for (WeakReference<Resources> wr : references) {
    Resources resources = wr.get();
    if (resources != null) {
        // Set the AssetManager of the Resources instance to our brand new one
        try {
            Field mAssets = Resources.class.getDeclaredField("mAssets");
            mAssets.setAccessible(true);
            mAssets.set(resources, newAssetManager);
        } catch (Throwable ignore) {
            Field mResourcesImpl = Resources.class.getDeclaredField("mResourcesImpl");
            mResourcesImpl.setAccessible(true);
            Object resourceImpl = mResourcesImpl.get(resources);
            Field implAssets = resourceImpl.getClass().getDeclaredField("mAssets");
            implAssets.setAccessible(true);
            implAssets.set(resourceImpl, newAssetManager);
        }
        resources.updateConfiguration(resources.getConfiguration(),
        resources.getDisplayMetrics());
    }
}

AssetManager版本差異

根據(jù)阿里發(fā)布的《深入探索Android熱修復(fù)技術(shù)原理6.29》p114 - 117的描述以及源碼分析可以知道

  • Android 4.4 及以下版本,addAssetPath 只是把補(bǔ)丁包的路徑添加到了 mAssetPath 中失晴,而真正解析的資源包的邏輯是在 app 第一次執(zhí)行 AssetManager::getResTable的時(shí)候剧腻。
  • 所以,像 Instant Run 這種方案涂屁,一定需要一個(gè)全新的 AssetManager 時(shí)书在,然后再加入完整的新資源包,替換掉原有的 AssetManager拆又。

資源id沖突 問(wèn)題

  • 知道AssetManager的構(gòu)造以及怎么加載插件資源儒旬,還有個(gè)問(wèn)題就是資源id沖突問(wèn)題栏账,導(dǎo)致資源獲取錯(cuò)誤問(wèn)題

解決方案

  • 修改aapt
  • 修改resource.arsc
  • 宿主插件之間資源隔離

aapt

  • 根據(jù)插件包的唯一id作為資源的package id來(lái)修改aapt生成資源的id,來(lái)保證資源id不沖突
  • 但是這種方案痛點(diǎn)是編譯版本的改變的話aapt也需要進(jìn)行修改定制

修改resource.arsc

  • 根據(jù)插件包的唯一id作為package id來(lái)修改插件包打包后的resource.arsc的package id值

宿主插件之間資源隔離

  • 通過(guò)單獨(dú)構(gòu)造宿主的AssetManager和Resource來(lái)對(duì)宿主資源的加載栈源,但這種方式宿主插件間無(wú)法訪問(wèn)資源

資源id沖突的思考

在解決資源id沖突這方面挡爵,我更傾向于宿主插件之間資源隔離,從以下幾方面考慮

  • 首先各大廠商都對(duì)系統(tǒng)做過(guò)定制甚垦,需要做不同平臺(tái)的設(shè)配
  • 宿主插件都擁有各自的Resource茶鹃,不必采用像Instant Run方式實(shí)現(xiàn),能更好兼容后續(xù)Android更高版本
  • 通過(guò)資源隔離艰亮,插件包可以使一個(gè)單獨(dú)的apk闭翩,可以直接運(yùn)行安裝

Android 7.0 WebView

  • Android 7.0創(chuàng)建WebView的時(shí)候會(huì)重新創(chuàng)建AssetManager
  • 目前的解決辦法可以是加載插件之前先創(chuàng)建WebView來(lái)避免

  • Instant Run部分來(lái)自《深入探索Android熱修復(fù)技術(shù)原理6.29》
  • AssetManager版本差異部分來(lái)自根據(jù)阿里發(fā)布的《深入探索Android熱修復(fù)技術(shù)原理6.29》p114 - 117
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市迄埃,隨后出現(xiàn)的幾起案子疗韵,更是在濱河造成了極大的恐慌,老刑警劉巖调俘,帶你破解...
    沈念sama閱讀 222,104評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件伶棒,死亡現(xiàn)場(chǎng)離奇詭異旺垒,居然都是意外死亡彩库,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)先蒋,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)骇钦,“玉大人,你說(shuō)我怎么就攤上這事竞漾∶写睿” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,697評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵业岁,是天一觀的道長(zhǎng)鳞仙。 經(jīng)常有香客問(wèn)我,道長(zhǎng)笔时,這世上最難降的妖魔是什么棍好? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,836評(píng)論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮允耿,結(jié)果婚禮上借笙,老公的妹妹穿的比我還像新娘。我一直安慰自己较锡,他們只是感情好业稼,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,851評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著蚂蕴,像睡著了一般低散。 火紅的嫁衣襯著肌膚如雪俯邓。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 52,441評(píng)論 1 310
  • 那天谦纱,我揣著相機(jī)與錄音看成,去河邊找鬼。 笑死跨嘉,一個(gè)胖子當(dāng)著我的面吹牛川慌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播祠乃,決...
    沈念sama閱讀 40,992評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼梦重,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了亮瓷?” 一聲冷哼從身側(cè)響起琴拧,我...
    開(kāi)封第一講書(shū)人閱讀 39,899評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎嘱支,沒(méi)想到半個(gè)月后蚓胸,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評(píng)論 1 318
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡除师,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,529評(píng)論 3 341
  • 正文 我和宋清朗相戀三年沛膳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片汛聚。...
    茶點(diǎn)故事閱讀 40,664評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锹安,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出倚舀,到底是詐尸還是另有隱情叹哭,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評(píng)論 5 350
  • 正文 年R本政府宣布痕貌,位于F島的核電站风罩,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏舵稠。R本人自食惡果不足惜超升,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,025評(píng)論 3 334
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柱查。 院中可真熱鬧廓俭,春花似錦、人聲如沸唉工。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,511評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)淋硝。三九已至雹熬,卻和暖如春宽菜,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背竿报。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,611評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工铅乡, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人烈菌。 一個(gè)月前我還...
    沈念sama閱讀 49,081評(píng)論 3 377
  • 正文 我出身青樓阵幸,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親芽世。 傳聞我的和親對(duì)象是個(gè)殘疾皇子挚赊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,675評(píng)論 2 359

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

  • 插件化-資源處理 寫(xiě)的比較長(zhǎng),可以選擇跳過(guò)前面2節(jié)济瓢,直接從0x03實(shí)例分析開(kāi)始荠割。如有錯(cuò)誤,請(qǐng)不吝指正旺矾。 0x00 ...
    唐一川閱讀 5,345評(píng)論 2 22
  • 前言 資源蔑鹦,是APK包體積過(guò)大的病因之一。插件化技術(shù)將模塊解耦箕宙,通過(guò)插件的形式加載嚎朽。插件化技術(shù)中,每個(gè)插件都能夠作...
    oceanLong閱讀 4,927評(píng)論 7 8
  • Android插件化基礎(chǔ)的主要內(nèi)容包括 Android插件化基礎(chǔ)1-----加載SD上APKAndroid插件化基...
    隔壁老李頭閱讀 7,121評(píng)論 13 48
  • 是時(shí)候來(lái)一波Android插件化了 是時(shí)候來(lái)一波Android插件化了前言Android開(kāi)發(fā)演進(jìn)模塊化介紹插件化介...
    流水不腐小夏閱讀 4,794評(píng)論 3 51
  • 不害羞的說(shuō)一句,小時(shí)候的夢(mèng)想是當(dāng)作家融撞。 這個(gè)夢(mèng)盼铁,存在心里二十多年。直到近兩年越來(lái)越多網(wǎng)絡(luò)小說(shuō)改編的電視劇出現(xiàn)在大眾...
    蘇卷閱讀 246評(píng)論 0 2