序:
1.本文是安卓插件化课程的第七篇,完整课程链接参见下面链接:
2.主要内容
前面几篇我们讲了,我们宿主启动插件Activity,使用的是启动HostActivity的方式。在宿主中给启动插件的Activity,我们可以改为启动HostActivity的方式,但是插件中怎么办呢?插件中的启动方式是startActivity已经是固定不变的了。
所以在插件中使用老的方式启动,仍然会遇到activity not found的问题,而本文就是解决这个问题。
一:原理简述
1.如何Hook
具体的流程前面有说过,这里就不详细介绍了。我们只要知道启动activity时,最终都是通过Instrumentation的execStarActivity的方法去启动的。既然前面我们已经hook了Instrumentation,那么我们是不是可以重写这个方法呢?
重写了这个方法,然后本来要跳转AActivity的,我强行改为跳转HostActivity并附带参数,这样就可以走前几章我们的hook流程了。
二:代码编写
1.重写execStarActivity方法
在重写方法里面我们要做两件事:
首先,要判断下,即将要加载的activity,是不是插件中的?因为只有插件中的才需要被代理。
第二,我们要把改完之后的数据,传递回Instrumentation的execStarActivity方法,让后续流程继续执行。
2.如何判断是否代理?
第一个问题,我的想法是这样的,如果是插件中的activity,那么一定是自定义classLoader加载的,所以只要通过自定义classLoader去尝试加载一下这个类,如果存在,那么就进行代理。不存在,则不需要代理。
代码如下:
//判断是否拦截,拦截的标准是插件的classLoader中是否存在该类
try {
Object classLoader = ObjectCache.getInstance().getParams(ClassLoader);
if (classLoader instanceof ClassLoader) {
ComponentName component = intent.getComponent();
String className = component.getClassName();
Class<?> aClass = ((ClassLoader) classLoader).loadClass(className);//不抛异常就是正常的
intent.setClass(target, HostActivity.class);
intent.putExtra(MyInstrumentation.ClassName, className);
}
} catch (Exception e) {
e.printStackTrace();
}
3.如何通知回父类方法?
本来是很简单的,只要super.execStartActivity 就可以了,但是很不幸,execStartActivity方法是UnsupportedAppUsage类型的,所以是无法正常调用的。我们需要通过反射去调用父类的方法。
因为我们调用的是父类中的方法,所以这时我们就需要用MethodHandle出马了。通过它来调用父类中的方法
MethodHandle h1;//通过methodHandle调用父类的同名方法
@RequiresApi(api = Build.VERSION_CODES.O)
@SuppressLint("SoonBlockedPrivateApi")
public MyInstrumentation() {
try {
MethodHandles.Lookup lookup = MethodHandles.lookup();
h1 = lookup.findSpecial(Instrumentation.class, "execStartActivity", MethodType.methodType(ActivityResult.class, Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class), MyInstrumentation.class);
} catch (Exception e) {
e.printStackTrace();
}
}
//替换这个方法
@RequiresApi(api = Build.VERSION_CODES.O)
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
Log.i("lxltest", "");
//判断是否拦截,拦截的标准是插件的classLoader中是否存在该类
try {
Object classLoader = ObjectCache.getInstance().getParams(ClassLoader);
if (classLoader instanceof ClassLoader) {
ComponentName component = intent.getComponent();
String className = component.getClassName();
Class<?> aClass = ((ClassLoader) classLoader).loadClass(className);//不抛异常就是正常的
intent.setClass(target, HostActivity.class);
intent.putExtra(MyInstrumentation.ClassName, className);
}
} catch (Exception e) {
e.printStackTrace();
}
Object invoke = null;
try {
invoke = h1.invoke(this, who, contextThread, token, target, intent, requestCode, options);
} catch (Throwable e) {
e.printStackTrace();
}
return (ActivityResult) invoke;
}
4.插件项目中写一个跳转,然后编译插件APK,拷贝
比如我这里写了一个MainActivity跳转Plugin5Activity的。
public class MainActivity extends Activity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_main);
findViewById(R.id.text1).setOnClickListener(v -> {
PluginJni pluginJni = new PluginJni();
String s = pluginJni.pluginSpliceString("hello", "world");
((TextView) findViewById(R.id.result)).setText(s);
});
findViewById(R.id.text2).setOnClickListener(v -> {
Intent intent = new Intent(MainActivity.this, Plugin5Activity.class);
startActivity(intent);
});
}
}
5.验证效果。
在宿主中,首先我们启动插件中的MainActivity。这时候显示两个按钮。
我们点击按钮2,这时候我们发现正常跳转了Plugin5Activity,说明我们实验成功。
当然,这里我们发现了一个问题,第二个按钮显示的文本是不对的。这个我们下一章再讲如何去解决。
三:要点总结
无
四。代码地址:
项目地址:
GitHub - aa5279aa/android_all_demo: 一直觉得研究各种技术,一个个demo的下载运行太费劲了,为什么不能有一个所有新技术的融合体demo呢?项目为此而生
插件项目位置:android_all_demo/DemoClient/appplugin at master · aa5279aa/android_all_demo · GitHub
调用类位置:android_all_demo/DynamicFragment.kt at master · aa5279aa/android_all_demo · GitHub