Android加载外部APK资源

直接在当前APK加载未安装apk中的资源

我现在就是要在当前安装的apk中去加载未安装的apk的res目录下的drawable、layout、string、color等。先来看一个简单的实践

首先去创建一个用于动态加载的项目dynamic_resource,我要做的事情很简单,就是获取res目录下的一个string字符串和drawable下的一张图片。我通过一个工具类来获取。

class ResourceUtils {
    //直接返回文本字符串
    fun getTextFromPlugin(): String {
        return "插件APK类里的文本内容"
    }

   //读取资源文件里的文本字符串
    fun getTextFromPluginRes(context: Context): String? {
        return context.resources.getString(R.string.plugin_text)
    }

    //读取资源文件里的图片
    fun getDrawableFromPlugin(context: Context): Drawable? {
        return context.resources.getDrawable(R.drawable.kb)
    }
}

然后,编译获取到这个apk,我直接把它放到当前已安装apk的私有目录/data/data/下,然后在当前app中去加载这个未安装apk的资源。想要实现这个不难吧,在之前文章Android类加载机制说过,可以通过类加载器中的DexClassLoader去加载外部的apk/dex文件,这样的话,就可以加载到ResourceUtils类,然后通过反射去调用加载资源的方法。

private fun loadPluginResource() {
        try {
            apkPath = filesDir.path + "/dynamic_resource-debug.apk"
           

            var odexFile: File = getDir("odex", Context.MODE_PRIVATE)
            var dexClassLoader = DexClassLoader(apkPath, odexFile.path, null, classLoader)
            //加载插件中的资源获取工具类
            var clazz = dexClassLoader.loadClass("com.zx.dynamic_resource.ResourceUtils")
            var obj = clazz.newInstance()

            //加载插件里类中定义的字符串资源
            var method: Method = clazz.getMethod("getTextFromPlugin")
            var text: String = method.invoke(obj) as String
            tv1.text = text

            method = clazz.getMethod("getTextFromPluginRes", Context::class.java)
            text = method.invoke(obj, this) as String
            tv2.text = text

            method = clazz.getMethod("getDrawableFromPlugin", Context::class.java)
            var drawable: Drawable = method.invoke(obj, this) as Drawable
            image.setImageDrawable(drawable)
        } catch (e: Exception) {
            Log.e("tag", "loadPluginResource---: $e")
        }
    }

上面解释过了,这段代码大家也都看得懂。但是执行效果如何?大家可以思考一下。结果是:只有tv1加载成功了,另外2个都失败了。

结果分析
第一个成功了,说明加载这个未安装apk成功了,也确实加载到这个工具类ResourceUtils,但是后2个方法之所以失败,是因为它们是通过当前apk传入的Context去加载的资源,具体来说用的是当前app的Resources对象,但实际上这个Resources对象并不能访问未安装apk的资源。下面就通过简单的源码分析来看一下为什么不能?

访问外部资源原理

这里只是介绍简单的,需要了解Resource和AssetManager创建流程的参考:Android资源动态加载以及相关原理分析

context.getResources().getText()

##Resources
@NonNull 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));
    }

 ##ResourcesImpl
 public AssetManager getAssets() {
        return mAssets;
    }

内部是调用了mResourcesImpl去访问的,这个对象是ResourcesImpl类型,最后是通过AssetManager去访问资源的。现在可以得出一个结论,AssetManager是真正加载资源的对象,而Resources是app层面API调用的类。

AssetManager

/**
 * Provides access to an application's raw asset files; see {@link Resources}
 * for the way most applications will want to retrieve their resource data.
 * This class presents a lower-level API that allows you to open and read raw
 * files that have been bundled with the application as a simple stream of
 * bytes.
 */
public final class AssetManager implements AutoCloseable {

   /**
     * 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
     */
    

这里非常的关键,需要解释一下,首先AssetManager是资源管理器,专门负责加载资源的,它内部有个隐藏方法addAssetPath,是用于加载指定路径下的资源文件,也就是说你把apk/zip的路径传给它,它就能吧资源数据读到AssetManager,然后就可以访问了。

但是有个问题,虽然实际加载资源的是AssetManager,但是我们通过API访问的确是Resources对象,所以看下Resources对象的构造方法

ResourcesImpl的创建

/**
     * Create a new Resources object on top of an existing set of assets in an
     * AssetManager.
     *
     * @param assets Previously created AssetManager.
     * @param metrics Current display metrics to consider when
     *                selecting/computing resource values.
     * @param config Desired device configuration to consider when
     *               selecting/computing resource values (optional).
     */
    public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
        this(null);
        mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
    }

看到这个构造方法,有点感觉了吧。可以通过AssetManager对象去构造mResourcesImpl对象,之前也分析过资源访问是通过mResourcesImpl.getAssets().getXXX()方法来完成的,那现在就有办法解决加载外部apk资源的问题了。

加载外部apk资源的解决思路

首先传入apk路径,通过AssetManager#addAssetPath()让AssetManager加载指定路径的资源,然后用用AssetManager创建Resources对象,这样的话就可以了。但是需要注意一点,由于addAssetPath是个隐藏方法,所以可以只能通过反射去调用。

    /**
     * 加载外部apk中的资源,方法是通过反射调用AssetManager.addAssetPath()方法去
     * 让AssetManager加载指定路径的资源
     */
    private fun initResources() {
        var clazz = AssetManager::class.java
        var assetManager = clazz.newInstance()
        var addAssetPathMethod = clazz.getMethod("addAssetPath", String::class.java)
        addAssetPathMethod.invoke(assetManager, apkPath)

        mResource = Resources(assetManager, resources?.displayMetrics, resources?.configuration)
    }

到这一步,就获得了加载外部apk资源的Resources对象。在刚才的工具类方法调用的时候是通过context.getResource().getXX调用的,当时的Resources对象是只能加载当前apk路径的资源,现在我们已经创建了一个可以加载外部apk资源的Resource,所以,我们只需要重写当前Activity的getResources()方法即可

    /**
     * 重写当前apk中的Resources对象,这样就可以加载指定路径(外部apk)中的资源对象
     */
    override fun getResources(): Resources? {
        return if (mResource == null) super.getResources() else mResource
    }

到这里,就可以成功加载外部apk的资源了。注意,这里只是提供了加载外部apk资源的一种思路,实际上在插件化和热修复中的解决方式会比这个更佳复杂,但是有了这个基础,后面再研究这2门技术等时候就会容易很多。关于动态加载资源在这2个技术中的应用,很快就会和大家见面,有所收获的同学点个赞吧。

项目地址

https://github.com/zhouxu88/PluginSamples

注意:外部apk是dynamic_resourceModule生成的,你需要先生成,然后拷贝到项目的/data/data/files目录下,然后才可以调用加载。



作者:唠嗑008
链接:https://www.jianshu.com/p/56b6665be502
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
posted @ 2020-12-07 17:00  chenxibobo  阅读(1715)  评论(0编辑  收藏  举报