Android Hook无清单启动Activity的应用

我们已经实现了启动没有在menifest中注册的Activity的效果,然而,这样做到底在生产开发中有什么样的应用呢?

答案:插件化

插件化是一个宽泛的概念,只要是实现了宿主app上插件功能的灵活拔插,实现了宿主app业务和插件功能的完全解耦,就可以称之为插件化

原理是用宿主中真实Activity作为代理,来启动插件中的Activity,管理插件中Activity的生命周期,并且处理好插件源代码资源文件

现在,插件化有另一种方式,就是利用无清单启动Activity的原理,实现插件apk中Activity的启动

整体思路

下方有两张图:表示了插件化架构中,插件单独运行,和插件作为宿主的一部分随宿主启动的技术关键点

如上图,如果跟随宿主一起启动,插件apk的资源文件要能够被宿主读到,插件的apkclass文件也必须能够被宿主读取,实现的方式就是,让在宿主的代码中进行hook编程,生成一个能够读取宿主以及所有插件内classClassLoader,以及一个能够读取宿主以及插件内所有资源的Resource。而,实现的具体过程,就是一个融合过程。

实际效果展示

绕过Manifest检测的插件化启动

宿主manifest文件:只有一个入口Activity,其他的一概没有

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<application
android:name=".app.MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".ui.MainActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

Demo源码讲解

无论是宿主的代码,还是插件的代码,都非常简单,唯一阅读价值的,就是宿主的Hook核心代码

在讲解Hook核心代码之前,先回顾一下所实现的效果:能够绕过系统的manifest检测机制,让没有在manifest中注册的Activity也能够正常启动
一定有读者在看完这篇文章之后,会想,能够不去注册就可以启动Activity,是很神奇,但是又有什么利用价值呢?仅仅是为了不去注册就去干涉系统逻辑,太华而不实了

这个问题的答案:
hook实现插件化启动Activity,插件中的manifest并不会和宿主的manifest发生融合,也就是说,即使我们完成了对ClassLoaderResource的融合,实现了宿主对插件class资源的访问,如果不能绕过系统的manifest检测,依然不能启动插件的Activity

所以,用hook技术实现插件化启动Activity,完整思路是:

以下是关键代码:

宿主的MyApplication.java主要用于调用Hook核心代码:

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
public class MyApplication extends Application {

private Resources newResource;

public static String pluginPath = null;

@Override
public void onCreate() {
super.onCreate();
pluginPath = AssetUtil.copyAssetToCache(this, Const.PLUGIN_FILE_NAME);

//Hook第一次,绕过manifest检测
GlobalActivityHookHelper.hook(this);

//Hook第二次把插件的源文件class导入到系统的ClassLoader中
HookInjectHelper.injectPluginClass(this);

//Hook第三次,加载插件资源包,让系统的Resources能够读取插件的资源
newResource = HookInjectHelper.injectPluginResources(this);
}

//重写资源管理器,资源管理器是每个Activity自带的,
//而Application的getResources则是所有Activity共有的
//重写了它,就不必一个一个Activity去重写了
@Override
public Resources getResources() {
return newResource == null ? super.getResources() : newResource;
}
}

绕过manifest检测的hook核心代码GlobalActivityHookHelper.java

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
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Handler;
import android.os.Message;
import android.util.Log;

import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

import cn.appblog.hookplugindemo.utils.Util;

