前言

9.0 源码相对于之前的版本都有很大的调整,在插件化技术上需要做单独的Hook。不过在插件化方案上还是没有太大的变动。

1.方案

插件化方案

本文采用的还是插件化技术中较为主流的“欺骗AMS”的策略。即为在向AMS发送请求时,将组件信息替换为宿主注册过的组件, 在通过AMS之后,将宿主组件的信息替换为目标插件组件信息。

2. Dex 和 Resource 的加载

想要完成对插件的完全调用,要解决两个问题。第一就是能够正常加载插件中的类,第二个就是能正常加载插件中的资源。

2.1 Dex

DexClassloader 结构

在宿主app中,都会调用到BaseDexClassLoader中的DexPathList来加载相应的类,DexPathList中的dexElements 是一个dex信息数组,在查找过程中会遍历该数组,直到找到目标类为止,所以我们可以将插件dex强行加入该数组,这样 在创建组件过程中,就能正常的找到插件组件类。

具体代码如下

 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
    /**
     * 添加插件dex到host
     *
     * @param hostClassLoader 宿主classloader
     * @param apkFile         插件apkfile
     */
    public static void addDexToHost(ClassLoader hostClassLoader, File apkFile, File optDexFile) {
        try {
            //取出pathList
            Object pathList = ReflectUtils.getFieldObjectWithClassName(DexClassLoader.class.getSuperclass().getName(),hostClassLoader, "pathList");
            //取出dexElements
            Object[] dexElments = (Object[]) ReflectUtils.getFieldObject(pathList, "dexElements");
            //构造新数组
            Class<?> componentType = dexElments.getClass().getComponentType();
            Object[] newArray = (Object[]) Array.newInstance(componentType, dexElments.length + 1);
            //构造新成员
            Class[] paramsType = new Class[]{DexFile.class, File.class};
            Object[] params = new Object[]{DexFile.loadDex(apkFile.getCanonicalPath(), optDexFile.getAbsolutePath(), 0), apkFile};
            Object objectWithParams = ReflectUtils.createObjectWithParams(componentType.getName(), paramsType, params);
            Object[] tonewArray = new Object[]{objectWithParams};
            //填充新数组
            System.arraycopy(dexElments, 0, newArray, 0, dexElments.length);
            System.arraycopy(tonewArray, 0, newArray, dexElments.length , tonewArray.length);
            //设置dexElements
            ReflectUtils.setFieldValue(pathList, "dexElements", newArray);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

2.2 Resource

对于插件资源的控制,需要用到AssetManager。AssetManager能将一个apk中的资源解析,提供给应用使用,所以我们需要给插件新建一个 AssetManager,并且插件的组件都要使用这个AssetManager。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
        PluginPackage pluginPackage ;
        //新建一个AssetManager
        AssetManager assetManager = AssetManager.class.newInstance();
        //新建一个Resources
        Resources resources = new Resources(assetManager,context.getResources().getDisplayMetrics(),context.getResources().getConfiguration());
        //解析插件资源
        Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
        addAssetPath.invoke(assetManager, file.getAbsolutePath());
        //封装
        pluginPackage = new PluginPackage();
        pluginPackage.setManager(assetManager);
        pluginPackage.setResources(resources);
        pluginPackage.setTheme(resources.newTheme());
        pluginPackage.getTheme().setTo(context.getTheme());
        String replace = file.getName().replace(Constants.PLUGIN_SUFFIX, "");
        pluginPackage.setName(replace);
        pluginPackageHashMap.put(replace,pluginPackage);

在这里需要注意的是,添加插件资源的操作是在宿主app中,而使用插件资源的操作是在插件app中,所以,宿主app和插件app都需要依赖一个 公共工程,这个工程就是专门管理插件资源的。

3. 实施

3.1 架构规划

项目结构

针对以上的分析,我们在项目实践时,首先需要规划好项目结构,如上图,我们需要建立一个公共库common,这个库中包含了插件的资源管理器 和需要执行Hook操作的工具类,不管是宿主工程还是插件工程都需要依赖此库。

Hooker中除了需要添加插件Dex的操作之外,还需要完成“欺骗策略”的Hook操作。熟悉四大组件的启动过程之后,我们就知道,想要完整的欺骗 AMS,需要完成两步Hook操作。

AMS 结构

第一步在发送启动请求时,需要将请求的插件组件的类名替换成宿主app中申请的组件类名,这一步,进过多步实验,直接Hook AMS是合理的。 代码中的AMSHooker是我们执行具体替换插件类名操作的类,这里属于动态代理的一种方案。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
 /**
     * 欺骗策略第一环节,拦截Intent,替换之后发送给AMS
     */
    public static void hookAMS() {
        try {
            Object iActivityManagerSingleton = ReflectUtils.getStaticFieldObject("android.app.ActivityManager", "IActivityManagerSingleton");
            Object mInstance = ReflectUtils.getFieldObjectWithClassName("android.util.Singleton",iActivityManagerSingleton, "mInstance");
            Class<?> aClass = Class.forName("android.app.IActivityManager");
            Object proxyInstance = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{aClass}, new AMSHooker(mInstance));
            ReflectUtils.setFieldValueWithClassName("android.util.Singleton",iActivityManagerSingleton,"mInstance",proxyInstance);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }

ActivityThread 结构

第二步,就是在通过了AMS验证之后,需要将目标插件组件的类名替换回来,好让ActivityThread完成反射操作。HandlerHooker也是我们处理 替换操作的代理类。

1
2
3
4
5
6
7
8
    /**
     * 欺骗策略第二环节,将插件Intent还原
     */
    public static void hookActivityThread(){
        Object sCurrentActivityThread = ReflectUtils.getStaticFieldObject("android.app.ActivityThread", "sCurrentActivityThread");
        Handler mH = (Handler) ReflectUtils.getFieldObject(sCurrentActivityThread, "mH");
        ReflectUtils.setFieldValueWithClassName(Handler.class.getName(),mH,"mCallback",new HandlerHooker(mH));
    }

3.2 Activity的启动

在两个代理类中,关于Activity的处理如下

 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
//AMSHooker.java

public class AMSHooker implements InvocationHandler {
    ...
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("startActivity".equals(method.getName())){
            int index = -1;
            Intent raw = null;
            //找出Intent的参数
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof  Intent){
                    index  = i;
                    raw = (Intent) args[i];
                    break;
                }
            }
            //将Intent中的Activity组件类名换成 宿主中的占位组件 StubActivity
            Intent newIntent =  new Intent();
            newIntent.setComponent(new ComponentName("com.zxy.plugin","com.zxy.plugin.StubActivity"));
            newIntent.putExtra(TARGET_AC,raw);
            if (index>-1){
                args[index] = newIntent;
            }
        }
        return method.invoke(mBase,args);
    }
}

