插件化实现原理简析(基础概念)

动态加载技术

在应用程序运行时,动态的去加载一些程序中原本不存的可执行文件并运行这些文件里的代码逻辑。

可执行文件总的来说分为两种:

  • 一种是动态链接库so
  • 一种是dex相关文件(dex以及包含dex的jar/apk文件)

随着应用开发技术和业务的逐步发展,动态加载技术派生出两个技术:热修复技术以及插件化技术。

热修复技术主要用来修复Bug,插件化技术主要来解决应用越来越庞大以及功能模块的解耦

插件化

插件化产生

在开发初期时,业务需求以及应用开发的复杂度都不是很高,在后续的开发过程中,容易出现以下情况:

  1. 业务复杂,模块耦合

    随着开发过程,应用的体积以及复杂度都会越来越大,模块的耦合也会越来越严重。

  2. 应用间的接入

    一个应用不再是单独的应用,他可能还需要接入其他的应用来完善功能。

  3. 65535限制

    代码量的增大,方法数也会增加,就很容易超出限制。

插件化定义

让我们不用像原来一样把所有的内容都放在一个apk中,把一些功能和逻辑单独的放到插件Apk中,由宿主Apk按需调用。方便减少Apk的体积,也可以简单实现热插拔,更加动态化。

插件化的客户端由宿主和插件两个部分组成,宿主多指安装好的Apk,插件就为经过处理的Apk、so的dex等文件。插件可以被宿主加载也可以单独运行。

插件化基本原理

类加载

Android中常用的有两种类加载器,DexClassLoaderPathClassLoader,它们都继承于BaseDexClassLoader。这两个加载器的区别是DexClassLoader多了一个optimizedDirectory参数,这个是用来缓存系统创建的Dex文件。在PathClassLoader中这个参数为null,所以只能去加载内部存储(/data/data/XX)中的Dex文件。

通过双亲委托机制可以保证类不会重复加载,通过先查看该类是否已被加载,未加载时首先让父加载器先去尝试加载,无法加载再交由自身处理。


单DexClassLoader与多DexClassLoader

通过给插件apk生成相应的DexClassLoader便可以去访问其中的类。这边又分成两种形式:

  • 单DexClassLoader

    单ClassLoader结构

    将插件Apk中的DexClassLoader的DexPathList都合并进宿主Apk中。可以在不同的插件及主工程间直接调用相关类和方法,也可以直接抽出共用模块供其他插件使用。

  • 多DexClassLoader

    多ClassLoader结构

    每个插件都会去生成一个DexClassLoader,当加载该插件中的类需要通过各自的DexClassLoader去加载,这样不同插件的类就是相互隔离的。

宿主和插件相互调用时需要注意以下几点:

  • 插件调用主工程:

    构造插件的ClassLoader时直接传入主工程的ClassLoader作为父加载器,所以插件可以直接去引用主工程的类。

  • 主工程调用插件:

    • 单ClassLoader结构

      主工程可以通过类名直接去访问插件中的类。

      需要注意插件中引用了不同版本的相同库时,需要尽量避免。

    • 多ClassLoader结构

      主工程引用插件中类需要先通过插件的ClassLoader加载该类再通过反射调用其方法。

资源加载

Android系统通过Resource加载资源,Resource又要依赖AssetManager去加载资源。

因此,只要将插件Apk的路径加入到AssetManager中,便能够实现对插件资源的访问。

资源的插件化方式主要有两种:

方式 优点 缺点
合并资源方案 插件和主工程可以直接相互访问资源 导致资源冲突
独立构建资源方案 资源隔离,不会造成冲突 资源共享比较麻烦

插件化实现实例

Activity插件化

Activity插件化主要有3种实现方式,分别是反射实现、接口实现以及Hook技术实现

反射实现会对应用的性能造成影响。

接口实现可以阅读dynamic-load-apk源码,框架提供基础四大组件基类,由需要插件化的组件进行继承。

Hook技术实现主流插件化的实现方案。

我们从Activity启动过程了解到了Activity的启动过程。如果我们需要对Activity进行插件化,需要对这段过程有很好的了解。

通过Hook方式去实现Activity插件化,主要需要解决两个问题:

  • 插件中的Activity并没有在AndroidManifest.xml进行注册,如何绕过AMS校验
  • 如何去构造Activity的实例,并同步生命周期
