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


相关阅读

插件化知识梳理(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 知识梳理系列:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,524评论 5 460
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,869评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,813评论 0 320
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,210评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,085评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,117评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,533评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,219评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,487评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,582评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,362评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,218评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,589评论 3 299
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,899评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,176评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,503评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,707评论 2 335

推荐阅读更多精彩内容