在返回后,处理如下。

 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
public class HandlerHooker implements Handler.Callback {

    private Handler mHandler;

    public HandlerHooker(Handler o) {
        mHandler = o;
    }

    @Override
    public boolean handleMessage(Message msg) {
        //在9.0的源码中,H 中159表示Activity的操作
        if (msg.what == 159) {
            Object obj = msg.obj;
            //每个操作都是封装成ClientTransaction 
            if ("android.app.servertransaction.ClientTransaction".equals(obj.getClass().getName())) {
                //一个ClientTransaction中包含一个关于操作activiy的集合
                List<Object> mActivityCallbacks = (List<Object>) ReflectUtils.getFieldObject(obj, "mActivityCallbacks");
                for (int i = 0; i < mActivityCallbacks.size(); i++) {
                    Object o = mActivityCallbacks.get(i);
                    //如果包含了Launch操作,就可以在这里执行替换操作
                    if ("android.app.servertransaction.LaunchActivityItem".equals(o.getClass().getName())) {
                        Intent mIntent = (Intent) ReflectUtils.getFieldObject(o, "mIntent");
                        Intent parcelableExtra = mIntent.getParcelableExtra(AMSHooker.TARGET_AC);
                        mIntent.setComponent(parcelableExtra.getComponent());
                    }
                }
            }
        }
        mHandler.handleMessage(msg);
        return true;
    }
}