Hook IActivityManager
1.注册占坑Activity

采用预先占坑的方式,即在AndroidManifest.xml中先注册一个占坑Activity来代表即将加入进来的插件Activity。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" package="com.example.wxy.ipc">

<application android:allowBackup="true"
android:label="@string/app_name"
android:name="com.example.wxy.ipc.App"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme" tools:ignore="GoogleAppIndexingWarning">

<activity android:name=".LoadActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!--设置占位Activity-->
<activity android:name=".StubActivity"/>
</application>
</manifest>
2.使用占坑Activity绕过AMS验证

分析Activity启动流程时,Instrumentation.execStartActivity()去启动Activity,内部实质是依靠远程调用AMS.startActivity()去执行启动流程。

在Android 8.0之前,依靠的是ActivityManagerNative.getDefault()执行远程调用

./android/app/Instrumentation.java before Android8.0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options)
{
...
int result = ActivityManagerNative.getDefault()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
}

// ../android/app/ActivityManagerNative.java
static public IActivityManager getDefault(){
return gDefault.get();
}

private static final Sigleton<IActivityManager> gDefault = new Singleton<IActivityManager>(){
protected IActivityManager create() {
IBinder b = ServiceManager.getService("activity");
IActivityManager am = asInterface(b);
return am;
}
}

第一次调用到getDefault()时,就会调用到IActivityManagerSingleton.get(),由源码可知,该类是一个单例类。


在Android8.0时,依靠的是ActivityManager.getService()执行远程调用

./android/app/Instrumentation.java in Android8.0
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
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options)
{
...
int result = ActivityManager.getService()
.startActivity(whoThread, who.getBasePackageName(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()),
token, target != null ? target.mEmbeddedID : null,
requestCode, 0, null, options);
}

// ../android/app/ActivityManager.java
public static IActivityManager getService() {
return IActivityManagerSingleton.get();
}

private static final Singleton<IActivityManager> IActivityManagerSingleton =
new Singleton<IActivityManager>() {
@Override
protected IActivityManager create() {
final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
final IActivityManager am = IActivityManager.Stub.asInterface(b);
return am;
}
};

在其中先去获取名为activity的一个代理对象(IBinder),后续实现利用了AIDL,根据asInterface()可以获得IActivityManager对象,他是AMS在本地的代理对象。然后就可以直接调用到AMSstartActivity()

根据上述两段源码分析,最终都需要通过IActivityManager去远程调用到AMS,可以将其作为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
30
31
32
33
34
public class IActivityManagerProxy implements InvocationHandler {

private Object mActivityManager;
private static final String TAG = "IActivityManagerProxy";

public IActivityManagerProxy(Object _object) {
mActivityManager = _object;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if ("startActivity".equals(method.getName())) {
Intent intent = null;
int index = 0;
for (int i = 0; i < args.length; i++) {
if (args[i] instanceof Intent) {
index = i;
break;
}
}
intent = (Intent) args[index];
Intent subIntent = new Intent();
String packageName = "com.example.wxy.ipc";
//这个地方配置的是设置好的占坑Activity
subIntent.setClassName(packageName, packageName + ".hook.StubActivity");
//存储原先启动目标Activity的Intent,方便后续进行还原
subIntent.putExtra("target_intent", intent);
Log.d(TAG,"hook 成功");
//把对目标Activity的请求指向到占坑Activity
args[index] = subIntent;
}
return method.invoke(mActivityManager, args);
}
}

通过定义上述的代理对象后,跳转到其他Activity时都会被定位到StubActivity上,无论是否在AndroidManifest.xml进行过注册

接下来要把设置好的代理对象Hook到原有的结构上。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class HookHelper {
public static void hookAMS() throws Exception {
Object defaultSingleton = null;
if (Build.VERSION.SDK_INT >= 26) {
Class<?> activityManagerClazz = Class.forName("android.app.ActivityManager");
//获取ActivityManager中的IActivityManagerSingleton字段
defaultSingleton = FieldUtil.getField(activityManagerClazz, null, "IActivityManagerSingleton");
} else {
@SuppressLint("PrivateApi") Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
//获取ActivityManager中的gDefault字段
defaultSingleton = FieldUtil.getField(activityManagerNativeClazz, null, "gDefault");
}
Class<?> singletonClazz = Class.forName("android.util.Singleton");
Field mInstanceField = FieldUtil.getField(singletonClazz, "mInstance");
//获取mInstance字段 即单例类
Object iActivityManager = mInstanceField.get(defaultSingleton);
Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
//使用新建的IActivityManagerProxy替换掉原有的IActivityManager
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivityManagerClazz},
new IActivityManagerProxy((iActivityManager)));
mInstanceField.set(defaultSingleton, proxy);
}
}

