Android插件化原理和实践 (四) 之 合并插件中的资源

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lyz_zyx/article/details/84872832

我们继续来学习Android插件化相关知识,还是要围绕着三个根本问题来展开。在前面两章中已经讲解过第一个根本问题:在宿主中如何去加载插件以及调用插件中类和组件代码。Demo中使用了Service来演示,因为还没有解决加载插件中资源的问题,用Activity不好展示。所以本文将要从资源的加载机制讲起,然后进一步介绍AssetManager类,最后就是为解决第二个根本问题,就是在宿主加载插件后如何解决资源读取问题做准备。

1 资源加载机制

1.1 资源分类

Android中资源文件分为两类:

一类是在res目录下存放的可编译资源文件。比如layout、drawable、string等,它们在编译时会被系统自动在R.java中生成资源文件的十六进制值。所以访问此类资源,如要获取一个字符串,那就使用代码:

String str = context.getResources().getString(R.string.XX);即可。

另一类是assets目录下存放的原始资源文件。Apk在编译时不会编译此类资源文件,要访问此类资源只能通过AssetManager类open方法来获取,AssetManager类又可以通过context.getResource().getAssets()方法获取。所以归根到底还是离不开Resource类。

1.2 Resources和 AssetManager

我们比较熟悉的Resources类对外的提供getLayout、getDrawable、getString等方法,其实都是间接调用了AssetManager类的方法,然后AssetManager再向系统读取资源。

就拿context.getResource().getString方法来看:

Resources.java

public String getString(@StringRes int id) throws NotFoundException {
    return getText(id).toString();
}
public CharSequence getText(@StringRes int id) throws NotFoundException {
    CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
    if (res != null) {
        return res;
    }
    throw new NotFoundException("String resource ID #0x" + Integer.toHexString(id));
}

在代码中mResourcesImpl.getAssets()返回的就是AssetManager类对象。

AssetManager如此重要,那就来看看它到底是何方神圣。其实在App启动时会进行加载资源,在那时会把当前apk路径传入给AssetManager类的addAssetPath方法,然后经过AssetManager内部的NDK方法和一系列逻辑后,AssetManager和Resources就能够访问当前apk的所有资源,因为apk打包时会在R.java中生成一个十六进制值,和生成一个resources.arsc文件,此文件是一个Hash表,对资源的十六进制值是对应的。其流程可见《Android应用程序启动详解(二)之Application和Activity的启动过程》中介绍的创建Applicatoin过程中有下面代码:

LoadedApk.java

public Application makeApplication(boolean forceDefaultAppClass, Instrumentation instrumentation) {
    ……
    try {
        ……
        // 关键代码1
        ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
        // 关键代码2
        app = mActivityThread.mInstrumentation.newApplication(cl, appClass, appContext);
        appContext.setOuterContext(app);
    } catch (Exception e) {
        ……
    }
    ……
}

我们当时只往关键代码2中往下介绍,今天就在此补充关键代码1中的资源加载流程,请往下看代码:

ContextImpl.java

static ContextImpl createAppContext(ActivityThread mainThread, LoadedApk packageInfo) {
    if (packageInfo == null) throw new IllegalArgumentException("packageInfo");
    ContextImpl context = new ContextImpl(null, mainThread, packageInfo, null, null, null, 0,
            null);
    // 关键代码
    context.setResources(packageInfo.getResources());
    return context;
}

这里有代码:context.setResources(),所以我们平时可以通过context.getResources()来获得资源,继续看setResources方法中的参数

LoadedApk.java

public Resources getResources() {
    if (mResource s == null) {
        ……
        // 关键代码
        mResources = ResourcesManager.getInstance().getResources(null, mResDir,
                splitPaths, mOverlayDirs, mApplicationInfo.sharedLibraryFiles,
                Display.DEFAULT_DISPLAY, null, getCompatibilityInfo(),
                getClassLoader());
    }
    return mResources;
}

这里又回到了LoaderApk类,继续往下看关键代码

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

从关键代码行中调用的getOrCreateResources方法名可猜到,资源的获取或创建就是此方法,继续往下看

private @Nullable Resources getOrCreateResources(@Nullable IBinder activityToken,
        @NonNull ResourcesKey key, @NonNull ClassLoader classLoader) {
    ……
    // 关键代码1
    // 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) {
        ……
        final Resources resources;
        if (activityToken != null) {
            resources = getOrCreateResourcesForActivityLocked(activityToken, classLoader,
                    resourcesImpl, key.mCompatInfo);
        } else {
            // 关键代码2
            resources = getOrCreateResourcesLocked(classLoader, resourcesImpl, key.mCompatInfo);
        }
        return resources;
    }
}

这里先提一下关键代码2,因为最终 Resources 的创建逻就在里头:

private @NonNull Resources getOrCreateResourcesLocked(@NonNull ClassLoader classLoader,
        @NonNull ResourcesImpl impl, @NonNull CompatibilityInfo compatInfo) {
    ……
    // Create a new Resources reference and use the existing ResourcesImpl object.
    Resources resources = compatInfo.needsCompatResources() ? new CompatResources(classLoader)
            : new Resources(classLoader);
    resources.setImpl(impl);
    ……
    return resources;
}

