Android資源的插件化

資源的查找過程

在android中查找資源分為以下兩種方式:

  • ContextImpl#getResource()#getxxx(R.xx.yy)
  • AssetManager#open()
    我們以android.content.res.Resources#getLayout為例
public XmlResourceParser getLayout(@LayoutRes int id) throws NotFoundException {
        return loadXmlResourceParser(id, "layout");
    }
XmlResourceParser loadXmlResourceParser(@AnyRes int id, @NonNull String type)
            throws NotFoundException {
        final TypedValue value = obtainTempTypedValue();
        try {
            final ResourcesImpl impl = mResourcesImpl;
            impl.getValue(id, value, true);
            if (value.type == TypedValue.TYPE_STRING) {
                return impl.loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);
            }
            throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id)
                    + " type #0x" + Integer.toHexString(value.type) + " is not valid");
        } finally {
            releaseTempTypedValue(value);
        }
    }
    void getValue(@AnyRes int id, TypedValue outValue, boolean resolveRefs)
            throws NotFoundException {
        boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
        if (found) {
            return;
        }
        throw new NotFoundException("Resource ID #0x" + Integer.toHexString(id));
    }
    boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
            boolean resolveRefs) {
        Preconditions.checkNotNull(outValue, "outValue");
        synchronized (this) {
            ensureValidLocked();
            final int cookie = nativeGetResourceValue(
                    mObject, resId, (short) densityDpi, outValue, resolveRefs);
            if (cookie <= 0) {
                return false;
            }

            // Convert the changing configurations flags populated by native code.
            outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
                    outValue.changingConfigurations);

            if (outValue.type == TypedValue.TYPE_STRING) {
                outValue.string = mApkAssets[cookie - 1].getStringFromPool(outValue.data);
            }
            return true;
        }
    }

再看下AssetManager#open方法的調(diào)用鏈

    public @NonNull InputStream open(@NonNull String fileName, int accessMode) throws IOException {
        Preconditions.checkNotNull(fileName, "fileName");
        synchronized (this) {
            ensureOpenLocked();
            final long asset = nativeOpenAsset(mObject, fileName, accessMode);
            if (asset == 0) {
                throw new FileNotFoundException("Asset file: " + fileName);
            }
            final AssetInputStream assetInputStream = new AssetInputStream(asset);
            incRefsLocked(assetInputStream.hashCode());
            return assetInputStream;
        }
    }

結(jié)論:

  1. 通過id獲取資源先后要經(jīng)過ContextImpl->Resource->ResourceImpl->AssetManager將id傳到native方法中囤耳,拿這個id通過arsc映射找到對應(yīng)的資源信息决摧,保存在TypedValue對象中返回种呐。
  2. 通過AssetManager獲取資源則是通過AssetManager的native方法直接去找assets目錄下對應(yīng)文件盗温。

Resource與AssetManager的生成時機

通過上述分析魄幕,我們知道了所有的資源最終都要通過AssetManager到對應(yīng)apk路徑下去訪問负间,那么 apk路徑是如何添加到AssetManager中的偶妖? 我們不妨正向分析一波,找到Resource與AssetManager的生成時機政溃。
ContextImpl.java

void setResources(Resources r) {
        if (r instanceof CompatResources) {
            ((CompatResources) r).setContext(this);
        }
        mResources = r;
    }

找到調(diào)用setResources方法的地方趾访,如

private static Resources createResources(IBinder activityToken, LoadedApk pi, String splitName,
            int displayId, Configuration overrideConfig, CompatibilityInfo compatInfo) {
        final String[] splitResDirs;
        final ClassLoader classLoader;
        try {
            splitResDirs = pi.getSplitPaths(splitName);
            classLoader = pi.getSplitClassLoader(splitName);
        } catch (NameNotFoundException e) {
            throw new RuntimeException(e);
        }
        return ResourcesManager.getInstance().getResources(activityToken,
                pi.getResDir(),
                splitResDirs,
                pi.getOverlayDirs(),
                pi.getApplicationInfo().sharedLibraryFiles,
                displayId,
                overrideConfig,
                compatInfo,
                classLoader);
    }