Application引用HookHelper类即可完成绕过验证操作

1
2
3
4
5
6
7
8
9
10
11
public class MyApplication extends Application{
@Override
public void attachBaseContext(Context base){
super.attachBaseContext(base);
try{
HookHelper.hookAMS();
}catch(Exception e){
e.printStackTrace();
}
}
}

在执行startActivity()跳转时,都会跳转到StubActivity界面。至此完成了通过AMS验证步骤

3.还原插件Activity

使用占坑Activity通过AMS校验后,因为当前的情况就是把跳转的都指向到了StubActivity中,需要做的是还原原本要跳转的Activity,使用原本Activity对StubActivity进行替换。

要实现替换功能,关键点在于找到真正开始绘制Activity的地方,然后实际绘制需要跳转的Activity。

Activity启动过程这节中,了解到绘制Activity的流程是从ActivityThread.handleLaunchActivity()开始执行,并调用到onCreate()。那就可以在执行这个方法之前,替换掉即将启动的Activity,在上一节中启动的就是StubActivity,需要把这个再替换成原本的目标Activity。

控制Activity的一套流程都是通过H这个Handler类去执行的,在其中定义了很多code,来分发不同的流程。可以通过Hook这套流程拦截原本的启动Activity流程,替换成自定义的启动流程。

使用Handler时如果想拦截原有的handleMessage(),就需要为Handler设置一个Callback,这样在分发消息(dispatchMessage())的时候,就会去执行到Callback.handlerMessage(msg)而不执行原有处理。在此基础上,可以对ActivityThread.H设置一个Callback拦截启动Activity的事件。

在此先自定义一个Callback

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
public class HCallback implements Handler.Callback {
Handler mHandler;

public HCallback(Handler _handler) {
mHandler = _handler;
}

@Override
public boolean handleMessage(Message msg) {
Object r = msg.obj;
switch (msg.what) {
case 100: //LAUNCH_ACTIVITY
try {
//得到消息中的Intent -- 启动StubActivity的Intent
Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
//从中取出原本要启动Activity的Intent
Intent target = intent.getParcelableExtra("target_intent");
//将启动目标Activity的Intent替换掉启动StubActivity的Intent
intent.setComponent(target.getComponent());
} catch (Exception e) {
e.printStackTrace();
}
break;
case 159: //Android P 对应的启动条件
//在Android P中取消了Activity的相关Code,把他们封装成ClientTransacion类型对象,然后存储在其中的 mActivityCallbacks
//LaunchActivityItem 启动Activity
//DestoryActivityListItem 关闭Activity
try {
List<Object> mCallbacks = (List<Object>) FieldUtil.getField(r.getClass(), r, "mActivityCallbacks");
if (!mCallbacks.isEmpty()) {
//找到启动Activity的消息
String className = "android.app.servertransaction.LaunchActivityItem";
if (mCallbacks.get(0).getClass().getCanonicalName().equals(className)) {
Object object = mCallbacks.get(0);
Intent intent = (Intent) FieldUtil.getField(object.getClass(), object, "mIntent");
Intent target = intent.getParcelableExtra("target_intent");
//替换进去
intent.setComponent(target.getComponent());
}
}
} catch (Exception e) {
e.printStackTrace();
}
break;
}
mHandler.handleMessage(msg);
return true;
}
}

实现了自定义Callback对象HCallback后,就需要把它设置到ActivityThread.H中使其拦截后续启动动作。

1
2
3
4
5
6
7
8
9
10
11
12
public class HookHelper {
public static void hookHandler() throws Exception {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
//当前对应的ActivityThread对象
Object currentActivityThread = FieldUtil.getField(activityThreadClass, null, "sCurrentActivityThread");
//对应Handler对象
Field mHField = FieldUtil.getField(activityThreadClass, "mH");
Handler mH = (Handler) mHField.get(currentActivityThread);
//替换掉mh中的mCallback对象
FieldUtil.setField(Handler.class, mH, "mCallback", new HCallback(mH));
}
}

