源码解析: 插件化原理

1,243 阅读6分钟

字节码替换

Android 提供了一些 Gradle 插件开发套件,其中有一项功能叫 Transform Api,它可以介入项目的构建过程,在字节码生成后、dex 文件生成前,对代码进行某些变换

插件化两种方式:

ProxyActivity代理

代理方式的关键总结起来有下面两点:

1,ProxyActivity中需要重写getResouces,getAssets,getClassLoader方法返回插件的相应对象。
生命周期函数以及和用户交互相关函数,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要转发给插件。 

2,PluginActivity中所有调用context的相关的方法,如setContentView,getLayoutInflater,getSystemService等都需要调用ProxyActivity的相应方法。

该方式有几个明显缺点:

插件中的Activity必须继承PluginActivity,开发侵入性强。 

如果想支持Activity的singleTask,singleInstance等launchMode时,需要自己管理Activity栈,实现起来很繁琐。 

插件中需要小心处理Context,容易出错。 如果想把之前的模块改造成插件需要很多额外的工作。

hook系统启动Activity的过程

基于Activity启动流程,使用hook思想代理startActivity这个方法,使用占坑的方式,在AndroidManifest中固定写死一个Activity,我们在启动我们插件apk的时候使用它去系统层校检合法性,然后等真正创建Activity的时候再通过hook思想拦截Activity的创建方法,提前将信息更换回来创建真正的插件apk。

一,Hook IActivityManager实现Activity插件化

2.1 AndroidManifest.xml中注册占坑Activity

2.2 使用占坑Activity通过AMS校验

仔细想一下,我们的目标其实很简单,就是在AMS执行startActivity()方法之前,将要启动的插件Activity替换成占坑Activity。而调用AMS的startActivity()方法是由AMS在本地的代理对象来完成的,所以我们就把目光聚焦到了这个AMS本地代理对象。

Activity启动流程源码解析中我们提到,关于获取AMS代理对象的方式,Android 8.0和之前的版本是有一些差别的。Android 8.0采用的是AIDL的实现方式获取AMS的代理对象,而Android 8.0之前是通过ActivityManagerNative.getDefault()来获取AMS的代理对象的。不过这个对我们影响不是很大,做好兼容处理就行。

Android 8.0源码

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;

                }

            };

Android 8.0之前源码

static public IActivityManager getDefault() {

        return gDefault.get();

    }

     private static final Singleton<IActivityManager> gDefault = new Singleton<IActivityManager>() {

        protected IActivityManager create() {

            IBinder b = ServiceManager.getService("activity");

            if (false) {

                Log.v("ActivityManager", "default service binder = " + b);

            }

            IActivityManager am = asInterface(b);

            if (false) {

                Log.v("ActivityManager", "default service = " + am);

            }

            return am;

        }

    };

由上述源码可知,不管是Android 8.0,还是Android之前,最终返回的AMS本地代理对象都是IActivityManager类型的对象。因此,IActivityManager就是一个很好的Hook点,我们只需要去拦截它的startActivity()方法,并且将要启动的插件Activity替换成占坑Activity。为了简单起见,省略了加载插件Activity的过程,直接创建了一个PluginActivity来代表已经加载进来的插件Activity,并且没有在AndroidManifest.xml中进行注册。同时,由于IActivityManager又是一个接口,所以我们完全可以采用动态代理来拦截它的startActivity()方法,具体实现如下:

public class IActivityManagerProxy implements InvocationHandler {

    private static final String TAG = "IActivityManagerProxy";

    private Object mActivityManager;

    public IActivityManagerProxy(Object activityManager) {

        this.mActivityManager = activityManager;

    }

    @Override

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

        if ("startActivity".equals(method.getName())) {

            Log.e(TAG, "invoke startActivity");

            int index = 0;

            for (int i = 0; i < args.length; i++) {

                if (args[i] instanceof Intent) {

                    index = i;

                    break;

                }

            }

            // 获取启动PluginActivity的Intent

            Intent pluginIntent = (Intent) args[index];

            // 新建用来启动StubActivity的Intent

            Intent stubIntent = new Intent();

            stubIntent.setClassName("com.lxbnjupt.pluginactivitydemo",

                    "com.lxbnjupt.pluginactivitydemo.activity.StubActivity");

            // 将启动PluginActivity的Intent保存在subIntent中,便于之后还原

            stubIntent.putExtra(HookHelper.PLUGIN_INTENT, pluginIntent);

            // 通过stubIntent赋值给args,从而将启动目标变为StubActivity,以此达到通过AMS校验的目的

            args[index] = stubIntent;

        }

