Activity插件化原理

概述

Activity的插件化一直以来都是插件化技术的重点。Activity的插件化有很多技术方案,但是主流实现还是使用Hook技术。所有本文主要讲解通过Hook技术的实现Activity插件化的方案。在了解Activity插件化之前首先要清楚Activity的启动流程,不清楚的可以看我之前写的文章。

Activity插件化方案

Hook技术主要有以下两种实现方案,本文也是围绕着这两种方案进行介绍:

  • Hook IActivityManager
  • Hook Instrumentation

占坑Activity

众所周知,Activity是需要在AndroidManifest.xml文件中进行注册的,而插件中的Activity是不能在宿主的AndroidManifest.xml文件中进行注册的,所以不管采用哪一种Hook方案都需要提前在宿主中创建一个Activity并且在AndroidManifest.xml中进行注册。通过启动这个占坑Activity我们就能完成对插件Activity的替换。如下所示:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.pluginactivity">
    <application
       ...
        <activity android:name=".StubActivity"/>
    </application>
</manifest>

Hook IActivityManager方案

     1.通过AMS校验

Activity启动的时候会由AMS对Activity的合法性进行校验。插件Activity因为没有在AndroidManifest.xml文件中进行注册,不能直接通过startActivity方法启动插件Activity,所以这个时候就需要利用占坑Activity来欺骗AMS。

在Activity的启动的时候,会调用IActivityManager的startActivity方法。因为Hook点IActivityManager是一个接口,所有我们可以通过动态代理的方式接管IActivityManager的startActivity方法。首先定义一个IActivityManager的代理类IActivityManagerProxy:

public class IActivityManagerProxy implements InvocationHandler {
    private Object mActivityManager;
    private static final String TAG = "IActivityManagerProxy";
    public IActivityManagerProxy(Object activityManager) {
        this.mActivityManager = activityManager;
    }
    @Override
    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
        if ("startActivity".equals(method.getName())) {//1
            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();//2
            String packageName = "com.example.pluginactivity";
            subIntent.setClassName(packageName,packageName+".StubActivity");//3
            subIntent.putExtra(HookHelper.TARGET_INTENT, intent);//4
            args[index] = subIntent;
        }
        return method.invoke(mActivityManager, args);
    }
}

在注释1处接管startActivity方法,并且获取到args中的第一个Intent对象,它是插件TargetActivity的Intent对象。注释2处新建一个subIntent,然后在注释3处使用新建的subIntent来启动StubActivity,这个Activity就是之前占坑的Activity。注释4处是将插件TargetActivity的Intent保存到subIntent中。通过一系列操作之后,启动的目标就会变成StubActivity,这样我们就可以通过AMS的校验。原来的插件TargetActivity的信息被保存在StubActivity的Intent对象中,后续我们会根据这个对象取出插件TargetActivity。

然后我门就可以通过Hook的方式替换掉系统的IActivityManager:

public class HookHelper {
    public static final String TARGET_INTENT = "target_intent";
    public static void hookAMS() throws Exception {
        Object defaultSingleton=null;
        if (Build.VERSION.SDK_INT >= 26) {//1
            Class<?> activityManageClazz = Class.forName("android.app.ActivityManager");
            //获取activityManager中的IActivityManagerSingleton字段
            defaultSingleton=  FieldUtil.getField(activityManageClazz ,null,"IActivityManagerSingleton");
        } else {
            Class<?> activityManagerNativeClazz = Class.forName("android.app.ActivityManagerNative");
            //获取ActivityManagerNative中的gDefault字段
            defaultSingleton=  FieldUtil.getField(activityManagerNativeClazz,null,"gDefault");
        }
        Class<?> singletonClazz = Class.forName("android.util.Singleton");
        Field mInstanceField= FieldUtil.getField(singletonClazz ,"mInstance");
        //获取iActivityManager
        Object iActivityManager = mInstanceField.get(defaultSingleton);//2
        Class<?> iActivityManagerClazz = Class.forName("android.app.IActivityManager");
        Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(),
                new Class<?>[] { iActivityManagerClazz }, new IActivityManagerProxy(iActivityManager));
        mInstanceField.set(defaultSingleton, proxy);//3
    }
}

Android 8.0和8.0以下的源码是不一样的,所有需要在注释1处作区分。在注释2处拿到系统的IActivityManager对象,然后在注释3处用前面创建的代理类IActivityManagerProxy替换IActivityManager。最后再自定义的Application中的attachBaseContext方法中调用HookHelper.hookAMS()。

这时候当我们想要启动插件TargetActivity的时候会发现启动的并不是TargetActivity,而是StubActivity。说明我们已经成功利用StubActivity通过了AMS的校验。下面我们就可以进行下一步操作,还原插件Activity。

2.还原插件Activity

熟悉Activity启动流程的同学应该知道,启动Activity的操作是由H类中的handleMessage方法处理的,H类继承自Handler。所以如果我们能够拦截H类的handleMessage方法,就可以还原插件Activity。具体实现我们可以把Handler的成员mCallback作为Hook点,通过创建一个Handler.Callback类型的对象来替换掉H类的成员mCallback,这样我们就能拦截H类中的handleMessage方法。不清楚为什么的可以看看Handler的源码

首先创建一个继承自Handler.Callback类型的类:

public class HCallback implements Handler.Callback{
    public 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 r = msg.obj;
            try {
                //1.得到消息中的Intent(启动SubActivity的Intent)
                Intent intent = (Intent) FieldUtil.getField(r.getClass(), r, "intent");
                //2.得到此前保存起来的Intent(启动TargetActivity的Intent)
                Intent target = intent.getParcelableExtra(HookHelper.TARGET_INTENT);
                //3.将启动SubActivity的Intent替换为启动TargetActivity的Intent
                intent.setComponent(target.getComponent());
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        //4
        mHandler.handleMessage(msg);
        return true;
    }
}

在注释1处得到之前占坑Activity的Intent对象,注释2处从占坑Activity的Intent中取出插件TargetActivity的Intent对象。注释3是将占坑Activity的Intent替换为插件TargetActivity的Intent,然后在注释4处交由H类分发。

接着就是Hook H类的成员mCallback:

public static void hookHandler() throws Exception {
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Object currentActivityThread= FieldUtil.getField(activityThreadClass ,null,"sCurrentActivityThread");
        Field mHField = FieldUtil.getField(activityThread,"mH");
        Handler mH = (Handler) mHField.get(currentActivityThread);
        FieldUtil.setField(Handler.class,mH,"mCallback",new HCallback(mH));
    }

获取到ActivityThread中的成员mH,这个成员就是H类的对象。然后在用自定义的Callback替换掉mH的成员mCallback。同样的我们需要在自定义Application中的attachBaseContext方法中调用HookHelper.hookHandler方法。

当我们再次从宿主中启动插件Activity时,会发现这次启动的正是插件TargetActivity。

Hook Instrumentation方案

Hook Instrumentation方案同样也需要利用占坑Activity通过AMS的校验和还原插件Activity,只是具体的实现会不一样。

1.通过AMS校验

首先自定义一个Instrumentation,然后在execStartActivity方法中将启动的插件TargetActivity替换为StubActivity:

public class InstrumentationProxy extends Instrumentation {
    private Instrumentation mInstrumentation;
    private PackageManager mPackageManager;
    public InstrumentationProxy(Instrumentation instrumentation, PackageManager packageManager) {
        mInstrumentation = instrumentation;
        mPackageManager = packageManager;
    }
    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);
        if (infos == null || infos.size() == 0) {
            intent.putExtra(HookHelper.TARGET_INTENsT_NAME, intent.getComponent().getClassName());//1
            intent.setClassName(who, "com.example.pluginactivity.StubActivity");//2
        }
        try {
            //3
            Method execMethod = Instrumentation.class.getDeclaredMethod("execStartActivity",
                    Context.class, IBinder.class, IBinder.class, Activity.class, Intent.class, int.class, Bundle.class);
            return (ActivityResult) execMethod.invoke(mInstrumentation, who, contextThread, token,
                    target, intent, requestCode, options);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

注释1处是将插件TargetActivity的ClassName保存起来,注释2是将插件TargetActivity替换为StubActivity,注释3是反射调用execStartActivity方法。这样我们就能利用占坑Activity通过AMS的验证

2.还原插件Activity

还原插件Activity比较简单,我们可以通过自定义Instrumentation的newActivity方法中还原插件Activity:

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

注释1处获取插件TargetActivity的ClassName,如果获取到了就会这注释2处将插件TargetActivity的ClassName传入父类的newActivity方法中,否则就说明需要启动的Activity不是插件Activity。

紧接着就是用自定义的Instrumentation替换系统的Instrumentation:

public static void hookInstrumentation(Context context) throws Exception {
        Class<?> contextImplClass = Class.forName("android.app.ContextImpl");
        Field mMainThreadField  =FieldUtil.getField(contextImplClass,"mMainThread");
        Object activityThread = mMainThreadField.get(context);
        Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
        Field mInstrumentationField=FieldUtil.getField(activityThreadClass,"mInstrumentation");
        FieldUtil.setField(activityThreadClass,activityThread,"mInstrumentation",new InstrumentationProxy((Instrumentation) mInstrumentationField.get(activityThread),
                context.getPackageManager()));
    }

最后在自定义Application中的attachBaseContext方法中调用HookHelper.hookInstrumentation()方法。运行程序之后发现,启动的正是插件Activity。

总结

Activity插件化是整个插件化技术的重点,本文针对Activity插件化的两种Hook方案作了讲解,想要完全理解Activity的插件化,需要对Activity的启动流程有所了解。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值