/**
* hookAMS Activity的实现方式3:
* hookAMS AMS(ActivityManagerService)兼容 26以上,以及26以下的版本(SDK 26对AMS实例的获取进行了代码更改)
* 今天,在已经能够实现全局hook MS的方案下,进一步改造,实现 无清单启动Activity
*/
public class GlobalActivityHookHelper {

public static void hook(Context context) {

hookAMS(context);//使用假的Activity,骗过AMS的检测

if (ifSdkOverIncluding28())
hookActivityThread_mH_AfterIncluding28();//将真实的Intent还原回去,让系统可以跳到原本该跳的地方.
else {
hookActivityThread_mH_before28(context);
}

hookPM(context);//由于AppCompatActivity存在PMS检测,如果这里不hook的话,就会包PackageNameNotFoundException
}

//设备系统版本是不是大于等于26
private static boolean ifSdkOverIncluding26() {
int SDK_INT = Build.VERSION.SDK_INT;
if (SDK_INT > 26 || SDK_INT == 26) {
return true;
} else {
return false;
}
}

//设备系统版本是不是大于等于26
private static boolean ifSdkOverIncluding28() {
int SDK_INT = Build.VERSION.SDK_INT;
if (SDK_INT > 28 || SDK_INT == 28) {
return true;
} else {
return false;
}
}

/**
* 这里对AMS进行hook
*
* @param context
*/
private static void hookAMS(Context context) {
try {
Class<?> ActivityManagerClz;
final Object IActivityManagerObj;//这个就是AMS实例
Method getServiceMethod;
Field IActivityManagerSingletonField;
if (ifSdkOverIncluding26()) {//26,27,28的ams获取方式是通过ActivityManager.getService()
ActivityManagerClz = Class.forName("android.app.ActivityManager");
getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService");
IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("IActivityManagerSingleton");//单例类成员的名字也不一样
} else {//25往下,是ActivityManagerNative.getDefault()
ActivityManagerClz = Class.forName("android.app.ActivityManagerNative");
getServiceMethod = ActivityManagerClz.getDeclaredMethod("getDefault");
IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("gDefault");//单例类成员的名字也不一样
}
IActivityManagerObj = getServiceMethod.invoke(null);//OK,已经取得这个系统自己的AMS实例

// 2.现在创建我们的AMS实例
// 由于IActivityManager是一个接口,那么其实我们可以使用Proxy类来进行代理对象的创建
// 结果被摆了一道,IActivityManager这玩意居然还是个AIDL,动态生成的类,编译器还不认识这个类,怎么办?反射咯
Class<?> IActivityManagerClz = Class.forName("android.app.IActivityManager");

// 构建代理类需要两个东西用于创建伪装的Intent
String packageName = Util.getPMName(context);
String clz = Util.getHostClzName(context, packageName);
Object proxyIActivityManager =
Proxy.newProxyInstance(
Thread.currentThread().getContextClassLoader(),
new Class[]{IActivityManagerClz},
new ProxyInvocation(IActivityManagerObj, packageName, clz));

//3.拿到AMS实例,然后用代理的AMS换掉真正的AMS,代理的AMS则是用 假的Intent骗过了 activity manifest检测.
//偷梁换柱
IActivityManagerSingletonField.setAccessible(true);
Object IActivityManagerSingletonObj = IActivityManagerSingletonField.get(null);
Class<?> SingletonClz = Class.forName("android.util.Singleton");//反射创建一个Singleton的class
Field mInstanceField = SingletonClz.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
mInstanceField.set(IActivityManagerSingletonObj, proxyIActivityManager);

} catch (Exception e) {
e.printStackTrace();
}
}

private static final String ORI_INTENT_TAG = "origin_intent";

/**
* 把InvocationHandler的实现类提取出来,因为这里包含了核心技术逻辑,最好独立,方便维护
*/
private static class ProxyInvocation implements InvocationHandler {

Object amsObj;
String packageName;//这两个String是用来构建Intent的ComponentName的
String clz;

public ProxyInvocation(Object amsInstance, String packageName, String clz) {
this.amsObj = amsInstance;
this.packageName = packageName;
this.clz = clz;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//proxy是创建出来的代理类,method是接口中的方法,args是接口执行时的实参
if (method.getName().equals("startActivity")) {
Log.d("GlobalActivityHook", "全局hook 到了 startActivity");

Intent currentRealIntent = null;//侦测到startActivity动作之后,把intent存到这里
int intentIndex = -1;
//遍历参数,找到Intent
for (int i = 0; i < args.length; i++) {
Object temp = args[i];
if (temp instanceof Intent) {
currentRealIntent = (Intent) temp;//这是原始的Intent,存起来,后面用得着
intentIndex = i;
break;
}
}

//构造自己的Intent,这是为了绕过manifest检测
Intent proxyIntent = new Intent();
ComponentName componentName = new ComponentName(packageName, clz);//用ComponentName重新创建一个intent
proxyIntent.setComponent(componentName);
proxyIntent.putExtra(ORI_INTENT_TAG, currentRealIntent);//将真正的proxy作为参数,存放到extras中,后面会拿出来还原

args[intentIndex] = proxyIntent;//替换掉intent
//哟,已经成功绕过了manifest清单检测. 那么,我不能老让它跳到 伪装的Activity啊,我要给他还原回去,那么,去哪里还原呢?
//继续看源码。

}
return method.invoke(amsObj, args);
}
}

//下面进行ActivityThread的mH的hook,这是针对SDK28做的hook
private static void hookActivityThread_mH_AfterIncluding28() {

try {
//确定hook点,ActivityThread类的mh
// 先拿到ActivityThread
Class<?> ActivityThreadClz = Class.forName("android.app.ActivityThread");
Field field = ActivityThreadClz.getDeclaredField("sCurrentActivityThread");
field.setAccessible(true);
Object ActivityThreadObj = field.get(null);//OK,拿到主线程实例

//现在拿mH
Field mHField = ActivityThreadClz.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mHObj = (Handler) mHField.get(ActivityThreadObj);//ok,当前的mH拿到了
//再拿它的mCallback成员
Field mCallbackField = Handler.class.getDeclaredField("mCallback");
mCallbackField.setAccessible(true);

//2.现在,造一个代理mH,
// 他就是一个简单的Handler子类
ProxyHandlerCallback proxyMHCallback = new ProxyHandlerCallback();//错,不需要重写全部mH,只需要对mH的callback进行重新定义

//3.替换
//将Handler的mCallback成员,替换成创建出来的代理HandlerCallback
mCallbackField.set(mHObj, proxyMHCallback);

} catch (Exception e) {
e.printStackTrace();
}
}

private static class ProxyHandlerCallback implements Handler.Callback {

private int EXECUTE_TRANSACTION = 159;//这个值,是android.app.ActivityThread的内部类H 中定义的常量EXECUTE_TRANSACTION

@Override
public boolean handleMessage(Message msg) {
boolean result = false;//返回值,请看Handler的源码,dispatchMessage就会懂了
//Handler的dispatchMessage有3个callback优先级,首先是msg自带的callback,其次是Handler的成员mCallback,最后才是Handler类自身的handlerMessage方法,
//它成员mCallback.handleMessage的返回值为true,则不会继续往下执行 Handler.handlerMessage
//我们这里只是要hook,插入逻辑,所以必须返回false,让Handler原本的handlerMessage能够执行.
if (msg.what == EXECUTE_TRANSACTION) {//这是跳转的时候,要对intent进行还原
try {
//先把相关@hide的类都建好
Class<?> ClientTransactionClz = Class.forName("android.app.servertransaction.ClientTransaction");
Class<?> LaunchActivityItemClz = Class.forName("android.app.servertransaction.LaunchActivityItem");

Field mActivityCallbacksField = ClientTransactionClz.getDeclaredField("mActivityCallbacks");//ClientTransaction的成员
mActivityCallbacksField.setAccessible(true);
//类型判定,好习惯
if (!ClientTransactionClz.isInstance(msg.obj)) return true;
Object mActivityCallbacksObj = mActivityCallbacksField.get(msg.obj);//根据源码,在这个分支里面,msg.obj就是 ClientTransaction类型,所以,直接用
//拿到了ClientTransaction的List<ClientTransactionItem> mActivityCallbacks;
List list = (List) mActivityCallbacksObj;

if (list.size() == 0) return true;
Object LaunchActivityItemObj = list.get(0);//所以这里直接就拿到第一个就好了

if (!LaunchActivityItemClz.isInstance(LaunchActivityItemObj)) return true;
//这里必须判定 LaunchActivityItemClz,
// 因为 最初的ActivityResultItem传进去之后都被转化成了这LaunchActivityItemClz的实例

Field mIntentField = LaunchActivityItemClz.getDeclaredField("mIntent");
mIntentField.setAccessible(true);
Intent mIntent = (Intent) mIntentField.get(LaunchActivityItemObj);
Intent oriIntent = (Intent) mIntent.getExtras().get(ORI_INTENT_TAG);
//那么现在有了最原始的intent,应该怎么处理呢?
Log.d("1", "2");
mIntentField.set(LaunchActivityItemObj, oriIntent);
return result;
} catch (Exception e) {
e.printStackTrace();
}
}
return result;
}
}

/**
* @param context
* @throws Exception
*/
private static void hookActivityThread_mH_before28(Context context) {
try {
Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");
Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField("sCurrentActivityThread");
sCurrentActivityThreadField.setAccessible(true);
Object sCurrentActivityThreadObj = sCurrentActivityThreadField.get(null);

Field mHField = activityThreadClazz.getDeclaredField("mH");
mHField.setAccessible(true);
Handler mH = (Handler) mHField.get(sCurrentActivityThreadObj);
Field callBackField = Handler.class.getDeclaredField("mCallback");
callBackField.setAccessible(true);
callBackField.set(mH, new ActivityThreadHandlerCallBack(context));
} catch (Exception e) {
e.printStackTrace();
}

}

public static class ActivityThreadHandlerCallBack implements Handler.Callback {

private final Context mContext;

public ActivityThreadHandlerCallBack(Context context) {
mContext = context;
}

@Override
public boolean handleMessage(Message msg) {
int LAUNCH_ACTIVITY = 0;
try {
Class<?> clazz = Class.forName("android.app.ActivityThread$H");
Field field = clazz.getField("LAUNCH_ACTIVITY");
LAUNCH_ACTIVITY = field.getInt(null);
} catch (Exception e) {
}
if (msg.what == LAUNCH_ACTIVITY) {
handleLaunchActivity(mContext, msg);
}
return false;
}
}

private static void handleLaunchActivity(Context context, Message msg) {
try {
Object obj = msg.obj;
Field intentField = obj.getClass().getDeclaredField("intent");
intentField.setAccessible(true);
Intent proxyIntent = (Intent) intentField.get(obj);
//拿到之前真实要被启动的Intent 然后把Intent换掉
Intent originallyIntent = proxyIntent.getParcelableExtra(ORI_INTENT_TAG);
if (originallyIntent == null) {
return;
}
proxyIntent.setComponent(originallyIntent.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
}


/**
* 由于我只在SDK 28 对应的9.0设备上做过成功的试验,所以此方法命名为hookPMAfter28
*
* @param context
*/
private static void hookPM(Context context) {
try {
String pmName = Util.getPMName(context);
String hostClzName = Util.getHostClzName(context, pmName);

Class<?> forName = Class.forName("android.app.ActivityThread");//PM居然是来自ActivityThread
Field field = forName.getDeclaredField("sCurrentActivityThread");
field.setAccessible(true);
Object activityThread = field.get(null);
Method getPackageManager = activityThread.getClass().getDeclaredMethod("getPackageManager");
Object iPackageManager = getPackageManager.invoke(activityThread);

String packageName = Util.getPMName(context);
PMSInvocationHandler handler = new PMSInvocationHandler(iPackageManager, packageName, hostClzName);
Class<?> iPackageManagerIntercept = Class.forName("android.content.pm.IPackageManager");
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new
Class<?>[]{iPackageManagerIntercept}, handler);
// 获取 sPackageManager 属性
Field iPackageManagerField = activityThread.getClass().getDeclaredField("sPackageManager");
iPackageManagerField.setAccessible(true);
iPackageManagerField.set(activityThread, proxy);
} catch (
Exception e)

{
e.printStackTrace();
}
}

static class PMSInvocationHandler implements InvocationHandler {

private Object base;
private String packageName;
private String hostClzName;

public PMSInvocationHandler(Object base, String packageName, String hostClzName) {
this.packageName = packageName;
this.base = base;
this.hostClzName = hostClzName;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

if (method.getName().equals("getActivityInfo")) {
ComponentName componentName = new ComponentName(packageName, hostClzName);
return method.invoke(base, componentName, PackageManager.GET_META_DATA, 0);//破费,一定是这样
}

return method.invoke(base, args);
}
}

}

将宿主和插件的ClassLoader/Resource融合的HookInjectHelper.java

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
import android.content.Context;
import android.content.res.AssetManager;
import android.content.res.Resources;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

import dalvik.system.DexClassLoader;
import dalvik.system.PathClassLoader;
import cn.appblog.hookplugindemo.app.MyApplication;

public class HookInjectHelper {

/**
*
* 此方法的作用是:插件内的class融合到宿主的classLoader中,让宿主可以直接读取插件内的class
*
* @param context
*/
public static void injectPluginClass(Context context) {
String cachePath = context.getCacheDir().getAbsolutePath();
String apkPath = MyApplication.pluginPath;

//还记不记得dexClassLoader?它是专门用于加载外部apk的classes.dex文件的
//(String dexPath, String optimizedDirectory, String librarySearchPath, ClassLoader parent)
// 4个参数分别是,外部dex的path,优化之后的目录,lib库文件查找目录,我们这没有用到lib里面的so,所以可以设置为null,最后一个是父ClassLoader
DexClassLoader dexClassLoader = new DexClassLoader(apkPath, cachePath, null, context.getClassLoader());
//先构造一个能够读取外部apk的classLoader对象

//第一步 找到插件的Elements数组dexPathlist dexElement

try {
Class myDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field myPathListFiled = myDexClazzLoader.getDeclaredField("pathList");
myPathListFiled.setAccessible(true);
Object myPathListObject = myPathListFiled.get(dexClassLoader);

Class myPathClazz = myPathListObject.getClass();
Field myElementsField = myPathClazz.getDeclaredField("dexElements");
myElementsField.setAccessible(true);
//自己插件的 dexElements[]
Object myElements = myElementsField.get(myPathListObject);

//第二步 找到系统的Elements数组dexElements
PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
Class baseDexClazzLoader = Class.forName("dalvik.system.BaseDexClassLoader");
Field pathListFiled = baseDexClazzLoader.getDeclaredField("pathList");
pathListFiled.setAccessible(true);
Object pathListObject = pathListFiled.get(pathClassLoader);

Class systemPathClazz = pathListObject.getClass();
Field systemElementsField = systemPathClazz.getDeclaredField("dexElements");
systemElementsField.setAccessible(true);
//系统的dexElements[]
Object systemElements = systemElementsField.get(pathListObject);
//第三步 上面的dexElements数组合并成新的dexElements然后通过反射重新注入系统的Field(dexElements)变量中

//新的Element[]对象
//dalvik.system.Element

int systemLength = Array.getLength(systemElements);
int myLength = Array.getLength(myElements);
//找到 Element 的Class类型数组每一个成员的类型
Class<?> sigleElementClazz = systemElements.getClass().getComponentType();
int newSysteLength = myLength + systemLength;
Object newElementsArray = Array.newInstance(sigleElementClazz, newSysteLength);
//融合
for (int i = 0; i < newSysteLength; i++) {
//先融合插件的Elements
if (i < myLength) {
Array.set(newElementsArray, i, Array.get(myElements, i));
} else {
Array.set(newElementsArray, i, Array.get(systemElements, i - myLength));
}
}
Field elementsField = pathListObject.getClass().getDeclaredField("dexElements");

elementsField.setAccessible(true);
//将新生成的EleMents数组对象重新放到系统中去
elementsField.set(pathListObject, newElementsArray);

} catch (Exception e) {
e.printStackTrace();
}
}

public static Resources injectPluginResources(Context context) {
AssetManager assetManager;
Resources newResource = null;
String apkPath = MyApplication.pluginPath;
try {
assetManager = AssetManager.class.newInstance();
Method addAssetPathMethod = assetManager.getClass().getDeclaredMethod("addAssetPath", String.class);
addAssetPathMethod.setAccessible(true);
addAssetPathMethod.invoke(assetManager, apkPath);
Resources supResource = context.getResources();
newResource = new Resources(assetManager, supResource.getDisplayMetrics(), supResource.getConfiguration());
} catch (Exception e) {
e.printStackTrace();
}
return newResource;
}
}

关于Resource的融合,在文章:Android hook技术实现一键换肤 里面有提及
绕过manifest检测,在另一篇文章 Android Hook-实现无清单启动Activity 有详解,这里不再赘述

详细讲讲ClassLoader如何融合

我们用context.getClassLoader拿到的是PathClassLoader,而我们构建能够访问插件中classclassLoaderDexClassLoader,它们有共同的父类BaseDexClassLoader,而且,这个BaseDexClassLoader类的本身就拥有能够装载多个dex路径的能力。

插件DexClassLoader读取的是插件apk中的classes.dex,宿主PathClassLoader读取的是data/app/包名/base.apkclasses.dex。它们分别将读取到的路径,存到了上图中的Element[] dexElements数组中

那么如果我们可以将插件DexClassLoader中的dexElements融合到宿主PathClassLoaderdexElements中去,就可以实现宿主读取插件apkclass.dex

HookInjectHelper类中的injectPluginClass方法,就是以上面的思路为依据进行的hook。具体步骤为:

