Android Hook技术详解

由于Android Hook技术底层原理其实说白了就是java的反射和动态代理,所以这里我们先来讲一下代理模式。

代理模式

代理模式主要是为了给某些不想直接访问或者访问起来有些困难的对象提供一个代理对象来简洁的访问,分为静态代理和动态代理。

静态代理

首先我们先来讲下静态代理,这里举一个小例子,我想要买一双aj,然后就朋友圈找了个微商代理:

在这里插入图片描述
在这里插入图片描述

代理类,也就是微商:
在这里插入图片描述

让我们来看下输出结果:
在这里插入图片描述

动态代理

相对于静态代理,在代码运行前就已经存在了代理类的calss编译文件,动态代理则是在代码运行时通过反射来动态的生成代理类的对象,并确定要代理谁。接下来我们来看代码,同样的例子:

在这里插入图片描述

在这里插入图片描述

java提供了动态的代理接口InvocationHandler:
在这里插入图片描述

在这里插入图片描述

来看下运行结果:
在这里插入图片描述

Android Hook

在Android操作系统中,有一套自己的事件分发机制,所有的代码调用和回调都是按照一定顺序执行的,Hook技术存在的意义就在于,我们可以在事件传送到终点前截获并监控该事件的传输,并且做一些自己的处理,可以简单的理解为把一件事中间拦截掉了,然后搞了点自己的小动作然后让他继续走下去。

为了保证hook的稳定性,一般拦截的点都会选择比较容易找到并且不易发生变化的对象,比如静态变量和单例。

实例:Hook实现Activity插件化

tips:这一小段源码层面我们基于Android api 24,也就是7.0。8.0上启动Activity实现不同,基本原理相同,读者可以自行操作。

目前,圈内的几个插件化框架在Activity插件化问题上,主要有3种实现方式,反射、接口和Hook技术。而反射因为性能问题,接口因为效率问题,Hook技术是主流实现方式,这里我们就用hook来实现Activity插件化。

这里我们主要注意点放在hook上,所以插件化具体不展开,具体Activity插件化过程可以描述成,A Activity要跳转到一个在Manifest.xml里没有注册过的B Activity,这个过程需要在AMS校验之前把跳转Activity目标从B改成一个在Manifest.xml注册过的C Activity,然后在AMS校验之后再把C改成B,然后实现跳转逻辑。

hook第一步,首先阅读源码,寻找hook点。注意上文中提到的,为了保证hook的稳定性,一般拦截的点都会选择比较容易找到并且不易发生变化的对象,比如静态变量和单例。这里需要读者熟悉Activity启动流程,这里就不展开了,有兴趣的朋友可以先去网上搜下大概有个概念然后再阅读下去。

我们来看startActivity()方法,不断的跟踪进去,我们会发现:
在这里插入图片描述
由mInstrumentation调用了execStartActivity方法启动Activity,注意这只在AMS校验之前,也就是这里AMS还没有校验要跳转的Activity是否在Manifest.xml里注册,我们可以在这里实现把B Activity替换成C的过程。

然后会在ActivityThread中的performLaunchActivity方法里调用mInstrumentation的newActivity方法用类加载器创建Activity的实例,我们可以在这里把它替换成我们要跳转的B Activity.。

在这里插入图片描述

这里,我们就找到hook点了就是mInstrumentation,我们只要自定义一个instrumentation替换掉即可,下面贴下代码,代码中都有注释,原理懂了,代码理解起来就很方便了。

工具类:FieldUtil.java
在这里插入图片描述
自定义instruction:ProxyInstrumentation.java

在这里插入图片描述

controll操作类:HookUtil.java
在这里插入图片描述

然后Application调用下操作即可:
在这里插入图片描述

Hook技术在项目优化中的用处

Toast WindowManager$BadTokenException

tips:这一小段源码层面我们主要针对于Android7.x。