我們發(fā)現(xiàn)Resource對象最后都是通過ResourcesManager.getInstance().getResources方法生成的。
ResourcesManager.java

    public @Nullable Resources getResources(@Nullable IBinder activityToken,
            @Nullable String resDir,
            @Nullable String[] splitResDirs,
            @Nullable String[] overlayDirs,
            @Nullable String[] libDirs,
            int displayId,
            @Nullable Configuration overrideConfig,
            @NonNull CompatibilityInfo compatInfo,
            @Nullable ClassLoader classLoader) {
        try {
            Trace.traceBegin(Trace.TRACE_TAG_RESOURCES, "ResourcesManager#getResources");
            final ResourcesKey key = new ResourcesKey(
                    resDir,
                    splitResDirs,
                    overlayDirs,
                    libDirs,
                    displayId,
                    overrideConfig != null ? new Configuration(overrideConfig) : null, // Copy
                    compatInfo);
            classLoader = classLoader != null ? classLoader : ClassLoader.getSystemClassLoader();
            return getOrCreateResources(activityToken, key, classLoader);
        } finally {
            Trace.traceEnd(Trace.TRACE_TAG_RESOURCES);
        }
    }

ResourcesManager.java

    private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
            @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
        synchronized (this) {
            if (DEBUG) {
                Throwable here = new Throwable();
                here.fillInStackTrace();
                Slog.w(TAG, "!! Get resources for activity=" + activityToken + " key=" + key, here);
            }

            if (activityToken != null) {
                final ActivityResources activityResources =
                        getOrCreateActivityResourcesStructLocked(activityToken);

                // Clean up any dead references so they don't pile up.
                ArrayUtils.unstableRemoveIf(activityResources.activityResources,
                        sEmptyReferencePredicate);

                // Rebase the key's override config on top of the Activity's base override.
                if (key.hasOverrideConfiguration()
                        && !activityResources.overrideConfig.equals(Configuration.EMPTY)) {
                    final Configuration temp = new Configuration(activityResources.overrideConfig);
                    temp.updateFrom(key.mOverrideConfiguration);
                    key.mOverrideConfiguration.setTo(temp);
                }

                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    return getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                            resourcesImpl, key.mCompatInfo);
                }

                // We will create the ResourcesImpl object outside of holding this lock.

            } else {
                // Clean up any dead references so they don't pile up.
                ArrayUtils.unstableRemoveIf(mResourceReferences, sEmptyReferencePredicate);

                // Not tied to an Activity, find a shared Resources that has the right ResourcesImpl
                ResourcesImpl resourcesImpl = findResourcesImplForKeyLocked(key);
                if (resourcesImpl != null) {
                    if (DEBUG) {
                        Slog.d(TAG, "- using existing impl=" + resourcesImpl);
                    }
                    return getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
                }

                // We will create the ResourcesImpl object outside of holding this lock.
            }
        }

        // If we're here, we didn't find a suitable ResourcesImpl to use, so create one now.
        ResourcesImpl resourcesImpl = createResourcesImpl(key);
        if (resourcesImpl == null) {
            return null;
        }

        synchronized (this) {
            ResourcesImpl existingResourcesImpl = findResourcesImplForKeyLocked(key);
            if (existingResourcesImpl != null) {
                if (DEBUG) {
                    Slog.d(TAG, "- got beat! existing impl=" + existingResourcesImpl
                            + " new impl=" + resourcesImpl);
                }
                resourcesImpl.getAssets().close();
                resourcesImpl = existingResourcesImpl;
            } else {
                // Add this ResourcesImpl to the cache.
                mResourceImpls.put(key, new WeakReference<>(resourcesImpl));
            }

            final Resources resources;
            if (activityToken != null) {
                resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                        resourcesImpl, key.mCompatInfo);
            } else {
                resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
            }
            return resources;
        }
    }

找到ResourcesImpl對象賦值的地方董虱,findResourcesImplForKeyLocked(key)看名字像是一個取緩存的方法扼鞋,最后我們發(fā)現(xiàn)ResourcesImpl對象是通過createResourcesImpl方法生成的。
ResourcesManager.java

    private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
        final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
        daj.setCompatibilityInfo(key.mCompatInfo);

        final AssetManager assets = createAssetManager(key);
        if (assets == null) {
            return null;
        }

        final DisplayMetrics dm = getDisplayMetrics(key.mDisplayId, daj);
        final Configuration config = generateConfig(key, dm);
        final ResourcesImpl impl = new ResourcesImpl(assets, dm, config, daj);

        if (DEBUG) {
            Slog.d(TAG, "- creating impl=" + impl + " with key: " + key);
        }
        return impl;
    }

我們找到了生成AssetManager對象的地方

final AssetManager assets = createAssetManager(key);