        return method.invoke(mActivityManager, args);

    }

}

之后,创建代理类IActivityManagerProxy,并且使用IActivityManagerProxy去替换原来的IActivityManager即可:

 /**

     * Hook IActivityManager

     *

     * @throws Exception

     */

    public static void hookAMS() throws Exception {

        Log.e(TAG, "hookAMS");

        Object singleton = null;

        if (Build.VERSION.SDK_INT >= 26) {

            Class<?> activityManageClazz = Class.forName("android.app.ActivityManager");

            // 获取ActivityManager中的IActivityManagerSingleton字段

            Field iActivityManagerSingletonField = ReflectUtils.getField(activityManageClazz, "IActivityManagerSingleton");

            singleton = iActivityManagerSingletonField.get(activityManageClazz);

        } else {

            Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");

            // 获取ActivityManagerNative中的gDefault字段

            Field gDefaultField = ReflectUtils.getField(activityManagerNativeClazz, "gDefault");

            singleton = gDefaultField.get(activityManagerNativeClazz);

        }

        Class<?> singletonClazz = Class.forName("android.util.Singleton");

        // 获取Singleton中mInstance字段

        Field mInstanceField = ReflectUtils.getField(singletonClazz, "mInstance");

        // 获取IActivityManager

        Object iActivityManager = mInstanceField.get(singleton);

        Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");

        // 获取IActivityManager代理对象

        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),

                new Class<?>[]{iActivityManagerClazz}, new IActivityManagerProxy(iActivityManager));

        // 将IActivityManager代理对象赋值给Singleton中mInstance字段

        mInstanceField.set(singleton, proxy);

    }

2.3 还原插件Activity

在AMS执行startActivity()方法之前,我们使用占坑Activity替换插件Activity,从而通过了AMS的校验。但是,我们真正要启动的是插件Activity,那么势必还是要替换回来的。那么,回想一下Activity的启动流程,我们的目标就换成了要在ActivityThread执行handleLaunchActivity()方法之前,将占坑Activity替换回插件Activity。ActivityThread会通过Handler内部类H将代码的逻辑切换到主线程中,H中重写的handleMessage方法会对LAUNCH_ACTIVITY类型的消息进行处理,我们可以将H的mCallback作为Hook点。

public class HCallback implements Handler.Callback {

    private static final int LAUNCH_ACTIVITY = 100;

    Handler mHandler;

    public HCallback(Handler handler) {

        mHandler = handler;

    }

    @Override

    public boolean handleMessage(Message msg) {

        if (msg.what == LAUNCH_ACTIVITY) {

            Object obj = msg.obj;

            try {

                // 获取启动SubActivity的Intent

                Intent stubIntent = (Intent) ReflectUtils.getField(obj.getClass(), "intent", obj);

                // 获取启动PluginActivity的Intent(之前保存在启动SubActivity的Intent之中)

                Intent pluginIntent = stubIntent.getParcelableExtra(HookHelper.PLUGIN_INTENT);

                // 将启动SubActivity的Intent替换为启动PluginActivity的Intent

                stubIntent.setComponent(pluginIntent.getComponent());

            } catch (Exception e) {

                e.printStackTrace();

            }

        }

        mHandler.handleMessage(msg);

        return true;

    }

}

由上述代码可知,HCallback实现了Handler.Callback,并重写了handleMessage方法,当收到消息的类型为LAUNCH_ACTIVITY时,将启动占坑Activity的Intent替换为启动插件Activity的Intent。 之后,我们创建HCallback的实例,并且用它来替换H的mCallback:

 /**

     * Hook ActivityThread中Handler成员变量mH

     *

     * @throws Exception

     */

    public static void hookHandler() throws Exception {

        Log.e(TAG, "hookHandler");

        Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");

        // 获取ActivityThread中成员变量sCurrentActivityThread字段

        Field sCurrentActivityThreadField = ReflectUtils.getField(activityThreadClazz, "sCurrentActivityThread");

        // 获取ActivityThread主线程对象

        Object currentActivityThread = sCurrentActivityThreadField.get(activityThreadClazz);

        // 获取ActivityThread中成员变量mH字段

        Field mHField = ReflectUtils.getField(activityThreadClazz, "mH");

        // 获取ActivityThread主线程中Handler对象

        Handler mH = (Handler) mHField.get(currentActivityThread);

        // 将我们自己的HCallback对象赋值给mH的mCallback

        ReflectUtils.setField(Handler.class, "mCallback", mH, new HCallback(mH));

    }

二,Hook Instrumentation实现Activity插件化

3.1 替换和还原插件Activity

Hook Instrumentation实现Activity插件化的思想同样也是使用占坑Activity,与Hook IActivityManager不同的地方是替换和还原的地方不同而已,而且相对来说会稍微简洁一些。

由Activity启动流程可知,启动一个Activity的过程中会调用到Instrumentation的execStartActivity()方法,在此方法中我们可以用占坑Activity来替换插件Activity,以此来通过AMS的验证。然后,在回到ActivityThread主线程的performLaunchActivity方法中时,会调用Instrumentation的newActivity方法创建Activity,在此方法中我们可以用插件Activity来替换占坑Activity。

public class InstrumentationProxy extends Instrumentation {

    private Instrumentation mInstrumentation;

    private PackageManager mPackageManager;

    public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {

        this.mInstrumentation = instrumentation;

        this.mPackageManager = packageManager;

    }

    public ActivityResult execStartActivity(

            Context who, IBinder contextThread, IBinder token, Activity target,

            Intent intent, int requestCode, Bundle options) {

        // 查找要启动的Activity是否已经在AndroidManifest.xml中注册

        List<ResolveInfo> infos = mPackageManager.queryIntentActivities(intent, PackageManager.MATCH_ALL);

        if (infos == null || infos.size() == 0) {

            // 要启动的Activity没有注册,则将启动它的Intent保存在Intent中,便于之后还原

            intent.putExtra(HookHelper.PLUGIN_INTENT, intent.getComponent().getClassName());

            // 替换要启动的Activity为StubActivity

            intent.setClassName(who, "com.lxbnjupt.pluginactivitydemo.activity.StubActivity");

        }

        try {

            Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",

                    Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);

            // 通过反射调用execStartActivity方法,将启动目标变为StubActivity,以此达到通过AMS校验的目的

            return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,

                    target, intent, requestCode, options);

        } catch (Exception e) {

            e.printStackTrace();

        }

        return null;

    }

    public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException,

            IllegalAccessException, ClassNotFoundException {

        String intentName = intent.getStringExtra(HookHelper.PLUGIN_INTENT);

        if (!TextUtils.isEmpty(intentName)) {

            // 还原启动目标Activity

            return super.newActivity(cl, intentName, intent);

        }

        return super.newActivity(cl, className, intent);

    }

}

接着,我们需要创建InstrumentationProxy对象,并且让其替换主线程中的Instrumentation对象即可:

/**

    /**

     * Hook Instrumentation

     *

     * @param context 上下文环境

     * @throws Exception

     */

    public static void hookInstrumentation(Context context) throws Exception {

        Log.e(TAG, "hookInstrumentation");

        Class<?> activityThreadClazz = Class.forName("android.app.ActivityThread");

        // 获取ActivityThread中成员变量sCurrentActivityThread字段

        Field sCurrentActivityThreadField = ReflectUtils.getField(activityThreadClazz, "sCurrentActivityThread");

        // 获取ActivityThread中成员变量mInstrumentation字段

        Field mInstrumentationField = ReflectUtils.getField(activityThreadClazz, "mInstrumentation");

        // 获取ActivityThread主线程对象(应用程序启动后就会在attach方法中赋值)

        Object currentActivityThread = sCurrentActivityThreadField.get(activityThreadClazz);

        // 获取Instrumentation对象

        Instrumentation instrumentation = (Instrumentation) mInstrumentationField.get(currentActivityThread);

        PackageManager packageManager = context.getPackageManager();

        // 创建Instrumentation代理对象

        InstrumentationProxy instrumentationProxy = new InstrumentationProxy(instrumentation, packageManager);

        // 用InstrumentationProxy代理对象替换原来的Instrumentation对象

        ReflectUtils.setField(activityThreadClazz, "mInstrumentation", currentActivityThread, instrumentationProxy);

    }