相信Android朋友们平时开发的时候应该都遇到过这个问题,这是我在做的app的线上报上来的日志:
在这里插入图片描述
ok,先来简单分析下这个问题的原因:token失效。在看Toast.java源码的过程中,我们会发现,Toast的展示并不是自己控制的,而是通过AIDL使用INotificationManager中的NotificationManagerService控制的。当要显示一个Toast的时候,NotificationManagerService会产生一个token用于校验。在WindowManager要添加这个Toast的时候会去校验这个token,如果token有效,则添加窗口,无效则报crash。

通常情况下是不会出现这个问题的,但是在某种情况下Android 进程某个 UI 线程的某个消息阻塞,导致toast.show()方法一直无法被调用,这个的同时NotificationManager的超时检测结束,删除了token,在show()方法之前,就会出现这个异常。

这个crash我在跑demo的时候的时候使用让ui线程休眠的方式在Android 7.1.1的虚拟机上没有复现出来,朋友们可以参考腾讯这篇文章Toast问题深度剖析(一),有复现出来的朋友欢迎评论区一起交流。

虽然没复现出来这个token is valid,但我在阅读Toast源代码后想到用另一种方式来复现这个BadTokenException:
在这里插入图片描述

先来看点击了按钮以后报的错误:
在这里插入图片描述
因为type==TYPE_TOAST的类型的toast不能重复添加,所以这样也会报一个BadTokenException,接下来我们就要通过这个demo,用hook的解决方案来解决这个异常。

阅读源码我们发现,在Android 7.0 Toast.java上:
在这里插入图片描述
和在Android 8.0 Toast.java上:
在这里插入图片描述
对比我们发现在Android 8.0中,在WindowManager进行addView的时候8.0进行了一层try catch保护,而在7.0上并没有。那么我们就可以参考8.0的方法,直接catch住这个异常。

然后我们就可以来找hook点了,这里Toast 里面有一个静态变量mTN,TN类中通过调用handleShow()方法来把toast添加到window上,而handleShow()是怎么调用的呢?通过一个final的Handler,所以就很简单了,我们hook点就定位这个mTN,然后反射替换TN的内部成员变量mHandler,从而添加try-catch做到保护即可。

具体代码如下:
在这里插入图片描述

TimeoutException

TimeoutException这个问题相信朋友们应该也不会陌生:
在这里插入图片描述
这个exception又是为什么会出现呢?我们先来分析下原因:
在VM GC的时候,为了减少程序的卡顿,会启动FinalizerWatchdogDaemon等四个守护线程,而FinalizerWatchdogDaemon的作用是用来监控FinalizerDaemon线程的执行的。一旦检测到执行成员函数finalize时超出一定时间,那么就会退出VM,抛出TimeoutException。

所以,如果要模拟这个问题,完美只要引用一个重写了finalize方法的实例,并且在finalize方法中有耗时操作,然后我们手动GC就可以了。关于finalizer对象对内存和性能的影响有兴趣的朋友可以去阅读下这篇[再谈Finalizer对象--大型App中内存与性能的隐性杀手](https://yq.aliyun.com/articles/225755)

ok接下来我们来寻找解决问题的点,这里我AS上阅读源码没找到Daemons这个类,然后我就在这里凑合着看了看,Daemons.java

阅读源码我们发现,可以通过反射将FinalizerWatchdogDaemon中的thread置空,这样就不会执行此线程,也就不会出现TimeoutException了

在这里插入图片描述

这个问题上,我推荐有兴趣的朋友再去看一下极客时间张绍文老师对这个问题的想法和demo,这里会出现一个问题就是在Android 6.0之前会有线程安全问题,在demo中对这个问题有全面的处理。

结语

Hook这个黑科技还是比较实用的,关键在于阅读源码,然后通过代码的依赖关系,发现一个取巧的 Hook 点。

当然,这里我建议在灰度环境经过大量测试,通过没问题以后再放到生产环境上,毕竟黑科技还是具有一定风险的。

个人微信公共账号已上线,欢迎关注:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/jieqiang3/article/details/86189077