ResourcesManager.java

    protected @Nullable AssetManager createAssetManager(@NonNull final ResourcesKey key) {
        AssetManager assets = new AssetManager();

        // resDir can be null if the 'android' package is creating a new Resources object.
        // This is fine, since each AssetManager automatically loads the 'android' package
        // already.
        if (key.mResDir != null) {
            if (assets.addAssetPath(key.mResDir) == 0) {
                Log.e(TAG, "failed to add asset path " + key.mResDir);
                return null;
            }
        }

        if (key.mSplitResDirs != null) {
            for (final String splitResDir : key.mSplitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    Log.e(TAG, "failed to add split asset path " + splitResDir);
                    return null;
                }
            }
        }

        if (key.mOverlayDirs != null) {
            for (final String idmapPath : key.mOverlayDirs) {
                assets.addOverlayPath(idmapPath);
            }
        }

        if (key.mLibDirs != null) {
            for (final String libDir : key.mLibDirs) {
                if (libDir.endsWith(".apk")) {
                    // Avoid opening files we know do not have resources,
                    // like code-only .jar files.
                    if (assets.addAssetPathAsSharedLibrary(libDir) == 0) {
                        Log.w(TAG, "Asset path '" + libDir +
                                "' does not exist or contains no resources.");
                    }
                }
            }
        }
        return assets;
    }

至此愤诱,我們發(fā)現(xiàn)了apk路徑是通過assets.addAssetPath(key.mResDir)調(diào)用添加進來的云头。

Resource與AssetManager對象是否全局唯一以及與LoadedApk的聯(lián)系

根據(jù)上述分析,我們知道了每個Resource對象中包含一個唯一的AssetManager對象淫半,因此Resource對象唯一溃槐,AssetManager對象便唯一。
又Resource對象是ContextImpl對象的成員變量科吭,而ContextImpl對象的數(shù)=Activity數(shù)+Service數(shù)+1個Application昏滴,所以Resource對象不唯一?我們不妨來分析下Application对人、Activity與Service在初始化的過程中對Resource是如何賦值的谣殊。

Application與Context

ActivityThread.java

    private void handleBindApplication(AppBindData data) {
           // ......
           // 獲取應(yīng)用信息LoadedApk
          data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
           // 實例化Application
          Application app = data.info.makeApplication(data.restrictedBackupMode, null);
          mInitialApplication = app;
    }

Activity與Context

ActivityThread.java

 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        //...
         if (r.packageInfo == null) {
            r.packageInfo = getPackageInfo(aInfo.applicationInfo, r.compatInfo,
                    Context.CONTEXT_INCLUDE_CODE);
        }
         //......   
        Activity activity = null;
        //......   
            activity = mInstrumentation.newActivity(
                    cl, component.getClassName(), r.intent);
        //......
                //createBaseContextForActivity返回了ContextImpl實例    
                Context appContext = createBaseContextForActivity(r, activity);
        //......    
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config,
                        r.referrer, r.voiceInteractor);

        //......    
        return activity;
    }

Service與Context

ActivityThread.java

    private void handleCreateService(CreateServiceData data) {
            //...
            LoadedApk packageInfo = getPackageInfoNoCheck(
                data.info.applicationInfo, data.compatInfo);
            //......
            service = (Service) cl.loadClass(data.info.name).newInstance();
            //......
            ContextImpl context = ContextImpl.createAppContext(this, packageInfo);
            context.setOuterContext(service);

            Application app = packageInfo.makeApplication(false, mInstrumentation);
            service.attach(context, this, data.info.name, data.token, app,
                    ActivityManagerNative.getDefault());
            //......
    }

由上述分析可知,Resource對象中的關(guān)鍵屬性都是由LoadedApk對象中傳遞的牺弄,因此只要LoadedApk對象唯一姻几,Resource對象便唯一。
而LoadedApk對象幾乎都是從ActivityThread#getPackageInfoNoCheck方法中獲取的势告。

    private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,
            ClassLoader baseLoader, boolean securityViolation, boolean includeCode,
            boolean registerPackage) {
        final boolean differentUser = (UserHandle.myUserId() != UserHandle.getUserId(aInfo.uid));
        synchronized (mResourcesManager) {
            WeakReference<LoadedApk> ref;
            if (differentUser) {
                // Caching not supported across users
                ref = null;
            } else if (includeCode) {
                ref = mPackages.get(aInfo.packageName);
            } else {
                ref = mResourcePackages.get(aInfo.packageName);
            }

            LoadedApk packageInfo = ref != null ? ref.get() : null;
            if (packageInfo == null || (packageInfo.mResources != null
                    && !packageInfo.mResources.getAssets().isUpToDate())) {
                if (localLOGV) Slog.v(TAG, (includeCode ? "Loading code package "
                        : "Loading resource-only package ") + aInfo.packageName
                        + " (in " + (mBoundApplication != null
                                ? mBoundApplication.processName : null)
                        + ")");
                packageInfo =
                    new LoadedApk(this, aInfo, compatInfo, baseLoader,
                            securityViolation, includeCode &&
                            (aInfo.flags&ApplicationInfo.FLAG_HAS_CODE) != 0, registerPackage);

                if (mSystemThread && "android".equals(aInfo.packageName)) {
                    packageInfo.installSystemApplicationInfo(aInfo,
                            getSystemContext().mPackageInfo.getClassLoader());
                }

                if (differentUser) {
                    // Caching not supported across users
                } else if (includeCode) {
                    mPackages.put(aInfo.packageName,
                            new WeakReference<LoadedApk>(packageInfo));
                } else {
                    mResourcePackages.put(aInfo.packageName,
                            new WeakReference<LoadedApk>(packageInfo));
                }
            }
            return packageInfo;
        }
    }

