前言
手把手讲解系列文章,是我写给各位看官,也是写给我自己的。
文章可能过分详细,但是这是为了帮助到尽量多的人,毕竟工作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:使用applictonContext
的startActivity
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);
的最终执行者是:ActivityThread
的 mInstrumentation
成员,持有者是ActivityThread
主线程.
两种方式都可以把mInstrumentation
当作hook切入点,将它从它的持有者中"偷梁换柱".
下面开始动手尝试:
2. 第一种启动方式的hook方案
创建一个HookActivityHelper.java
,然后三步走:
找到
hook
点,以及hook
对象的持有者,上文中已经说明:hook
点是Activity
的mInstrumentation
成员,持有者就是Activity
Field mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation"); mInstrumentationField.setAccessible(true); Instrumentation base = (Instrumentation) mInstrumentationField.get(activity);
base
是系统原来的执行逻辑,存起来后面用得着.
创建
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; } }
用代理类对象替换
hook
对象.
ProxyInstrumentation proxyInstrumentation = new ProxyInstrumentation(base); mInstrumentationField.set(activity, proxyInstrumentation);
如何使用: 在
MainActivity
的onCreate
中加入一行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的对象和该对象的持有者
锁定ActivityThread
的mInstrumentation
成员.
//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);
如何使用: 在
Main4Activity
的onCreate
中加入一行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
下方红框标记的部分,就是取得
AMS
(ActivityManagerService
实例)的代码.image.png
如果可以在系统接收到AMS实例之前,把他截
了,是不是就可以达到我们的目的?
进去看看getService的代码:
image.png
真正的AMS
实例来自一个Singleton
单例辅助类的create()
方法,并且这个Singleton
单例类,提供get
方法,获得真正的实例.
image.png
那么,我们从这个单例中,就可以获得系统当前的 AMS
实例,将它取出来,然后保存.
OK,确认:hook
对象: ActivityManager
的IActivityManagerSingleton
成员 变量内的 单例 mInstance
.hook
对象的持有者:ActivityManager
的IActivityManagerSingleton
成员变量
那么,动手:
找到
hook
对象,并且存起来
//1.把hook的对象取出来保存 //矮油,静态的耶,开心. Class<?> ActivityManagerClz = Class.forName("android.app.ActivityManager"); Method getServiceMethod = ActivityManagerClz.getDeclaredMethod("getService"); final Object IActivityManagerObj = getServiceMethod.invoke(null);//OK,已经取得这个系统自己的AMS实例
创建自己的代理类对象,
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); } });
偷梁换柱:这次有点复杂, 不再是简单的
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
的源码,怎么办?
从谷歌官网下载
SDK 23
的源码,然后用SourceInsight
查看)
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 28
和23
在这块代码上的区别就是:
获得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
开发,是学习源码思想,改变源码执行流程,所以,在多个版本的设备上运行,很容易发生不兼容的情况.
解决方案:找到不兼容的设备版本,根据报的异常,参照源码的版本变迁做出相应的兼容性改动.
作者:波澜步惊
链接:https://www.jianshu.com/p/efce746836f5
共同学习,写下你的评论
评论加载中...
作者其他优质文章