Android個人項目插件化總結——插件apk資源加載

爲什麼加載不了資源

我們知道獲取資源時實際都是使用了Resource。

mResource這個變量在Activity父類ContextThemeWrapper中。

ContextThemeWrapper的mResource的獲得是通過Context的實現類得到的。

Context實現類ContextImpl中獲得Resources 是通過

 Resources resources = packageInfo.getResources(mainThread);

packageInfo 是一個LoadedApk對象。

這個對象相當於是一個apk在內存中的表示, Apk文件的相關信息、資源都可以通過此對象獲取。

這個LoadedApk是怎麼創建的呢。

在ActivityThread的getPackageInfo方法中:

private LoadedApk getPackageInfo(ApplicationInfo aInfo, CompatibilityInfo compatInfo,  ClassLoader baseLoader, boolean securityViolation, boolean includeCode,boolean registerPackage) {
    synchronized (mResourcesManager) {
        WeakReference<LoadedApk> ref;
        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);
          ······
        }
        return packageInfo;
    }
} 

可以看到構造參數裏面有個參數是ApplicationInfo 。

所以說不管前面是hook IActivityManager合併dex還是hook Instrumentation 傳入我們的ClassLoader,這個LoadedApk裏傳入的ApplicationInfo其實還是我們宿主的,並不是插件apk中的。

所以我們在插件apk中直接使用資源,等到插件apk被宿主調用器後,使用的是宿主的資源庫,而宿主的資源中並沒有我們插件apk中的資源,所以一運行的時候就會報錯。

那麼怎麼解決呢。

我們創建一個Resource給我們的插件Activity就可以了。

如何加載資源

我們知道一般得到資源調用的是getResource方法,最終調用了Context.getResources()方法。

我們看看Context實現類ContextImpl對應的方法。

 @Override
public Resources getResources() {
    return mResources;
}

再看看mResource是如何賦值的。

private ContextImpl(ContextImpl container, ActivityThread mainThread,
        LoadedApk packageInfo, IBinder activityToken, UserHandle user, int flags,
        Display display, Configuration overrideConfiguration, int createDisplayWithId) {

    ...

    mPackageInfo = packageInfo;

    //這裏拿到了一個ResourcesManager,單例的,說明我們應用當中使用的都是同一套資源
    mResourcesManager = ResourcesManager.getInstance();

    ...

    //LoadedApk對象中得到Resources對象
    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賦值
    mResources = resources;
    ...

} 

ResourcesManager是一個單例,通過它獲得mResources,保證了我們每個Context獲取的都是同樣的資源。

resources通過getTopLevelResources方法賦值,我們具體看看是如何創建的

public Resources getTopLevelResources(String resDir, String[] splitResDirs,
            String[] overlayDirs, String[] libDirs, int displayId,
            Configuration overrideConfiguration, CompatibilityInfo compatInfo, IBinder token) {

		······
        //創建一個AssetManager對象
        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 (resDir != null) {'
            //添加資源路徑
            if (assets.addAssetPath(resDir) == 0) {
                return null;
            }
        }

        if (splitResDirs != null) {
            for (String splitResDir : splitResDirs) {
                if (assets.addAssetPath(splitResDir) == 0) {
                    return null;
                }
            }
        }

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

        if (libDirs != null) {
            for (String libDir : libDirs) {
                if (assets.addAssetPath(libDir) == 0) {
                    Slog.w(TAG, "Asset path '" + libDir +
                            "' does not exist or contains no resources.");
                }
            }
        }

        //Slog.i(TAG, "Resource: key=" + key + ", display metrics=" + metrics);
        DisplayMetrics dm = getDisplayMetricsLocked(displayId);
        Configuration config;
        boolean isDefaultDisplay = (displayId == Display.DEFAULT_DISPLAY);
        final boolean hasOverrideConfig = key.hasOverrideConfiguration();
        if (!isDefaultDisplay || hasOverrideConfig) {
            config = new Configuration(getConfiguration());
            if (!isDefaultDisplay) {
                applyNonDefaultDisplayMetricsToConfigurationLocked(dm, config);
            }
            if (hasOverrideConfig) {
                config.updateFrom(key.mOverrideConfiguration);
            }
        } else {
            config = getConfiguration();
        }'
        //創建一個Resource對象
        r = new Resources(assets, dm, config, compatInfo, token);
        if (false) {
            Slog.i(TAG, "Created app resources " + resDir + " " + r + ": "
                    + r.getConfiguration() + " appScale="
                    + r.getCompatibilityInfo().applicationScale);
        }

        synchronized (this) {
            WeakReference<Resources> wr = mActiveResources.get(key);
            Resources existing = wr != null ? wr.get() : null;
            if (existing != null && existing.getAssets().isUpToDate()) {
                // Someone else already created the resources while we were
                // unlocked; go ahead and use theirs.
                r.getAssets().close();
                return existing;
            }

            // XXX need to remove entries when weak references go away
            mActiveResources.put(key, new WeakReference<Resources>(r));
            return r;
        }
    } 

從上述源碼可以得到如下創建Resource的步驟

  • 創建一個AssetManager對象
  • 通過addAssetPath方法將資源路徑添加給AssetManager,需要注意的是,這個方法是hide的,需要反射調用。
  • 通過AssetManager對象創建一個Resource對象,構造方法中,需要一個AssetManager,其他的可以使用宿主的。

這樣就好辦了。

我們在activity剛創建好時,將我們的Resource通過反射添加到ContextThemeWrapper中。

這個時機該是什麼時候呢?

那就是Instrumentation的newActivity方法中。

看一下代碼實現:


    public PluginApp loadPluginApk(String apkPath) {
        String addAssetPathMethod = "addAssetPath";
        PluginApp pluginApp = null;
        try {
            AssetManager assetManager = AssetManager.class.newInstance();
            Method addAssetPath = assetManager.getClass().getMethod(addAssetPathMethod,
                    String.class);
            addAssetPath.invoke(assetManager, apkPath);
            Resources pluginRes = new Resources(assetManager,
                    mContext.getResources().getDisplayMetrics(),
                    mContext.getResources().getConfiguration());
            pluginApp = new PluginApp(pluginRes);
            pluginApp.mClassLoader = createDexClassLoader(apkPath);
        } catch (IllegalAccessException
                | InstantiationException
                | NoSuchMethodException
                | InvocationTargetException e) {
            e.printStackTrace();
        }
        return pluginApp;
    }


    @Override
    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {

        if (mPluginManager.hookToPluginActivity(intent)) {
            String targetClassName = intent.getComponent().getClassName();
            PluginApp pluginApp = mPluginManager.getLoadedPluginApk();
            Activity activity = mBase.newActivity(pluginApp.mClassLoader, targetClassName, intent);
            ReflectUtil.setField(ContextThemeWrapper.class, activity, Constants.FIELD_RESOURCES, pluginApp.mResources);
            return activity;
        }

        return super.newActivity(cl, className, intent);
    }

先創建好我們的Resource,然後在newActivity方法內通過反射賦值。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章