    1. 构建插件DexClassLoader对象
    1. 获得系统的PathClassLoader对象
    1. 分别获得插件DexClassLoader和系统PathClassLoaderDexPathList中的dexElements数组
    1. 将上述两个dexElements数组进行融合
    1. 将融合之后的的dexElements设置到系统PathClassLoader

至此,系统也能够访问插件apk中的class了

那么接下来,如何启动插件中的Activity呢?

由于我们在写宿主代码的时候,并不能直接引用插件的类,所以我们只能通过如下方式:

1
2
3
4
5
6
7
8
9
findViewById(R.id.btn1).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent();
intent.setComponent(new ComponentName("cn.appblog.plugin", //插件的包名
"cn.appblog.plugin.Plugin1Activity")); //插件Activity的完整类名
startActivity(intent);
}
});

那么又如何启动宿主自身的Activity其他呢?可以按照上面的方式。或者也可以用普通的方式:

1
2
3
4
5
6
7
findViewById(R.id.button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
Intent intent = new Intent(MainActivity.this, Main2Activity.class);
startActivity(intent);
}
});

而宿主的manifest里,依然只有一个Activity,其他的都可以不经注册直接启动,剩下的这一个是为了作为Launch Activity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<application
android:name=".app.MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<activity android:name=".ui.MainActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