关于插件Activity的资源引用,在上文我们也说到了,需要使用插件自己的AssetManager,所以这里我们会抽象一个插件Activity基类

 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 abstract class PluginBaseActivity extends Activity {
    //每个插件的资源封装
    private PluginResourceManager.PluginPackage aPackage;
    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        //在插件Actiivity启动时就使用资源管理器将相应的插件资源获取
        aPackage = PluginResourceManager.getInstance().getPackage(getPluginKey().replace(".","_"));
    }
    //插件标示,每个插件都有不一样的标示
    protected abstract String getPluginKey();
    //将以下三个方法返回值都替换为插件中的包装,这样插件Activity就能正常使用插件资源了
    @Override
    public AssetManager getAssets() {
        return aPackage.getManager();
    }
    @Override
    public Resources.Theme getTheme() {
        return aPackage.getTheme();
    }
    @Override
    public Resources getResources() {
        return aPackage.getResources();
    }
}

代码中的PluginResourceManager是一个单例,在宿主app启东时会完成初始化,将所有的插件app的Resource都放入宿主app的 内存中,以供后来的插件app调用。

3.3 Service的启动

启动之前的操作如下

 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 AMSHooker implements InvocationHandler {
    ...
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        if ("startService".equals(method.getName())){
            int index = -1;
            Intent raw = null;
            for (int i = 0; i < args.length; i++) {
                if (args[i] instanceof  Intent){
                    index  = i;
                    raw = (Intent) args[i];
                    break;
                }
            }
            Intent newIntent =  new Intent();
            String className = raw.getComponent().getClassName();
            //Service由于其特殊性,插件app中的Service与宿主的占位Service需要一一对应。所以这里使用了字典的方式来管理
            String stubService = PluginServiceManager.serviceManagerSingleton.getmInstance().getStubService(className);
            if (!TextUtils.isEmpty(stubService)){
                newIntent.setComponent(new ComponentName("com.zxy.plugin",stubService));
                newIntent.putExtra(TARGET_SE,raw);
                if (index>-1){
                    args[index] = newIntent;
                }
            }
        }
        return method.invoke(mBase,args);
    }
}

返回之后的处理如下。

 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
public class HandlerHooker implements Handler.Callback {
    ...
    @Override
    public boolean handleMessage(Message msg) {
        //114属于创建指令
        if (msg.what == 114) {
            Object obj = msg.obj;
            if ("android.app.ActivityThread$CreateServiceData".equals(obj.getClass().getName())) {
                ServiceInfo info = (ServiceInfo) ReflectUtils.getFieldObjectWithClassName("android.app.ActivityThread$CreateServiceData", obj, "info");
                //替换
                info.name = getTargetService(info.name);
            }
        }
        //115属于启动指令
        if (msg.what == 115) {
            Object obj = msg.obj;
            if ("android.app.ActivityThread$ServiceArgsData".equals(obj.getClass().getName())) {
                Intent intent = (Intent) ReflectUtils.getFieldObjectWithClassName("android.app.ActivityThread$ServiceArgsData", obj, "args");
                Intent target = intent.getParcelableExtra(AMSHooker.TARGET_SE);
                //替换
                intent.setComponent(target.getComponent());
            }
        }
        mHandler.handleMessage(msg);
        return true;
    }
}

为了统一资源的使用,service也需要加载插件的资源

 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
public abstract class BasePluginService extends Service {
    private PluginResourceManager.PluginPackage aPackage;

    @Override
    protected void attachBaseContext(Context newBase) {
        super.attachBaseContext(newBase);
        aPackage = PluginResourceManager.getInstance().getPackage(getPluginKey().replace(".","_"));
    }

    protected abstract String getPluginKey();

    @Override
    public AssetManager getAssets() {
        return aPackage.getManager();
    }

    @Override
    public Resources.Theme getTheme() {
        return aPackage.getTheme();
    }

    @Override
    public Resources getResources() {
        return aPackage.getResources();
    }

}

总结

对于一个完整的插件化框架,光有上述工作还是不够的,比如Activity的启动模式的解决,Broadcastrecevier和ContentProvider 的插件化实施,还有对于各个sdk版本的兼容。再接再厉,后续会继续完善。