手把手讲解 Android Hook-Activity的启动流程

前言

手把手讲解系列文章,是我写给各位看官,也是写给我自己的。
文章可能过分详细,但是这是为了帮助到尽量多的人,毕竟工作5,6年,不能老吸血,也到了回馈开源的时候.
这个系列的文章:
1、用通俗易懂的讲解方式,讲解一门技术的实用价值
2、详细书写源码的追踪,源码截图,绘制类的结构图,尽量详细地解释原理的探索过程
3、提供Github 的 可运行的Demo工程,但是我所提供代码,更多是提供思路,抛砖引玉,请酌情cv
4、集合整理原理探索过程中的一些坑,或者demo的运行过程中的注意事项
5、用gif图,最直观地展示demo运行效果

如果觉得细节太细,直接跳过看结论即可。
本人能力有限,如若发现描述不当之处,欢迎留言批评指正。

学到老活到老,路漫漫其修远兮。与众君共勉 !


引子

上一篇文章手把手讲解 Android Hook入门Demo 中,用了一个最最简单的案例 讲解hook是个什么玩意. 咱不能老玩低端,来点复杂的吧。Activity的启动流程,做安卓开发的人都是绕不开它的,但是要真正知悉其源码逻辑,还是不太容易.
先给出本文的代码Demo,有兴趣的大神们可以下载看看


鸣谢

翻了很多关于hook Activity启动流程的博客,这位大佬的文章给我的启发最大
https://blog.csdn.net/gdutxiaoxu/article/details/81459910
但是,可能大佬的博文对于有些基础不足的初中级安卓工程师还不够友好,所以我把大佬的思想用更通俗,更具象化的方式再展示一遍.并且,阅读源码的时候一些坑,我都会详细给出解决方案。


正文大纲

1. 两种启动Activity的方式源码追踪 示例代码,程序执行走向图.
2. 第一种启动方式的hook方案
3. 第二种启动方式的hook方案
4. 目前方案弊端分析
5. 最终解决方案
6. HOOK开发可能的坑


正文

1. 两种启动Activity的方式源码追踪 (源码基于 SDK 28 ~ android-9.0)

方式1:使用Activity自带的startActivity

示例代码

private void startActivityByActivity() {
        Intent i = new Intent(MainActivity.this, Main2Activity.class);
        startActivity(i);
    }

程序执行走向图.

代码追踪:


image.png

image.png

image.png
这里有个if(mParent==null)判定,先看true分支:

发现一个坑,mInstrumentation.execStartActivity 这里居然不能继续往下索引了?很奇怪,不过不重要,我们直接进入Instrumentation.java去找这个方法:

image.png

在这个execStartActivity中,可以找到关键代码

int result = ActivityManager.getService()
              .startActivity(whoThread, who.getBasePackageName(), intent,
                       intent.resolveTypeIfNeeded(who.getContentResolver()),
                      token, target != null ? target.mEmbeddedID : null,
                     requestCode, 0, null, options);
checkStartActivityResult(result, intent);

通过这种方式启动Activity,最终的执行权被交给了 ActivityManager.getService()(即AMS),它的作用是 启动一个Activity并且返回result,然后checkStartActivityResult(result, intent);这句话,对当前的跳转意图intent进行检测;

image.png

have you declared this activity in your AndroidManifest.xml 这句异常应该很熟悉了吧?启动一个没有注册的Activity的报错.

再看个if(mParent==null)false分支:

image.png

image.png

控制权依然是交给了mInstrumentation.execStartActivity(),剩余的代码索引和上面的一样.

所以,代码索引的结论,按照一张图来表示就是:

代码索引结论图1.png