OK,全部讲完

坑坑更健康

细心的读者会发现,在宿主里面用的是android.app.Activity,而不是AppCompatActivity。包括宿主内的第二个Main2Activity,依然是android.app.Activity

因为发现,如果换成AppCompatActivity,在启动宿主的时候,就会报莫名其妙的异常:

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
03-09 18:39:19.069 16437-16437/cn.appblog.hookplugindemo E/AndroidRuntime: FATAL EXCEPTION: main
Process: cn.appblog.hookplugindemo, PID: 16437
java.lang.RuntimeException: Unable to start activity ComponentInfo{cn.appblog.hookplugindemo/cn.appblog.hookplugindemo.ui.MainActivity}: java.lang.NullPointerException: Attempt to invoke interface method 'void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2443)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2503)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5529)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635)
Caused by: java.lang.NullPointerException: Attempt to invoke interface method 'void android.support.v7.widget.DecorContentParent.setWindowCallback(android.view.Window$Callback)' on a null object reference
at android.support.v7.app.AppCompatDelegateImplV9.createSubDecor(AppCompatDelegateImplV9.java:410)
at android.support.v7.app.AppCompatDelegateImplV9.ensureSubDecor(AppCompatDelegateImplV9.java:323)
at android.support.v7.app.AppCompatDelegateImplV9.setContentView(AppCompatDelegateImplV9.java:284)
at android.support.v7.app.AppCompatActivity.setContentView(AppCompatActivity.java:139)
at cn.appblog.hookplugindemo.ui.MainActivity.onCreate(MainActivity.java:22)
at android.app.Activity.performCreate(Activity.java:6278)
at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1107)
at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2396)
at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2503)
at android.app.ActivityThread.-wrap11(ActivityThread.java)
at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1353)
at android.os.Handler.dispatchMessage(Handler.java:102)
at android.os.Looper.loop(Looper.java:148)
at android.app.ActivityThread.main(ActivityThread.java:5529)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:745)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:635)