可能看到代码中通过了new CompatResources或者 new Resources来创建了Resources对象。现在我们回头来看看getOrCreateResources方法的关键代码1:

private @Nullable ResourcesImpl createResourcesImpl(@NonNull ResourcesKey key) {
    final DisplayAdjustments daj = new DisplayAdjustments(key.mOverrideConfiguration);
    daj.setCompatibilityInfo(key.mCompatInfo);
    // 关键代码
    final AssetManager assets = createAssetManager(key);
    ……
    return impl;
}
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;
}

流程到这就能看到,最终是调用了AssetManager类的addAssetPath方法传入各种资源目录来对其进行加载:

AssetManager.java

/**
 * Add an additional set of assets to the asset manager.  This can be
 * either a directory or ZIP file.  Not for use by applications.  Returns
 * the cookie of the added asset, or 0 on failure.
 * {@hide}
 */
public final int addAssetPath(String path) {
    return  addAssetPathInternal(path, false);
}

到这就不打算继续往下看源码了,因为已经达到了我们的目的。addAssetPath方法是一个hide不对外开放的方法,先来翻译一下它的注释,意思大概是:添加一组额外的资源,可以是目录或zip文件。从注释意思我们就更加确定addAssetPath方法就是我们要找的添加插件资源的方法。虽然此方法是不对外开放,但是我们可以通过反射来把插件apk的路径传入这个方法,那么就可以把插件中资添也添加到这个资源池中去了。

2 合并插件中的资源

既然已经清楚了App资源的加载机制,现在就来看看反射这个过程应该怎样做了。所以步骤如下:

  1. 创建一个新的 AssetManager 对象,并将宿主和插件的资源都能过addAssetPath方法塞入
  2. 通过新的AssetManager对象来创建出一个新的Resources对象
  3. 将新的Resources对象替换ContextImpl中的mResources变量、LoadedApk变量里的mResources变量 以及 至空mThem变量

所以按照上述步骤通过代码实现如下:

private static void loadPluginResources(Application application, String apkName)
        throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
    // 创建一个新的 AssetManager 对象
    AssetManager newAssetManagerObj = AssetManager.class.newInstance();
    Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
    // 塞入原来宿主的资源
    addAssetPath.invoke(newAssetManagerObj, application.getBaseContext().getPackageResourcePath());
    // 塞入插件的资源
    File optDexFile = application.getBaseContext().getFileStreamPath(apkName);
    addAssetPath.invoke(newAssetManagerObj, optDexFile.getAbsolutePath());

    // ----------------------------------------------

    // 创建一个新的 Resources 对象
    Resources newResourcesObj = new Resources(newAssetManagerObj,
            application.getBaseContext().getResources().getDisplayMetrics(),
            application.getBaseContext().getResources().getConfiguration());

    // ----------------------------------------------

    // 获取 ContextImpl 中的 Resources 类型的 mResources 变量,并替换它的值为新的 Resources 对象
    Field resourcesField = application.getBaseContext().getClass().getDeclaredField("mResources");
    resourcesField.setAccessible(true);
    resourcesField.set(application.getBaseContext(), newResourcesObj);

    // ----------------------------------------------

    // 获取 ContextImpl 中的 LoadedApk 类型的 mPackageInfo 变量
    Field packageInfoField = application.getBaseContext().getClass().getDeclaredField("mPackageInfo");
    packageInfoField.setAccessible(true);
    Object packageInfoObj = packageInfoField.get(application.getBaseContext());

    // 获取 mPackageInfo 变量对象中类的 Resources 类型的 mResources 变量,,并替换它的值为新的 Resources 对象
    // 注意:这是最主要的需要替换的,如果不需要支持插件运行时更新,只留这一个就可以了
    Field resourcesField2 = packageInfoObj.getClass().getDeclaredField("mResources");
    resourcesField2.setAccessible(true);
    resourcesField2.set(packageInfoObj, newResourcesObj);

    // ----------------------------------------------

    // 获取 ContextImpl 中的 Resources.Theme 类型的 mTheme 变量,并至空它
    // 注意:清理mTheme对象,否则通过inflate方式加载资源会报错, 如果是activity动态加载插件,则需要把activity的mTheme对象也设置为null
    Field themeField = application.getBaseContext().getClass().getDeclaredField("mTheme");
    themeField.setAccessible(true);
    themeField.set(application.getBaseContext(), null);
}

上述方法的调用时机就是在执行完插件dex合并后调用即可,但是你会发现,就算调用了此方法后使得插件中的资源能和宿主中的资源合并成功了,但还是没有解决到问题。知道为什么啊?那是因为宿主和插件的资源id冲突了。关于如何解决资源id冲突的问题,我们留到下一遍文章来解决。

 

 

 

 

 

猜你喜欢

转载自blog.csdn.net/lyz_zyx/article/details/84872832
今日推荐