文章目录
- 1. 前言
- 2. 分析
- 3. 加载外部资源文件代码
- 4. References
1. 前言
在上篇Android插件化开发指南——Hook技术(一)【长文】中提到最终的效果其实在插件中的MainActivity
加载的资源文件activity_main.xml
其实加载的还是宿主app
的activity_main.xml
文件。所以在这篇中将解决如何从插件apk
中加载资源文件的问题。首先我们需要知道资源存储在apk
包的什么位置,不妨在AS
中打开插件的apk
文件,可以看见其文件结构为:
也就是在resources.arse
文件中。不妨来看看在Android
中是如何加载资源的。
2. 分析
比如下面从strings.xml
文件中获取值:
// MainActivity.java
String string = getString(R.string.app_name);
就来看看这个方法的背后是怎么实现的。追踪可以看到:
// Context.java
public final String getString(@StringRes int resId) {
return getResources().getString(resId);
}
也就是说是通过context
上下文对象的getResources
方法,然后再通过getString
来得到的。换句话说,在上下文context
调用getResources
方法后,就持有了资源本身,所以才可以通过getString
来得到。为了验证,这里不妨先追踪下getString()
方法:
// Resources.java
private ResourcesImpl mResourcesImpl;
public String getString(@StringRes int id) throws Resources.NotFoundException {
return getText(id).toString();
}
public CharSequence getText(@StringRes int id) throws Resources.NotFoundException {
CharSequence res = mResourcesImpl.getAssets().getResourceText(id);
if (res != null) {
return res;
}
throw new Resources.NotFoundException("String resource ID #0x"
+ Integer.toHexString(id));
}
最终通过mResourcesImpl.getAssets()
来得到一个AssetManager
对象,然后再通过AssetManager
对象来获取到资源。所以说这里其实流程为:
通过
context
来获取到资源对象Resources
,然后在资源对象Resources
中,通过ResourcesImpl
类的实例对象来获取到AssetManager
对象,然后再获取到资源对象。
所以如果我们可以仿写一个得到我们自己资源的Resources
,并把他赋值给当前context
的getResources()
方法,那么就可以做到资源的替换。那么思路为在App中定义一个继承自Application的父类,在这个方法中重写getResources()
方法,如果调用getResources()
方法能够获取到我们自定义的插件的资源,就直接返回;如果获取不到那么就使用当前应用自己的getResources()
方法。也就是这里的重点在于如何仿写一个获取到插件Resources
对象的包装类。
在前面提到了,插件资源文件位于resources.arse
文件中。所以说如果我们需要加载插件中的资源文件,类似的还是需要从apk
文件中读取。我们知道要得到Resources
对象,首先需要封装一个AssetManager
对象,所以这里看看AssetManager.java的实现。当然首先需要解决的问题是如何通过反射来获取到这个对象,在这个类中提供了一个加载外部资源文件的方法:
/**
* 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}
*/
public final int addAssetPath(String path) {
return addAssetPathInternal(path, false);
}
所以这里也是通过反射这个方法来加载外部资源对象。比如:
private static String pluginPath = "/sdcard/plugin-debug.apk";
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, pluginPath);
因为在getResources()
方法返回的是一个Resources
对象,所以这里继续查看Resources.java类中的和资源文件关联的方法。可以看到这么一个方法:
@Deprecated
public Resources(AssetManager assets, DisplayMetrics metrics, Configuration config) {
this(null);
mResourcesImpl = new ResourcesImpl(assets, metrics, config, new DisplayAdjustments());
}
故而尝试使用下面的代码:
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(),
context.getResources().getConfiguration());
3. 加载外部资源文件代码
public static Resources loadPluginResource(Context context){
Resources resources = null;
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPath.setAccessible(true);
addAssetPath.invoke(assetManager, pluginPath);
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(),
context.getResources().getConfiguration());
}catch (Exception e){
e.printStackTrace();
}
return resources;
}
为了能满足资源文件要么找自己应用程序的资源文件,要么找外部插件中的资源文件的逻辑,这里构建一个BaseApplication
:
public class BaseApplication extends Application {
private static final String pluginPath = "/sdcard/plugin-debug.apk";
private Resources pluginResources;
@Override
public void onCreate() {
super.onCreate();
LoadUtils.loadClass(this, pluginPath); // 原init方法,修改了名字
HookAMSUtils.getActivityManagerService(this, ProxyActivity.class);
HookAMSUtils.hookActivityThreadToLaunchActivity();
pluginResources = LoadUtils.loadPluginResource(this, pluginPath);
}
@Override
public Resources getResources() {
if (pluginResources != null) return pluginResources;
return super.getResources();
}
}
然后配置一下清单文件:
android:name="BaseApplication"
当然因为写代码是在Activity
中,所以这里还需要定义一个BaseActivity
,在这个类的getResources
方法中调用BaseApplication
的getResources
方法,即:
public class BaseActivity extends AppCompatActivity {
@Override
public Resources getResources() {
if(getApplication() != null && getApplication().getResources() != null)
return getApplication().getResources();
return super.getResources();
}
}
案例地址:https://github.com/baiyazi/pluginLearn/tree/main/demo1
4. References
References
- Android插件化开发指南——Hook技术(一)【长文】
- AssetManager.java
- 29讲玩转插件化:深入底层分析Android插件化原理,从0到1手写实现360插件化项目架构