方式2:使用applictonContextstartActivity
private void startActivityByApplicationContext() {
        Intent i = new Intent(MainActivity.this, Main2Activity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        getApplicationContext().startActivity(i);
    }

方式1 中已经展示了源码索引的方式,所以这里不再赘述贴图.直接给出代码索引结论图:

代码索引结论图2.png

两张图对比,我们很容易得出一个结论:
启动Activity的最终执行权,都被交给了 Instrumentation.java 类,
方式1:Activity.startActivity的最终执行者是 它的mInstrumentation成员,mInstrumentation的持有者是 Activity自身.
方式2:getApplicationContext().startActivity(i); 的最终执行者是:ActivityThreadmInstrumentation成员,持有者是ActivityThread 主线程.
两种方式都可以把mInstrumentation当作hook切入点,将它从它的持有者中"偷梁换柱".

下面开始动手尝试:


2. 第一种启动方式的hook方案

创建一个HookActivityHelper.java ,然后三步走:

  1. 找到hook点,以及hook对象的持有者,上文中已经说明:hook点是ActivitymInstrumentation成员,持有者就是Activity
           Field mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation");
           mInstrumentationField.setAccessible(true);
           Instrumentation base = (Instrumentation) mInstrumentationField.get(activity);

base是系统原来的执行逻辑,存起来后面用得着.

  1. 创建Instrumentation代理类, 继承Instrumentation然后,重写execStartActivity方法,加入自己的逻辑,然后再执行系统的逻辑.
private static class ProxyInstrumentation extends Instrumentation {
        public ProxyInstrumentation(Instrumentation base) {
            this.base = base;
        }

        Instrumentation base;

        public ActivityResult execStartActivity(
                Context who, IBinder contextThread, IBinder token, Activity target,
                Intent intent, int requestCode, Bundle options) {

            Log.d("ProxyInstrumentation", "我们自己的逻辑");

            //这里还要执行系统的原本逻辑,但是突然发现,这个execStartActivity居然是hide的,只能反射咯
            try {
                Class<?> InstrumentationClz = Class.forName("android.app.Instrumentation");
                Method execStartActivity = InstrumentationClz.getDeclaredMethod("execStartActivity",
                        Context.class, IBinder.class, IBinder.class, Activity.class,
                        Intent.class, int.class, Bundle.class);
                return (ActivityResult) execStartActivity.invoke(base, 
                            who, contextThread, token, target, intent, requestCode, options);
            } catch (Exception e) {
                e.printStackTrace();
            }

            return null;
        }

    }
  1. 用代理类对象替换 hook对象.
  ProxyInstrumentation proxyInstrumentation = new ProxyInstrumentation(base);
  mInstrumentationField.set(activity, proxyInstrumentation);

如何使用: 在MainActivityonCreate中加入一行ActivityHookHelper.hook(this)

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        ActivityHookHelper.hook(this);
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivityByActivity();
            }
        });

    }

    private void startActivityByActivity() {
        Intent i = new Intent(MainActivity.this, Main2Activity.class);
        startActivity(i);
    }

   
}

效果:跳转依然正常,并且logcat中可以发现下面的日志.

image.png
ok,插入自己的逻辑,成功

3. 第二种启动方式的hook方案

创建ApplicationContextHookHelper.java,然后 同样是三步走

1.确定hook的对象和该对象的持有者
锁定 ActivityThreadmInstrumentation成员.

            //1.主线程ActivityThread内部的mInstrumentation对象,先把他拿出来
            Class<?> ActivityThreadClz = Class.forName("android.app.ActivityThread");
            //再拿到sCurrentActivityThread
            Field sCurrentActivityThreadField = ActivityThreadClz.getDeclaredField("sCurrentActivityThread");
            sCurrentActivityThreadField.setAccessible(true);
            Object activityThreadObj = sCurrentActivityThreadField.get(null);//静态变量的属性get不需要参数,传null即可.
            //再去拿它的mInstrumentation
            Field mInstrumentationField = ActivityThreadClz.getDeclaredField("mInstrumentation");
            mInstrumentationField.setAccessible(true);
            Instrumentation base = (Instrumentation) mInstrumentationField.get(activityThreadObj);// OK,拿到

2.创建代理对象 和上面的代理类一模一样,就不重复贴代码了

            //2.构建自己的代理对象,这里Instrumentation是一个class,而不是接口,所以只能用创建内部类的方式来做
            ProxyInstrumentation proxyInstrumentation = new ProxyInstrumentation(base);

3.替换掉原对象

            //3.偷梁换柱
            mInstrumentationField.set(activityThreadObj, proxyInstrumentation);

如何使用: 在Main4ActivityonCreate中加入一行ApplicationContextHookHelper.hook();

public class Main4Activity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main4);

        ApplicationContextHookHelper.hook();
        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                startActivityByApplicationContext();
            }
        });
    }

    private void startActivityByApplicationContext() {
        Intent i = new Intent(Main4Activity.this, Main5Activity.class);
        i.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
        getApplicationContext().startActivity(i);
    }
}

效果

image.png

OK,第二种启动方式,我们也可以加入自己的逻辑了.hook成功!


4. 目前方案弊端分析

