直接在当前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_resource
Module生成的,你需要先生成,然后拷贝到项目的/data/data/files
目录下,然后才可以调用加载。
作者:唠嗑008
链接:https://www.jianshu.com/p/56b6665be502
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。