美文网首页Android面试知识Android知识Android开发
插件化知识梳理(6) - Small 源码分析之 Hook 原理

插件化知识梳理(6) - Small 源码分析之 Hook 原理

作者: 泽毛 | 来源:发表于2017-06-07 21:12 被阅读320次

    相关阅读

    插件化知识梳理(1) - Small 框架之如何引入应用插件
    插件化知识梳理(2) - Small 框架之如何引入公共库插件
    插件化知识梳理(3) - Small 框架之宿主分身
    插件化知识梳理(4) - Small 框架之如何实现插件更新
    插件化知识梳理(5) - Small 框架之如何不将插件打包到宿主中
    插件化知识梳理(6) - Small 源码分析之 Hook 原理
    插件化知识梳理(7) - 类的动态加载入门
    插件化知识梳理(8) - 类的动态加载源码分析
    插件化知识梳理(9) - 资源的动态加载示例及源码分析
    插件化知识梳理(10) - Service 插件化实现及原理


    一、前言

    至此,花了四天时间、五篇文章,学习了如何使用Small框架来实现插件化。但是,对于我来说,一开始的目标就不是满足于仅仅知道如何用,而是希望通过这一框架作为平台,学习插件化中所用到的知识。

    对于许多插件化的开源框架而言,一个比较核心的部分就是Hook的实现,所谓Hook,简单地来说就是在应用侧启动A.Activity,但是在AMS看来却是启动的B.Activity,之后AMS通知应用侧后,我们再重新替换成A.Activity

    在阅读这篇文章之前,大家可以先看一下之前的这篇文章 Framework 源码解析知识梳理(1) - 应用进程与 AMS 的通信实现Small其实就是通过替换这一双向通信过程中的关键类,对调用方法中传递的参数进行替换,来实现Hook机制。

    二、源码分析

    Hook的过程是Small预初始化的第一步,就是我们前面在自定义的Application构造方法中所进行的操作:

    public class SmallApp extends Application {
    
        public SmallApp() {
            Small.preSetUp(this);
        }
    
    }
    

    SmallpreSetUp(Application context)函数中,做了下面的两件事:

    • 实例化三个BundleLauncher的实现类,添加到Bundle类中的静态变量sBundleLaunchers中,这三个类的继承关系为:

    • 依次调用这个三个实现类的onCreate()方法。

        public static void preSetUp(Application context) {
            //1.添加关键的 BundleLauncher。
            registerLauncher(new ActivityLauncher());
            registerLauncher(new ApkBundleLauncher());
            registerLauncher(new WebBundleLauncher());
            //2.调用 BundleLauncher 的 onCreate() 方法。
            Bundle.onCreateLaunchers(context);
        }
    

    对于之前添加进入的三个实现类,只有ApkBundleLauncher()实现了onCreate()方法,其它两个都是空实现。

        protected static void onCreateLaunchers(Application app) {
            //调用之前添加进入的 BundleLauncher 的 onCreate() 方法。
            for (BundleLauncher launcher : sBundleLaunchers) {
                launcher.onCreate(app);
            }
        }
    

    我们看一下ApkBundleLauncher的内部实现,这里就是Hook的实现代码:

        @Override
        public void onCreate(Application app) {
            super.onCreate(app);
    
            Object/*ActivityThread*/ thread;
            List<ProviderInfo> providers;
            Instrumentation base;
            ApkBundleLauncher.InstrumentationWrapper wrapper;
            Field f;
    
            // Get activity thread
            thread = ReflectAccelerator.getActivityThread(app);
    
            // Replace instrumentation
            try {
                f = thread.getClass().getDeclaredField("mInstrumentation");
                f.setAccessible(true);
                base = (Instrumentation) f.get(thread);
                wrapper = new ApkBundleLauncher.InstrumentationWrapper(base);
                f.set(thread, wrapper);
            } catch (Exception e) {
                throw new RuntimeException("Failed to replace instrumentation for thread: " + thread);
            }
    
            // Inject message handler
            ensureInjectMessageHandler(thread);
    
            // Get providers
            try {
                f = thread.getClass().getDeclaredField("mBoundApplication");
                f.setAccessible(true);
                Object/*AppBindData*/ data = f.get(thread);
                f = data.getClass().getDeclaredField("providers");
                f.setAccessible(true);
                providers = (List<ProviderInfo>) f.get(data);
            } catch (Exception e) {
                throw new RuntimeException("Failed to get providers from thread: " + thread);
            }
    
            sActivityThread = thread;
            sProviders = providers;
            sHostInstrumentation = base;
            sBundleInstrumentation = wrapper;
        }
    

    (1) 获得当前应用进程的 ActivityThread 实例

    首先,我们通过反射获得当前应用进程的ActivityThread实例

    thread = ReflectAccelerator.getActivityThread(app)
    

    具体的逻辑为:

        public static Object getActivityThread(Context context) {
            try {
                //1.首先尝试通过 ActivityThread 内部的静态变量获取。
                Class activityThread = Class.forName("android.app.ActivityThread");
                // ActivityThread.currentActivityThread()
                Method m = activityThread.getMethod("currentActivityThread", new Class[0]);
                m.setAccessible(true);
                Object thread = m.invoke(null, new Object[0]);
                if (thread != null) return thread;
    
                //2.静态变量获取失败,那么再通过 Application 的 mLoadedApk 中的 mActivityThread 获取。
                Field mLoadedApk = context.getClass().getField("mLoadedApk");
                mLoadedApk.setAccessible(true);
                Object apk = mLoadedApk.get(context);
                Field mActivityThreadField = apk.getClass().getDeclaredField("mActivityThread");
                mActivityThreadField.setAccessible(true);
                return mActivityThreadField.get(apk);
            } catch (Throwable ignore) {
                throw new RuntimeException("Failed to get mActivityThread from context: " + context);
            }
        }
    

    这里面的逻辑为:

    • 通过ActivityThread中的静态方法currentActivityThread来获取:
        public static ActivityThread currentActivityThread() {
            return sCurrentActivityThread;
        }
    

    sCurrentActivityThread是在ActivityThread#attach(boolean)方法中被赋值的,而attach方法则是在入口函数main中调用的:

       public static void main(String[] args) {
            //创建应用进程的 ActivityThread 实例。
            ActivityThread thread = new ActivityThread();
            thread.attach(false);
        }
    
    • 如果上面的方法获取失败,那么我们再尝试获取Application中的LoadedApk#mActivityThread

    (2) 替换 ActivityThread 中的 mInstrumentation

    通过(1)拿到ActivityThread实例之后,接下来就是替换其中mInstrumentation成员变量为Small自己的实现类ApkBundleLauncher.InstrumentationWrapper,并将原始的mInstrumentation传入作为其成员变量。

    正如 Framework 源码解析知识梳理(1) - 应用进程与 AMS 的通信实现 中所介绍的,当我们调用startActivity之后,那么会调用到它内部的mInstrumentationexecStartActivity方法,经过替换之后,就会调用ApkBundleLauncher.InstrumentationWrapper的对应方法,下面截图中的mBase就是原始的mInstrumentation


    ReflectAccelerator又通过反射调用了mBase的对应方法:

    由此可见,hook的目的就在于替代者的方法被调用,到调用原始对象的对应方法之间所进行的操作,也就是下面红色框中的这两句:

    首先看一下wrap(Intent intent)方法,它的作用为:当我们在应用侧启动一个插件Activity时,需要将它替换成为AndroidManifest.xml预先注册好的占坑Activity

            private void wrapIntent(Intent intent) {
                ComponentName component = intent.getComponent();
                String realClazz;
                //判断是否显示地设置了目标组件的类名。
                if (component == null) {
                    //如果没有显示设置 Component,那么通过 resolveActivity 来解析出目标组件。
                    component = intent.resolveActivity(Small.getContext().getPackageManager());
                    if (component != null) {
                        return;
                    }
    
                    //获得目标组件全路径名。
                    realClazz = resolveActivity(intent);
                    if (realClazz == null) {
                        return;
                    }
                } else {
                    //如果设置了类名,那么直接取出。
                    realClazz = component.getClassName();
                    if (realClazz.startsWith(STUB_ACTIVITY_PREFIX)) {
                        realClazz = unwrapIntent(intent);
                    }
                }
                if (sLoadedActivities == null) return;
                //根据类名,确定它是否是插件当中的 Activity 
                ActivityInfo ai = sLoadedActivities.get(realClazz);
                if (ai == null) return;
                //将真实的 Activity 保存在 Category 中,并加上 > 标识符。
                intent.addCategory(REDIRECT_FLAG + realClazz);
                //选取占坑的 Activity 
                String stubClazz = dequeueStubActivity(ai, realClazz);
                //重新设置 intent,用占坑的 Activity 来替代目标 Activity 
                intent.setComponent(new ComponentName(Small.getContext(), stubClazz));
            }
    

    其中,dequeueStubActivity就是取出占坑的Activity,它是预先在AndroidManifest.xml中注册的一些占坑Activity,同时,我们也会把真实的目标Activity放在Category字段当中。

    接下来,再看一下ensureInjectMessageHandler(Object thread)函数,代码的逻辑很简单,就是替换AcitivtyThreadmH中的mCallback变量为sActivityThreadHandlerCallback,它的类型为ActivityThreadHandlerCallback,是我们自定的一个内部类。

        private static void ensureInjectMessageHandler(Object thread) {
            try {
                Field f = thread.getClass().getDeclaredField("mH");
                f.setAccessible(true);
                Handler ah = (Handler) f.get(thread);
                f = Handler.class.getDeclaredField("mCallback");
                f.setAccessible(true);
    
                boolean needsInject = false;
                if (sActivityThreadHandlerCallback == null) {
                    needsInject = true;
                } else {
                    Object callback = f.get(ah);
                    if (callback != sActivityThreadHandlerCallback) {
                        needsInject = true;
                    }
                }
    
                if (needsInject) {
                    // Inject message handler
                    sActivityThreadHandlerCallback = new ActivityThreadHandlerCallback();
                    f.set(ah, sActivityThreadHandlerCallback);
                }
            } catch (Exception e) {
                throw new RuntimeException("Failed to replace message handler for thread: " + thread);
            }
        }
    

    Framework 源码解析知识梳理(1) - 应用进程与 AMS 的通信实现 我们分析过,Activity的生命周期是由AMS使用运行在系统进程的代理对象ApplicationThreadProxy,通过Binder通信发送消息,在应用进程中的ActivityThread#ApplicationThreadonTransact()收到消息后,再通过mH(一个自定的Handler,类型为H),发送消息到主线程,HhandleMessage中处理消息,回调Activity对应的生命周期方法。

    ensureInjectMessageHandler所做就是让HhandleMessage方法被调用之前,进行一些额外的操作,例如在占坑的Activity启动完成之后,将它在应用测的记录替换成为Activity,而这一过程是通过替换Handler当中的mCallback对象,因为在调用handleMessage之前,会先去调用mCallbackhandleMessage,并且在其不返回true的情况下,会继续调用Handler本身的handleMessage方法:

    对于Small来说,它会对以下四种类型的消息进行拦截:

    我们以redirectActivity为例,看一下将占坑的Activity重新替换为真实的Activity的过程。

            private void redirectActivity(Message msg) {
                Object/*ActivityClientRecord*/ r = msg.obj;
                //通过反射获得启动该 Activity 的 intent。
                Intent intent = ReflectAccelerator.getIntent(r);
                //就是通过前面放在 Category 中的字段,来取得真实的 Activity 名字。
                String targetClass = unwrapIntent(intent);
                boolean hasSetUp = Small.hasSetUp();
                if (targetClass == null) {
                    if (hasSetUp) return; // nothing to do
                    if (intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
                        return;
                    }
                    Small.setUpOnDemand();
                    return;
                }
                if (!hasSetUp) {
                    //确保初始化了。
                    Small.setUp();
                }
                //重新替换为真实的 Activity
                ActivityInfo targetInfo = sLoadedActivities.get(targetClass);
                ReflectAccelerator.setActivityInfo(r, targetInfo);
            }
    

    由于handleMessage的返回值为false,按照前面的分析,mHhandleMessage方法也会得到执行。

    以上就是整个Hook的过程,简单的总结下来就是在应用进程与AMS进程的通信过程的某个节点,通过替换类的方式,插入一些逻辑,以绕过系统的检查:

    • 从应用进程到AMS所在进程的通信,是通过替换应用进程中的ActivityThreadmInstrumentation为自定义的ApkBundleLauncher.InstrumentationWrapper,在其中将Intent当中真实的Activity替换成为占坑的Activity,然后再调用原始的mInstrumentation通知AMS
    • AMS所在进行到应用进程的通信,是通过替换应用进程中的H中的mCallback,在其中将占坑Activity替换成为真实的Activity,再执行原本的操作。

    (3) ActivityThread 内部的 mBoundApplication 变量

    这一步没有进行Hook操作,而是先获得ActivityThread内部的mBoundApplication实例,然后获得该实例内部的providers变量,它的类型为List<ProviderInfo>

    (4) 备份

    最后一步,就是备份一些关键变量,用于之后的操作:

            //ActivityThread 实例
            sActivityThread = thread;
            //List<ProviderInfo> 实例
            sProviders = providers;
            //原始的 Instrumentation 实例
            sHostInstrumentation = base;
            //执行 Hook 操作的 Instrumentation 实例
            sBundleInstrumentation = wrapper;
    

    三、实例分析

    以上就是源码分析部分,下面,我们通过一个启动插件Activity的过程,来验证一下前面的分析:

    3.1 从应用进程到 AMS 进程

    通过下面的方法启动一个插件Activity

        public void startStubActivity(View view) {
            Small.openUri("upgrade", this);
        }
    

    按照前面的分析,此时应当会调用经过Hook之后的Instrumentation实例的execStartActivity方法,可以看到在wrapIntent方法调用之前,我们的目标Activity仍然是真实的UpgradeActivity


    让断点继续往下走,经过wrapIntent之后,Intent的目标对象替换成为了占坑的Activity

    3.2 从 AMS 进程到应用进程

    而当AMS需要通知应用进程时,它第一次回调的是占坑的Activity,也就是如下所示:


    通过反射,我们修改ActivityClientRecord中的内容,让其还原成为真实的Activity

    四、小结

    以上就是Small预初始化所做的一些事情,也就是其Hook实现的原理,很多第三方的插件化都是基于该原理来实现启动不在AndroidManifest.xml中注册的组件的,开始的时候,理解起来可能会有点困难,关键是要弄清楚应用程序和AMS进程的交互原理,欢迎阅读 Framework 源码解析知识梳理(1) - 应用进程与 AMS 的通信实现


    更多文章,欢迎访问我的 Android 知识梳理系列:

    相关文章

      网友评论

      • different_loyal:mark楼主,对插件化开发系列的简文,我想抽出一天时间来看
        泽毛: @你可以叫我鑫仔 谢谢,多多交流😊

      本文标题:插件化知识梳理(6) - Small 源码分析之 Hook 原理

      本文链接:https://www.haomeiwen.com/subject/jqvnfxtx.html