启动方式1的hook: 只是在针对单个Activity类,来进行hook,多个Activity则需要写多次,或者写在BaseActivity里面.
启动方式2的hook:可以针对全局进行hook,无论多少个Activity,只需要调用一次ApplicationContextHookHelper.hook();函数即可,但是,它只能针对 getApplicationContext().startActivity(i); 普通的Activity.startActivity则不能起作用.

那么有没有一种完全体的解决方案:能够在全局起作用,并且可以在两种启动方式下都能hook.
回顾之前的两张代码索引结论图,会发现,两种启动Activity的方式,最终都被执行到了 AMS内部,
下一步,尝试hook AMS.


5. 最终解决方案

代码索引: 基于SDK 28 ~ android9.0

下方红框标记的部分,就是取得AMSActivityManagerService实例)的代码.

image.png

如果可以在系统接收到AMS实例之前,把他了,是不是就可以达到我们的目的?
进去看看getService的代码:

image.png

真正的AMS实例来自一个Singleton单例辅助类的create()方法,并且这个Singleton单例类,提供get方法,获得真正的实例.

image.png

那么,我们从这个单例中,就可以获得系统当前的 AMS实例,将它取出来,然后保存.
OK,确认:
hook对象: ActivityManagerIActivityManagerSingleton成员 变量内的 单例 mInstance.
hook对象的持有者:ActivityManagerIActivityManagerSingleton成员变量

那么,动手:

  1. 找到hook对象,并且存起来
            //1.把hook的对象取出来保存
            //矮油,静态的耶,开心.
            Class<?> ActivityManagerClz = Class.forName("android.app.ActivityManager");
            Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService");
            final Object IActivityManagerObj = getServiceMethod.invoke(null);//OK,已经取得这个系统自己的AMS实例
  1. 创建自己的代理类对象,IActivityManager 是一个AIDL生成的动态接口类,所以在编译时,androidStudio会找不到这个类,所以,先反射,然后用Proxy进行创建代理。
            //2.现在创建我们的AMS实例
            //由于IActivityManager是一个接口,那么我们可以使用Proxy类来进行代理对象的创建
            // 结果被摆了一道,IActivityManager这玩意居然还是个AIDL,动态生成的类,编译器还不认识这个类,怎么办?反射咯
            Class<?> IActivityManagerClz = Class.forName("android.app.IActivityManager");
            Object proxyIActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), 
                new Class[]{IActivityManagerClz}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //proxy是创建出来的代理类,method是接口中的方法,args是接口执行时的实参
                    if (method.getName().equals("startActivity")) {
                        Log.d("GlobalActivityHook", "全局hook 到了 startActivity");
                    }
                    return method.invoke(IActivityManagerObj, args);
                }
            });
  1. 偷梁换柱:这次有点复杂, 不再是简单的field.set,因为这次的hook对象被包裹在了一个Singleton里。
           //3.偷梁换柱,这里有点纠结,这个实例居然被藏在了一个单例辅助类里面
            Field IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("IActivityManagerSingleton");
            IActivityManagerSingletonField.setAccessible(true);
            Object IActivityManagerSingletonObj = IActivityManagerSingletonField.get(null);
            //反射创建一个Singleton的class
            Class<?> SingletonClz = Class.forName("android.util.Singleton");
            Field mInstanceField = SingletonClz.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            mInstanceField.set(IActivityManagerSingletonObj, proxyIActivityManager);

使用方法:老样子,在你自己的Activity onCreate里面加入GlobalActivityHookHelper.hook();
运行起来,预期结果应该是:能够在logcat中看到日志 :
GlobalActivityHook - 全局hook 到了 startActivity;
但是,你运行起来可能看不到这一行。

如果你看不到这个日志,那么原因就是:

程序报错了,

报错啦!

没有这样的方法,怎么回事?
debug找原因:
image.png

为什么会没有getService这个方法!?
查看了我当前设备的系统版本号
image.png

居然是23版本,6.0.
所以,恍然大悟,我们写的hook代码并没有兼容性,遇到低版本的设备,就失灵了.

解决方案:

