Android打开插件中Activity的实现原理
摘要
Android打开插件Activity的方式有很多种,类名固定的可以使用预注册的方式。代理也是一种很好的方式,同时代理的方式也可以用于打开插件中的Service。这两种方式在之前的博客中都有分享:
- 预注册的方式打开插件Activity:http://blog.csdn.net/h28496/article/details/49966503
- 代理的方式打开插件中的Activity:http://blog.csdn.net/h28496/article/details/50414873
这两种方式都有一些弊端,这篇文章要分享的是如何更好地打开插件中的Activity,采用Instrumentation注入的方式。
其大致步骤为:
- 在创建Intent时,目标Activity为已注册的Activity(设为ActivityA);
- 在 intent 中 put 插件Activity的信息(设为PluginActivity);
- 继承Instrumentation,实现子类(设为InstrumentationHook),重写newActivity()方法,使得要实例化ActivityA时,实例化PluginActivity;
- 在Application的onCreate()方法中,利用反射,通过ActivityThread的currentActivityThread()方法获得ActivityThread的实例(该实例会传递给后续创建的所有Activity);
- 通过反射的方式,替换ActivityThread实例中的mInstrumentation变量(原类型是Instrumentation,替换为子类InstrumentationHook的变量)
1. Activity是如何被实例化的
1.1 在哪一个类、哪一个方法中实例化的
当开发者实现一个Activity时,不能自己添加一个带参数的构造方法。如果添加了,也需要实现一个无参构造方法。原因是在调用Context#startActivity(…)后,系统会利用反射的方式根据activityClass实例化一个Activity对象:
Activity activity = (Activity)clazz.newInstance();
其中clazz就是传入的Activity的子类。这一行代码在Instrumentaion.java中的newActivity(…)方法中。可以看出,Activity的实例化是在Instrumentation这个类里面,通过反射的方式进行的。
1.2 谁持有了Instrumentation的引用
每个Activity对象都持有了一个mInstrumentation的变量,该变量是由ActivityThread传递给Activity的。在ActivityThread中,有了一个名为mInstrumentation的变量。
2. 如何偷梁换柱打开插件的Activity
2.1 Activity是否在AndroidManifest.xml注册的校验
如果一个Activity没有注册,想要打开它,会抛出异常:
Unable to find explicit activity class XXXXActivity have you declared this activity in your AndroidManifest.xml?
这个异常是Instrumentation的checkStartActivityResult方法抛出的:
/** @hide */
public static void checkStartActivityResult(int res, Object intent) {
if (res >= ActivityManager.START_SUCCESS) {
return;
}
switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND:
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
...(以下省略)
}
}
很明显,插件中的Activity是没有在AndroidManifest.xml中注册的,直接打开肯定抛异常崩溃。同时由于checkStartActivityResult方法是静态方法,所以不能通过重写这个方法绕过校验。
2.2 如何绕过Activity的注册校验
在Instrumentation中,checkStartActivityResult(…)方法 是在 newActivity(…)方法之前执行的。由于:
1. 当开发者使用一个已经注册过的Activity去接受校验时,肯定能通过校验;
2. 实例化出来的Activity不一定是经过校验的那一个Activity。
让我们看一下Instrumentation#newActivity(…)方法的具体实现(有两个重载实现):
第一个:
public Activity newActivity(Class<?> clazz, Context context,
IBinder token, Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
Object lastNonConfigurationInstance) throws InstantiationException,
IllegalAccessException {
Activity activity = (Activity)clazz.newInstance();
ActivityThread aThread = null;
activity.attach(context, aThread, this, token, 0, application, intent,
info, title, parent, id,
(Activity.NonConfigurationInstances)lastNonConfigurationInstance,
new Configuration(), null);
return activity;
}
第二个:
public Activity newActivity(ClassLoader cl, String className,
Intent intent)
throws InstantiationException, IllegalAccessException,
ClassNotFoundException {
return (Activity)cl.loadClass(className).newInstance();
}
注意参数列表中有一个 Intent intent 参数,这个参数就是我们在startActivity(Intent intent);传入的那个intent。两个重载方法的参数列表中都包含了这个intent。
所以绕过注册验证的思路大致就出来了:
- 第一步:
Intent intent = new Intent(this, 某个已经注册的ActivityA);
intent.putString(“插件中的的Activity的类名”); - 系统在接受startActivity后,校验”已经注册Activity”是否注册,结果肯定是已经注册,顺利通过;
- 接着来到newActivity(…)方法准备实例化Activity
我们通过反射的方式,事先替换掉newActivity(…)方法,改为我们自己的方法。在我们自己的newActivity方法中做以下工作: