Android加载外部APK资源原理与实战

作者: 唠嗑008 | 来源:发表于2020-04-23 17:49 被阅读0次

    之所以要单独讲一下这个知识点,是因为在热修复中的资源修复和插件化方案中都需要去加载外部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
         */
        @UnsupportedAppUsage
        public int addAssetPath(String path) {
            return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
        }
    }
    

    这里非常的关键,需要解释一下,首先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目录下,然后才可以调用加载。

    相关文章

      网友评论

        本文标题:Android加载外部APK资源原理与实战

        本文链接:https://www.haomeiwen.com/subject/twcpihtx.html