2.主要内容
上一篇章已经讲了如何去使用插件中的资源。但是也仅限
本篇主要讲如何启动插件中的activity,并且插件中的activity可以正常使用插件中的资源文件(不包含layout)。
一:原理简述
1.activity中如何获取资源
上一篇章中,我们已经实现了在宿主中使用插件中的资源,但是有一个限制,只能通过我们自定义的resource才能获取到插件的资源。那么问题就来了,如果我们在插件actvitiy中使用资源文件的,获取到的resource是哪里额呢?
我们activity中调用getResource()方法,最终调用到的是ContextThemeWrapper中的getResources()方法,最终会调用到getResourcesInternal()。
@Override
public Resources getResources() {
return getResourcesInternal();
}
private Resources getResourcesInternal() {
if (mResources == null) {
if (mOverrideConfiguration == null) {
mResources = super.getResources();
} else {
final Context resContext = createConfigurationContext(mOverrideConfiguration);
mResources = resContext.getResources();
}
}
return mResources;
}
在看super.getResources(),最终调用的其实是ContextImpl中的mResources对象。
2.插件中如何获取资源
看了获取resource的流程,我们可以知道,如果替换掉activity最终获取getResources返回的对象,那么就可以实现在插件中获取插件中的资源内容。如何替换,我们发现有两个地方可以尝试hook。
1.ContextThemeWrapper中存有mResources的缓存,如果我们替换掉这个缓存,那么activity中获取到的就是我们自定义的mResources对象了。
2.在ContextThemeWrapper中的mResources创建之前,直接替换掉ContextImpl中的mResources。
考虑到影响范围,我们选择影响更小一点的第一种方案。既然确定了可以hook的点,那么下一步我们就考虑hook的时机了。
3.替换时机
时机肯定要在onCreate方法之前,因为插件中在onCreate方法中就会使用到很多资源文件。插件中的actiivty我们是不能去改造的,所以通过重写父类方法的方案也是不行的。
这时候我们又想到了Instrumentation,因为activity就是它来负责创建的,在创建后,在onCreate之前,我们就可以去操作。最终我选择callActivityOnCreate方法,activity的onCreate就是它来负责调用的,所以作为替换的时机最为合适。
二:代码编写
1.插件项目中创建Plugin3Activity
这里引用了插件中的string,name:plugin_str1
public class Plugin3Activity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Resources resources = getResources();
setContentView(R.layout.layout_plugin3);
String string = resources.getString(R.string.plugin_str1);
((TextView) findViewById(.text2)).setText(string);
}
}
layout也很简单:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http:///apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<TextView
android:id="@+id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="layout中直接使用字符串layout_plugin3" />
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/plugin_str3" />
</LinearLayout>
2.编译项目,拷贝apk
老步骤了,编译后拷贝APK到宿主的asset目录
3.宿主项目中,重写Instrumentation中的callActivityOnCreate方法
@Override
public void callActivityOnCreate(Activity activity, Bundle icicle) {
//替换掉resource
Resources plugin = DynamicResourceManager.getInstance().resourcesMap.get("plugin");
if (plugin != null) {
try {
Field declaredField = ContextThemeWrapper.class.getDeclaredField("mResources");
declaredField.setAccessible(true);
declaredField.set(activity, plugin);
} catch (Exception e) {
e.printStackTrace();
}
}
super.callActivityOnCreate(activity, icicle);
}
我们通过反射获取到mResources对象,然后替换成我们自定义的Resources。
4.启动Plugin3Activity
if (position == 5) {
//activity中使用插件的png和string
if (DynamicResourceManager.getInstance().resourcesMap["plugin"] == null) {
ToastUtil.showCenterToast("请先点击使用插件中的资源")
return
}
val intent = Intent(context, HostActivity::class.java)
intent.putExtra(MyInstrumentation.ClassName, "com.xt.appplugin.Plugin3Activity")
startActivity(intent)
return
}
5.修复资源崩溃
这时候点击启动,竟然意外的发现崩溃了,崩溃日志如下:
Caused by: android.content.res.Resources$NotFoundException: Resource ID #0x7f0b0001
at android.content.res.ResourcesImpl.getValueForDensity(ResourcesImpl.java:234)
at android.content.res.Resources.getDrawableForDensity(Resources.java:982)
at android.content.res.Resources.getDrawable(Resources.java:922)
at android.content.Context.getDrawable(Context.java:753)
at com.android.internal.widget.ToolbarWidgetWrapper.setIcon(ToolbarWidgetWrapper.java:322)
at com.android.internal.widget.ActionBarOverlayLayout.setIcon(ActionBarOverlayLayout.java:755)
at com.android.internal.policy.PhoneWindow.setDefaultIcon(PhoneWindow.java:1812)
at .Activity.initWindowDecorActionBar(Activity.java:3506)
at .Activity.setContentView(Activity.java:3521)
at com.xt.appplugin.Plugin4Activity.onCreate(Plugin4Activity.java:21)
at .Activity.performCreate(Activity.java:8051)
at .Activity.performCreate(Activity.java:8031)
at .Instrumentation.callActivityOnCreate(Instrumentation.java:1329)
at com.xt.client.function.dynamic.hook.MyInstrumentation.callActivityOnCreate(MyInstrumentation.java:64)
排查步骤就不说了,直接说原因吧。onCreate创建界面的时候,系统会去尝试创建actionBar,创建的时候会使用到APP中的图片资源。因为我们是在宿主中执行的,获取对应的图片资源ID也是宿主的,而这里的resouces我们已经替换成我们自定义的了,所以自定义的resouces肯定找不到宿主的图片资源,就崩溃了。
解决方案想到了两个:
第一,可以不让activity去加载actionBar。但是这个等于说对插件APP产生了要求,所以放弃了。
第二,自定义resouces中做一层补偿逻辑,如果自身获取不到,可以从宿主resources中获取。
最终采用的是第二种方案,重写Resouces方法的getDrawable方法。
@Override
public Drawable getDrawable(@DrawableRes int id, Theme theme)
throws NotFoundException {
try {
return getDrawableForDensity(id, 0, theme);
} catch (Exception e) {
}
return DynamicResourceManager.getInstance().resourcesMap.get("host").getDrawable(id, theme);
}
这里host是在加载资源插件的时候缓存的:
DynamicResourceManager.getInstance().resourcesMap["host"] = getResources()
6.测试验证
点击加载插件,然后点击启动插件activity(使用插件中的资源)。
我们可以看到插件已经正常启动了。
三:要点总结
1.activity中的resources,最终来自于ContextImpl,每个activity都存在一份缓存resources。
2.Activity和Application的Resouces对象并不是一个,本文虽然没介绍,但是调试过程中发现了。并且不同Activity的Resouces对象也不一定是一个。
3.替换掉Instrmentation之后,在callActivityOnCreate方法中,我们可以做很多事情,这是我们hook的最佳时机。因为此时activity中的各种对象都已经赋值,而onCreate恰好还没有执行。