LoadedApk對象以包名為鍵值緩存在一個ArrayMap中蛇捌。因此,LoadedApk對象全局唯一咱台,修改了LoadedApk中的資源路徑豁陆,也便修改了Resource對象中的資源路徑。
又Resource對象實際查找資源的能力是在ResourceImpl對象中吵护,ResourceImpl對象是全局唯一的盒音,而Resource對象每次在調(diào)用android.app.ResourcesManager#getResources時都會生成表鳍。

參考:https://segmentfault.com/a/1190000013048236?utm_medium=referral&utm_source=tuicool

資源的插件化方案

資源的插件化方案分為兩種:一種是合并資源方案,將插件的所有資源添加到宿主的Resources中祥诽,這種插件方案可以訪問宿主的資源譬圣。另一種是構(gòu)建插件資源方案,為每個插件都構(gòu)造出獨立的Resources雄坪,這種方案不可以訪問宿主資源厘熟。
hook思路主要分兩種:一種是在Application初始化前替換掉LoadedApk的資源路徑,這種方式可以一勞永逸维哈,以VirtualApp為代表绳姨;另一種是自己實現(xiàn)Contextmpl并重寫getResources()方法,返回自己創(chuàng)建的Resources對象阔挠,再在每次Application或四大組件初始化的時候?qū)⒆约旱腸ontext對象替換進去飘庄,以VirtualApk為代表。

插件化資源沖突的處理

插件化資源沖突主要是指資源id的沖突购撼,資源id由三部分組成跪削,即PackageId+TypdId+EntryId,如0x7f0b0001代表的是layout類型的第二個資源迂求。同一資源id可能對應(yīng)了宿主和插件apk中兩個不同的資源碾盐。解決這個問題就是要為不同的插件設(shè)置不同的PackageId。
方案一: 修改AAPT揩局,為每個插件指定不同的前綴毫玖,只要不是0x7f就行。
方案二: 在aapt執(zhí)行后凌盯,修改R.java和arsc文件付枫,修改R.java中所有的資源id前綴,修改arsc文件中所有的資源id前綴十气。(gradle-small插件,hook了processReleaseResource task)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末春霍,一起剝皮案震驚了整個濱河市砸西,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌址儒,老刑警劉巖芹枷,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異莲趣,居然都是意外死亡鸳慈,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進店門喧伞,熙熙樓的掌柜王于貴愁眉苦臉地迎上來走芋,“玉大人绩郎,你說我怎么就攤上這事∥坛眩” “怎么了肋杖?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長挖函。 經(jīng)常有香客問我状植,道長,這世上最難降的妖魔是什么怨喘? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任津畸,我火速辦了婚禮,結(jié)果婚禮上必怜,老公的妹妹穿的比我還像新娘肉拓。我一直安慰自己,他們只是感情好棚赔,可當(dāng)我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布帝簇。 她就那樣靜靜地躺著,像睡著了一般靠益。 火紅的嫁衣襯著肌膚如雪丧肴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天胧后,我揣著相機與錄音芋浮,去河邊找鬼。 笑死壳快,一個胖子當(dāng)著我的面吹牛纸巷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播眶痰,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼瘤旨,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了竖伯?” 一聲冷哼從身側(cè)響起存哲,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎七婴,沒想到半個月后祟偷,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡打厘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年修肠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片户盯。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡嵌施,死狀恐怖饲化,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艰管,我是刑警寧澤滓侍,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站牲芋,受9級特大地震影響撩笆,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜缸浦,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一夕冲、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧裂逐,春花似錦歹鱼、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至掺涛,卻和暖如春庭敦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背薪缆。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工秧廉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拣帽。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓疼电,卻偏偏與公主長得像,于是被迫代替她去往敵國和親减拭。 傳聞我的和親對象是個殘疾皇子蔽豺,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,781評論 2 361

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