请教了大佬,得到了靠谱答案,AppCompatActivity在启动的时候会进行上下文检查,于是报出了上面的问题。使用Activity即可,不用使用AppCompatActivity

实际上后续也查了两者的区别,AppCompatActivity是为了兼容低版本设备而设计的,它和Activity的区别是,AppCompatActivity拥有默认的ActionBar,也拥有自己的Theme类。而Activity默认不带ActionBarTheme的使用也和前者不同

所以到目前为止也很疑惑,不过倒并不影响我们插件化开发,用android.app.ActivityAppCompatActivity开发的Activity也并没有出现什么兼容问题

其实在 Android插件化启动Activity 中,也出现过一次类似的问题,使用android.app.Activity没问题,但是换成AppCompatActivity,则会报上面一样的错误,相当诡异,但是也同样不影响开发.

结语

插件化开发这个话题,看起来高深莫测,实际上玩起来也并不简单。实现的方式也不止一种。目前了解看来有两种解决方案,用宿主的真实Activity去代理插件Activity,另一种就是用hook去绕过manifest检查。

两种方案各有优劣,hook可能会失效,因为谷歌最近发布了禁用反射的API名单,而且`Android Studio也在使用反射的时候提示,反射可能失效。但是,还是那句话,天塌下来砸不到我们的头上,自然有大佬顶着,到时候,如果谷歌真的禁用反射,国内的巨佬们自然有新的解决办法,到时候跟随大流就好。

代理Activity的方式,则多了一个PluginLib层,需要维护,好处就是,不用看谷歌脸色。

本文转载至:https://www.jianshu.com/p/a8184c8fe688

Powered by AppBlog.CN     浙ICP备14037229号

Copyright © 2012 - 2020 APP开发技术博客 All Rights Reserved.

访客数 : | 访问量 :