如何绕过清单文件,动态注入activity

本文探讨了如何在Android中绕过清单文件动态注入Activity,以实现插件开发。通过分析Activity的实例化过程,了解到Instrumentation在其中的作用。接着介绍了如何偷梁换柱,包括绕过未注册Activity的校验和利用Intent参数来启动插件Activity。最后,详细阐述了如何替换系统方法,用自己的newActivity方法来控制Activity的创建。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

【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就可以了。代码如下:
其中:

ActivityThread_method_currentActivityThread 就是ActivityThread的currentActivityThread方法; activityThread就是当前的ActivityThread。 InstrumentationHook 是我们自定义的Instrumentaion,修改了系统的newActivity。
?
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了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值