【Android】Android插件开发 —— 打开插件的Activity(Hook系统方法)
Android打开插件中Activity的实现原理
摘要
Android打开插件Activity的方式有很多种,类名固定的可以使用预注册的方式。代理也是一种很好的方式,同时代理的方式也可以用于打开插件中的Service。
这两种方式都有一些弊端,这篇文章要分享的是如何更好地打开插件中的Activity,采用Instrumentation注入的方式。
1. Activity是如何被实例化的
1.1 在哪一个类、哪一个方法中实例化的
当开发者实现一个Activity时,不能自己添加一个带参数的构造方法。如果添加了,也需要实现一个无参构造方法。原因是在调用Context.startActivity(…)后,系统会利用反射的方式根据activityClass实例化一个Activity对象:
1
|
Activity activity = (Activity)clazz.newInstance();
|
其中clazz就是传入的Activity的子类。这一行代码是在Instrumentaion.java中的newActivity(…)方法中的。也就是说,Activity的实例化是在Instrumentation这个类里面,通过反射的方式进行的。
1.2 谁持有了Instrumentation的引用
每个Activity对象都持有了一个mInstrumentation的变量,该变量并不是Activity自己创建的,而是由ActivityThread传递给Activity的。而在ActivityThread中,持有了一个名为mInstrumentation的变量。因此,创建Activity的Instrumentation的对象,是被ActivityThread持有着。
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方法中抛出的:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
/** @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中注册的,直接打开肯定抛异常崩溃。读者可能会想:我要是直接重写这个方法,不管什么情况全部不抛异常,校验不就通过了吗?但是很抱歉,这个方法是static类型的,子类无法重写。这个条件我们没有办法解决,只能绕过去。
2.2 如何绕过Activity的注册校验
Instrumentation有一个很重要的事:checkStartActivityResult(…)方法 是在newActivity(…)方法之前执行的。请看这两个信息:
当开发者使用一个已经注册过的Activity去接受校验时,肯定能通过校验; 实例化出来的Activity一定是经过校验的那一个Activity吗?不一定。 让我们看一下Instrumentation#newActivity(…)方法的具体实现(有两个重载实现):
第一个:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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;
}
|
第二个:
1
2
3
4
5
6
|
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方法中做以下工作: 开始从intent中获得通过校验的Activity的类,赋值给replyActivityClassA == ActivityA ?从intent中获得插件中的Activity的类名,赋值给pluginActivityClassName根据类名反射获得对应的类, pluginActivityClass由C实例化出插件Activity对象pluginActivity返回说明并不是要打开插件中的Activity结束yesno
最后将pluginActivity就打开我们插件中的Activity了。修改后的代码如下:
其中,SonaInner.getRelayActivity() 就是事先约定好的中继Activity,也就是上面说得ActivityA。
另外,PluginManager.loadClassFromPlugin(…) 方法可以简单理解为是Class.forName()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
|
package
com.mzdxl.sona.hooks;
import
android.app.Activity;
import
android.app.Application;
import
android.content.Context;
import
android.content.Intent;
import
android.content.pm.ActivityInfo;
import
android.content.res.AssetManager;
import
android.content.res.Resources;
import
android.os.Bundle;
import
android.os.IBinder;
import
android.view.ContextThemeWrapper;
import
com.mzdxl.sona.Log;
import
com.mzdxl.sona.PluginManager;
import
com.mzdxl.sona.SonaInner;
import
java.lang.reflect.Field;
import
java.lang.reflect.Method;
/**
* @author 郑海鹏
* @since 2016/5/16 21:57
*/
public
class
InstrumentationHook
extends
android.app.Instrumentation{
/* -------------------------------------------------------------
Fields
------------------------------------------------------------- */
public
static
final
String PLUGIN_ACTIVITY_NAME =
"plugin_activity"
;
public
static
final
String PLUGIN_PATH =
"plugin_path"
;
protected
String pluginPath;
/* -------------------------------------------------------------
System Override / Implements Methods
------------------------------------------------------------- */
@Override
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 {
Log.i(
"CustomInstrumentation#newActivity 执行了!code 1"
);
Activity handleResult = createActivity(intent);
if
(handleResult !=
null
){
return
handleResult;
}
return
super
.newActivity(clazz, context, token, application, intent, info, title, parent, id, lastNonConfigurationInstance);
}
@Override
public
Activity newActivity(ClassLoader cl, String fromClassName, Intent intent)
throws
InstantiationException, IllegalAccessException, ClassNotFoundException {
Log.i(
"CustomInstrumentation#newActivity 执行了!code 2"
);
Activity handleResult = createActivity(intent);
if
(handleResult !=
null
){
handleResult.attach(context,
null
,
this
, token,
0
, application, intent,
info, title, parent, id,
(Activity.NonConfigurationInstances)lastNonConfigurationInstance,
new
Configuration(),
null
);
return
handleResult;
}`
return
super
.newActivity(cl, fromClassName, intent);
}
@Override
public
void
callActivityOnCreate(Activity activity, Bundle icicle) {
super
.callActivityOnCreate(activity, icicle);
injectResources(activity);
}
/* -------------------------------------------------------------
Methods
------------------------------------------------------------- */
/**
* 根据Intent中的class判断是否存在中继Activity,如果目标Activity是中继Activity则打开插件
*/
@SuppressWarnings
(
"unchecked"
)
protected
Activity createActivity(Intent intent){
SonaInner.checkInit();
// 获得Intent中要打开的Activity
String className = intent.getComponent().getClassName();
// 判断该Activity是否是中继Activity
if
(SonaInner.getRelayActivity().getName().equals(className)) {
// 如果是中继Activity,取出真实想启动的插件的Activity的类名、插件的位置
String pluginActivityName = intent.getStringExtra(PLUGIN_ACTIVITY_NAME);
pluginPath = intent.getStringExtra(PLUGIN_PATH);
// 实例化Activity
Class<!--?
extends
Activity--> PluginActivity;
try
{
if
(pluginPath ==
null
){
// 如果插件保存地址为null,不从插件中找
PluginActivity = (Class<!--?
extends
Activity-->) Class.forName(pluginActivityName);
}
else
{
PluginActivity = PluginManager.loadClassFromPlugin(getContext(), pluginPath, pluginActivityName);
}
return
PluginActivity ==
null
?
null
: PluginActivity.newInstance();
}
catch
(Exception e) {
e.printStackTrace();
Log.e(
"Intent中传入的插件的Class名无法实例化, 或者Class名不是一个Activity的类名,或者对应插件中不包含这个Activity。"
);
}
}
return
null
;
}
/**
* 注入插件的资源
*/
protected
void
injectResources(Activity activity){
if
(pluginPath ==
null
){
return
;
}
// 获取Activity的Resource资源
Resources hostResource = activity.getApplication().getResources();
// 获取插件的Resource
try
{
// 获得系统assetManager
AssetManager assetManager = AssetManager.
class
.newInstance();
// 将插件地址添加到资源地址
Method method_addAssetPath = AssetManager.
class
.getDeclaredMethod(
"addAssetPath"
, String.
class
);
method_addAssetPath.setAccessible(
true
);
method_addAssetPath.invoke(assetManager, pluginPath);
// 获得新的完整的资源
Resources resources =
new
Resources(assetManager, hostResource.getDisplayMetrics(), hostResource.getConfiguration());
Field field_mResources = ContextThemeWrapper.
class
.getDeclaredField(
"mResources"
);
field_mResources.setAccessible(
true
);
field_mResources.set(activity, resources);
}
catch
(Exception e) {
e.printStackTrace();
Log.e(
"复制插件Resource时出现异常"
);
}
}
}
|
2.3 如何用自己的newActivity方法替换系统方法
在1.2中已经提到了,Instrumentation的对象是保存在ActivityThread里的。很不幸,ActivityThread对象我们无法直接获得到,同时连ActivityThread这个类我们在代码中都不能直接调用。ActivityThread有一个静态方法:
1
2
3
|
public
static
ActivityThread currentActivityThread() {
return
sCurrentActivityThread;
}
|
该方法返回了当前的ActivityThread。
我们可以通过反射的方式,先获得ActivityThread,再获得currentActivityThread方法,最后调用这个方法就可以获得ActivityThread的变量(设为activityThread)了。最后把我们自己的Instrumentation对象替换掉activityThread里的mInstrumentation就可以了。代码如下:
其中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
|
package
com.mzdxl.sona;
import
android.content.res.AssetManager;
import
com.mzdxl.sona.hooks.InstrumentationHook;
import
java.lang.reflect.Field;
import
java.lang.reflect.InvocationTargetException;
import
java.lang.reflect.Method;
/**
* @author 郑海鹏
* @since 2016/5/17 10:25
*/
public
class
HookManager {
/* -------------------------------------------------------------
Fields
------------------------------------------------------------- */
static
Class<!--?--> ActivityThread;
static
Method ActivityThread_method_currentActivityThread;
static
Object obj_activityThread;
static
Method AssetManager_method_addAssetPath;
/* -------------------------------------------------------------
Static Methods
------------------------------------------------------------- */
/**
* 初始化操作,获得一些基本的类、变量、方法等。
*/
public
static
void
init()
throws
ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
// 获得ActivityThread类
ActivityThread = Class.forName(
"android.app.ActivityThread"
);
// 获得ActivityThread#currentActivityThread()方法
ActivityThread_method_currentActivityThread = ActivityThread.getDeclaredMethod(
"currentActivityThread"
);
// 根据currentActivityThread方法获得ActivityThread对象
obj_activityThread = ActivityThread_method_currentActivityThread.invoke(ActivityThread);
// 获得AssetManager#addAssetPath()方法
AssetManager_method_addAssetPath = AssetManager.
class
.getDeclaredMethod(
"addAssetPath"
, String.
class
);
AssetManager_method_addAssetPath.setAccessible(
true
);
}
/**
* 注入Sona的Instrumentation
*/
public
static
void
injectInstrumentation()
throws
NoSuchFieldException, IllegalAccessException {
Log.i(
"开始注入Sona的Instrumentation。"
);
// 获得ActivityThread类中的Instrumentation字段
Field field_instrumentation = obj_activityThread.getClass().getDeclaredField(
"mInstrumentation"
);
field_instrumentation.setAccessible(
true
);
// 创建出一个新的Instrumentation
InstrumentationHook obj_custom_instrumentation =
new
InstrumentationHook();
// 用Instrumentation字段注入Sona的Instrumentation变量
field_instrumentation.set(obj_activityThread, obj_custom_instrumentation);
}
}
|
最后我们需要在自己Application中的onCreate()方法中调用HookManager.init() 和 HookManager.injectInstrumentation()就可以替换为我们自己的Instrumentation了。