之所以要单独讲一下这个知识点,是因为在热修复中的资源修复和插件化方案中都需要去加载外部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_resource
Module生成的,你需要先生成,然后拷贝到项目的/data/data/files
目录下,然后才可以调用加载。
网友评论