序:
1.本文是安卓插件化课程的第四篇,完整课程链接参见下面链接:
2.主要内容
本篇主要讲的是如何在宿主APP中,使用插件中的资源文件,这是以使用插件中的一个string为例子。
一:原理简述
想知道如何加载插件中的资源,那么一定先要了解正常获取资源的流程。
我们先扩展一下,了解一下获取资源的流程,以及应用加载资源的时机和流程。
1.1.获取资源文件的全流程
1.resource.getString()
这里仍然以取string为例子,resource中getString,最终调用的是getText方法,
@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));
}
最终调用的是assetManager的getResourceText方法。
2.getResourceText方法
@Nullable CharSequence getResourceText(@StringRes int resId) {
synchronized (this) {
final TypedValue outValue = mValue;
if (getResourceValue(resId, 0, outValue, true)) {
return outValue.coerceToString();
}
return null;
}
}
交给了getResourceValue方法去处理,返回值是boolean告知是否查找最成功。如果成功则会给TypeValue赋值。
@UnsupportedAppUsage
boolean getResourceValue(@AnyRes int resId, int densityDpi, @NonNull TypedValue outValue,
boolean resolveRefs) {
Objects.requireNonNull(outValue, "outValue");
synchronized (this) {
ensureValidLocked();
final int cookie = nativeGetResourceValue(
mObject, resId, (short) densityDpi, outValue, resolveRefs);
if (cookie <= 0) {
return false;
}
// Convert the changing configurations flags populated by native code.
outValue.changingConfigurations = ActivityInfo.activityInfoConfigNativeToJava(
outValue.changingConfigurations);
if (outValue.type == TypedValue.TYPE_STRING) {
if ((outValue.string = getPooledStringForCookie(cookie, outValue.data)) == null) {
return false;
}
}
return true;
}
}
这里可以看到,最终是交给了nativeGetResourceValue方法去处理的,JNI层。
3.nativeGetResourceValue方法
待补充
1.2应用何时加载资源
1.前面讲过了,应用的dex文件,其实是由虚拟机去加载的。那么资源文件是何时加载呢?讲到资源加载就不得不提一下一个应用启动的流程。具体流程这个可以参照我的另外一片文章:Android中APP启动的完整流程。
这里就简单的讲一下:
桌面点击图标->通知系统->系统创建APP进程并调用ActivityThread的main方法->APP调用attach方法通知系统已经启动->系统通知APP进行Application的创建,调用handleBindApplication方法来创建application。
这里主要包含四个关键步骤:
第一步: data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo);
service传递过来ApplicationInfo,app这边来用来创建LoadedApk对象。这时候赋值记录资源包路径等信息。
第二步:ContextImpl.createAppContext(this, data.info);使用LoadedApk对象去创建Context对象。
我们activity中获取resource对象等操作,最终都是由ContextImpl来提供的。ContextImpl就是ContextWapper这个application和activity的父类中的mBase。
第三步:mInstrumentation = new Instrumentation();创建Instrumentation对象
第四部:app = data.info.makeApplication(data.restrictedBackupMode, null);创建最终的application对象
所以资源文件的加载,就发生在第二步中,资源都是通过ContextImpl来获取的。
不过实际上加载资源的流程,application和activity对应的assetManager其实是两个,但流程是一样的。
2.创建流程的调用逻辑如下图所示:
而最终添加的资源路径其实就是apk文件的地址,下面就是一个例子
ApkAssets{path=<empty> and /data/app/~~s9LfytxMb1TEz7xqWvupRA==/com.xt.appplugin-IwJNMlbgwYpZNTVrhYPk4w==/base.apk}
1.3如何加载外部资源
1.上面资源加载的流程和时机,因为我们一般来说最早的调用时机是application中的onCreate方法,但是在这之前资源对象就已经生成好了,所以生成时去hook就不太可能了。
2.在1.1中我们知道获取资源的流程是先通过resource对象,然后resource对象又通过assetManager去获取的。所以这里我们有两个方案,第一是替换掉resource对象,第二就是对assetManager做一些改造。
参照在assetManager.Builder的build方法中,最终生成assetManager的方法如下:
final AssetManager assetManager = new AssetManager(false /*sentinel*/);
assetManager.mApkAssets = apkAssets;
AssetManager.nativeSetApkAssets(assetManager.mObject, apkAssets,
false /*invalidateCaches*/);
assetManager.mLoaders = mLoaders.isEmpty() ? null
: mLoaders.toArray(new ResourcesLoader[0]);
return assetManager;
给mApkAssets赋值,然后通知native层刷新。我们发现assetManager中还有一个方法也提供了这样的功能:
/**
* @deprecated Use {@link #setApkAssets(ApkAssets[], boolean)}
* @hide
*/
@Deprecated
@UnsupportedAppUsage
public int addAssetPath(String path) {
return addAssetPathInternal(path, false /*overlay*/, false /*appAsLib*/);
}
3.所以第一个想法是,获取到assetManager对象之后,通过反射调用这个方法,把外部APK中资源也加载进来。但是尝试了一把之后,失败了,虽然mApkAssets中的路径确实已经变了,但是我们获取资源的时候,仍然获取不到。
4.既然对assetManager改造不行,那我们就回过头来想想如何改造resource对象。好行也不行,因为Rosources中也是直接交给asset处理的。
@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));
}
5.既然hook不行,那我们能不能模拟系统的整个流程,也构造一个Resource出来呢?这条路试了下,是可以的,那我们就先按照这个方法去实现。
1.4如何在插件中使用插件中的资源
这个就是下一章要讲的内容了,本章就不扩展了。
二:代码编写
1.插件项目中资源项
<resources>
<string name="app_name">AppPlugin</string>
<string name="plugin_str1">这是plugin中资源文件中的字符串</string>
</resources>
2.编译项目,拷贝apk
我们可以看到已经string资源已经打包进apk了。
3.加载插件资源
我们直接生成一个AssetManager,然后调用addAssetPath的方法,最终生成Resources对象。
val apkPath = context.filesDir.absolutePath + File.separator + APK_NAME
//反射加载资源包
val newInstance = AssetManager::class.java.newInstance()
val setApkAssetsMedthod = AssetManager::class.java.getDeclaredMethod(
"addAssetPath",
String::class.java
)
setApkAssetsMedthod.invoke(newInstance, apkPath)
val resources = Resources(
newInstance,
context.resources.displayMetrics,
context.resources.configuration
)
然后通过resource去获取插件中的资源。
val string1 = resources.getString(0x7f040001)
showResult(string1)
这里的ID是直接从插件APK中拷贝的。
4.测试验证
首先点击加载插件,然后点击使用插件中的资源。
这时候我们看到,插件中的字符串已经显示出来了。
三:要点总结
待补充
四。代码地址:
项目地址:
https://github.com/aa5279aa/android_all_demo
插件项目位置:https://github.com/aa5279aa/android_all_demo/tree/master/DemoClient/appplugin