1.找到SDK 23的源码
(注意,前方有坑,androidStudio,你如果直接把combileSDK改成23.会出现很多位置问题,所以不建议这么做. 但是我们一定要看SDK 23的源码,怎么办?

2.查看getService方法不存在的原因,两个版本28 和 23,在这一块代码上有什么不同.
3.改造 GlobalActivityHookHelper.java ,判定当前设备的系统版本号,让它可以兼容所有版本.

按照上面的步骤:
我发现SDK 23里面:
Instrumentation类的 execStartActivitiesAsUser(Context who, IBinder contextThread, IBinder token, Activity target, Intent[] intents, Bundle options, int userId)方法里,获取AMS实例的方式完全不同.

image.png

它是使用 ActivityManagerNative.getDefault()来获得的,继续往下找,看看有没有什么不同。
进去ActivityManagerNative 找找看:
image.png

image.png

OK,找到了区别,确定结论:SDK 2823在这块代码上的区别就是:
获得AMS实例的类名和方法名都不同.另外,查了度娘之后发现,这个变化是在SDK 26版本修改的,所以26和26以后,ActivityManager.getService()来获取,26以前,用ActivityManagerNative.getDefault()来获得
调整当前的hook方法,修改为下面这样:

public class GlobalActivityHookHelper {

    //设备系统版本是不是大于等于26
    private static boolean ifSdkOverIncluding26() {
        int SDK_INT = Build.VERSION.SDK_INT;
        if (SDK_INT > 26 || SDK_INT == 26) {
            return true;
        } else {
            return false;
        }
    }

    public static void hook() {

        try {
            Class<?> ActivityManagerClz;
            final Object IActivityManagerObj;
            if (ifSdkOverIncluding26()) {
                ActivityManagerClz = Class.forName("android.app.ActivityManager");
                Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService");
                IActivityManagerObj = getServiceMethod.invoke(null);//OK,已经取得这个系统自己的AMS实例
            } else {
                ActivityManagerClz = Class.forName("android.app.ActivityManagerNative");
                Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getDefault");
                IActivityManagerObj = getServiceMethod.invoke(null);//OK,已经取得这个系统自己的AMS实例
            }

            //2.现在创建我们的AMS实例
            //由于IActivityManager是一个接口,那么其实我们可以使用Proxy类来进行代理对象的创建
            // 结果被摆了一道,IActivityManager这玩意居然还是个AIDL,动态生成的类,编译器还不认识这个类,怎么办?反射咯
            Class<?> IActivityManagerClz = Class.forName("android.app.IActivityManager");
            Object proxyIActivityManager = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class[]{IActivityManagerClz}, new InvocationHandler() {
                @Override
                public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                    //proxy是创建出来的代理类,method是接口中的方法,args是接口执行时的实参
                    if (method.getName().equals("startActivity")) {
                        Log.d("GlobalActivityHook", "全局hook 到了 startActivity");
                    }
                    return method.invoke(IActivityManagerObj, args);
                }
            });

            //3.偷梁换柱,这里有点纠结,这个实例居然被藏在了一个单例辅助类里面
            Field IActivityManagerSingletonField;
            if (ifSdkOverIncluding26()) {
                IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("IActivityManagerSingleton");
            } else {
                IActivityManagerSingletonField = ActivityManagerClz.getDeclaredField("gDefault");
            }

            IActivityManagerSingletonField.setAccessible(true);
            Object IActivityManagerSingletonObj = IActivityManagerSingletonField.get(null);
            Class<?> SingletonClz = Class.forName("android.util.Singleton");//反射创建一个Singleton的class
            Field mInstanceField = SingletonClz.getDeclaredField("mInstance");
            mInstanceField.setAccessible(true);
            mInstanceField.set(IActivityManagerSingletonObj, proxyIActivityManager);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

再次尝试:

image.png

成功,实现了全局范围内的startActivity动作的hook.


6. HOOK开发可能的坑

1. androidStudio阅读源码很多类无法索引,这是因为有一些类是@hide的,无法Ctrl点进去,
解决方案:Ctrl+shift+R 输入类名,手动进入.

2.androidStudio阅读源码直接报红 :或者一些是AIDL动态生成的接口,无法直接查看,比IActivityManager. ,
解决方案:这种接口不用管它,如果非要用到它,那就使用本类的包名+IActivityManager作为全限定名,去反射创建它.

3. hook开发,是学习源码思想,改变源码执行流程,所以,在多个版本的设备上运行,很容易发生不兼容的情况.
解决方案:找到不兼容的设备版本,根据报的异常,参照源码的版本变迁做出相应的兼容性改动.


结语

历时3天,忙里偷闲,总算是写完了.
喜欢的客官帮忙点个赞哦,你们的鼓励是我最大的动力,以后还会更新更多干货.
最后~本文的代码Demo奉上.

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

推荐阅读更多精彩内容