上述执行完毕后,启动的就会是目标Activity。

4.插件Activity的生命周期

上述三步执行完毕后,就可以打开插件Activity,但是这种操作下会不会影响到原有的生命周期,实际上还是依赖了StubActivity

Activity生命周期的回调代码都是交由Instrumentation.callActivityOnXX(ActivityClientRecord.activity)执行对应的回调代码。其中ActivityClientRecoed用于描述应用进程中的Activity。我们只要分析ActivityClientRecord.activity对应的是否为目标Activity,是的话那么生命周期就没有问题。

./android/app/ActivityThread.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
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
...
Activity activity = null;
try {
java.lang.ClassLoader cl = appContext.getClassLoader();
//加载Activity,其实这时加载的已经是目标Activity了
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
...
} catch (Exception e) {
...
}
...
try {
//创建Application对象
Application app = r.packageInfo.makeApplication(false, mInstrumentation);

if (activity != null) {
...
//Activity的初始化操作
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback);
...
//调用 onCreate() 回调方法
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
mInstrumentation.callActivityOnCreate(activity, r.state);
}
//设置ActivityClientRecord中的Activity为目标Activity
r.activity = activity;
...
mActivities.put(r.token,r);
}
}catch(Exception e){
...
}
}

从以上源码分析可知,performLaunchActivity()时会设置当前Activity为目标Activity,生命周期也会跟着当前Activity去执行,即生命周期是同步的。

Hook Instrumentation

该实现相对上面会简单很多,主要就是去操作Instrumentation,Hook掉其中的两个方法:

  • newActivity():新建Activity 用目标Activity替换掉StubActivity
  • execStartActivity():启动Activity 拦截跳转到StubActivity上
1.注册占坑Activity

方法同上

2.设置Instrumentation代理对象
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
public class InstrumentationProxy extends Instrumentation {
private static final String TAG = "InstrumentationProxy";
private Instrumentation mInstrumentation;
private PackageManager mPackageManager;

public InstrumentationProxy(Instrumentation _instrumentation, PackageManager _packageManager) {
mInstrumentation = _instrumentation;
mPackageManager = _packageManager;
}

public Activity newActivity(ClassLoader cl, String className,
Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
String intentName = intent.getStringExtra("target_intent");
if (!TextUtils.isEmpty(intentName)) {
return super.newActivity(cl, intentName, intent);
}
return super.newActivity(cl, className, intent);
}

public ActivityResult execStartActivity(Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);
//判断需要启动的Activity是否已被注册
if (infos.isEmpty()) {
intent.putExtra("target_intent", intent.getComponent().getClassName());
//未注册则指向StubActivity
intent.setClassName(who, "com.example.wxy.ipc.hook.StubActivity");
}
try {
//反射调用 execStartActivity
@SuppressLint("PrivateApi") Method execStartActivity = Instrumentation.class.getDeclaredMethod(
"execStartActivity",
Context.class, IBinder.class, IBinder.class, Activity.class,
Intent.class, int.class, Bundle.class);
execStartActivity.setAccessible(true);
return (ActivityResult) execStartActivity.invoke(mInstrumentation, who,
contextThread, token, target, intent, requestCode, options);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

设置好代理对象后,需要把代理对象Hook到ActivityThread上,方便后续调用

1
2
3
4
5
6
7
8
9
10
11
12
13
public class HookHelper {
public static void hookInstrumentation(Context context) throws Exception {
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
//获取ActivityThread中的 sCurrentActivityThread 代指当前进程的ActivityThread
Object activityThread = FieldUtil.getField(activityThreadClass,null,"sCurrentActivityThread");
Field mInsrumentationField = FieldUtil.getField(activityThreadClass, "mInstrumentation");
//获取到 mInstrumentation
Object mInstrumentation = mInsrumentationField.get(activityThread);
//使用InstrumentationProxy替换掉原先的mInstrumentation
FieldUtil.setField(activityThreadClass, activityThread, "mInstrumentation",
new InstrumentationProxy((Instrumentation) mInstrumentation, context.getPackageManager()));
}
}

Application中的attachBaseContext()调用HookHelper.hookInstrumentation()即可完成插件Activity的加载。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!