本文示例代码:AndroidSimpleHook-github
背景
booster
是一款专门为移动应用设计的易用、轻量级且可扩展的质量优化框架,其目标主要是为了解决随着 APP 复杂度的提升而带来的性能、稳定性、包体积等一系列质量问题。
booster
项目地址:didi/booster: 🚀Optimizer for mobile applications (github.com)
booster
项目提供了非常多的拿来即用的好用的工具,包括通用 api
封装、性能优化插件、包体积插件、系统 bug 修复插件等等,具体详情可以参考项目 wiki
。 而我们今天,就将会使用过 booster
封装的 ASM api
来二次封装个简单的 hook
框架。
无处不在的 hook 需求
不管项目大小,总归有一些通过 hook
技术来进行切面编程的需求,比如:
- 检查或者禁用掉项目对于隐私敏感
API
的调用 - 特殊日子全局置灰,
Activity
可以通过ActivityLifecycleCallbacks
切入,但是像Dialog
、PopupWindow
散落各地且没法简单同意注入的就可以简单的hook
一下关键方法就行 - 想让项目中的
logcat
在线上不打印,也可以通过hook android.util.Log
来实现 so
文件不内置且需要动态下发,就可以通过hook System.load
进入到自定义的下载、检查、加载逻辑
当然,上面提到的这些只是一些比较基础的 hook
需求举例,甚至部分可能不通过 hook
也可以做到,这里只是作为需求引入。
好了,那么当我们有了 hook
的需求后,就要去想法子进行实现了,这很容易让人想到 Transform API
,gradle
比较远古的版本就支持了 Transform API
,允许第三方插件在将已编译的 class
文件转换为 dex
文件之前对其进行操作。所以我们今天的 hook 框架就用 Transform API
来实现。
那就会有同学问,你为啥不自己写个 Transform 插件,而要用 booster 呢?答案很简单,booster 封装过一层 API 后,使用起来更简单哈,「懒」
。
怎样进行 hook 更加简单
接下来,我们可以开始考虑考虑,我们的 hook
工具需要变成什么样子才会使用起来更加简单,结论显而易见:敲最少次数的键盘 + 容易阅读,那就是所谓的配置化了,配置化的优点有以下:
- 添加一个
hook
需求只需要做个简单配置 - 能一眼看全配置了哪些
hook
条目
给大家举个例子,当大家在项目中想要 hook
所有的Dialog#show()
方法,在弹任何 Dialog
的时候,全局进行计数,如果只需要写下面这些代码,是不是非常简单:
1 | ini复制代码HookClass( |
我们上面只干了以下几个事情:
- 指定要
hook
的类和方法 - 实现
hook
后的逻辑及指定hook
后的替换类和方法
这样大家就可以开心的进行想要的 hook
,而不需要思考如何去实现 hook
过程。
配置化的 hook 我们希望指定哪些条目可以配置
接下来我们可以开脑洞想想,我们在配置 hook
项时需要有哪些配置,其实大体可以分为两部分:
- 指定
hook
处的配置项 - 排除不想
hook
处的filter
那么我们可以大概列出来:
needHookClass
- 需要hook
的class
afterHookClass
-hook
后逻辑实现的class
hookMethods
- 需要被hook
的方法们
methodName
- 方法名methodDesc
- 方法描述,包括参数类型、返回值类型,比如(Ljava/lang/String;)Ljava/lang/String;
就表明入参是string
类型,返回值也是string
类型methodFilter
- 提供一个自定义的过滤器
doNotHookPackages
- 哪些包下面的被hook
类的指定方法调用不进行hook
,当然也可以实现成类似filter
的东西
当然,如果要做的更加完善,肯定还会有其他更多的配置项,我这里做演示只加这些哈
如何描述 hook 配置
一般来说,在 Android 项目中经常能见到的描述配置化的东西有以下几种方式:
- 类似
build.gradle
中的DSL
方案
groovy dsl
kotlin dsl
XML
配置解析json
配置文件- 其他
在这篇文章里,我们将一个不采用,为啥呢?嫌麻烦,由于 kotlin
支持命名参数,我们直接把配置写在 kt
代码里,key
用命名参数描述就行了,虽然用 dsl
方案会更加优雅。
那我们将这样描述我们的 hook
配置:
1 | ini复制代码//用 kotlin 的命名参数来描述 key-value,先突出一个格式整齐 |
上述这份配置表里将会涉及以下几个 class
文件。一个是 HookConfig
,就是我们所谓的配置,HookClass
是我们需要 hook
的目标类,HookMethod
是我们需要 hook
的目标方法。
1 | kotlin复制代码class HookConfig(val hookClassList: List<HookClass>) |
如何实现配置下就能完成 hook
我们参考How to Create Customized Transformer · didi/booster Wiki (github.com) 来自定义 Transformer
,具体实现步骤为:
- 自定义一个
HookTransformer
来实现ClassTransformer
接口,同时加上注解@AutoService(ClassTransformer::class)
- 将
HookTransformer
放到buildSrc
包中 - 没了。。
在我们实际的项目中,我们可能大部分的 hook
都是在 hook
某个对象方法或者静态方法,所以我们这篇文章里 只介绍针对对象方法、静态方法的 hook ,而且这两种场景也是最容易实现的,因此作为演示内容。
大家可以想一想,当我们要 hook
某个方法并且转到另一个实现方法时,怎么做会更简单呢?假如是一个对象方法:
1 | ini复制代码Dialog dialog = new Dialog(context); |
我们要 hook
它的话,有两种方式:
1. 继承 Dialog
重写 show
方法,同时将对象构造给替换掉,即:
1 | scala复制代码Dialog dialog = new DialogTest(context); |
2. 替换 show 方法
替换掉 show
方法,转成静态方法实现,即:
1 | java复制代码Dialog dialog = new Dialog(context); |
出于以下两点考虑,本文将采用转静态方法的 hook 方式来进行 hook:
- 实现简单,只需两步:
- 修改
opcode
为Opcodes.INVOKESTATIC
- 如果是对象方法,在方法的参数列表中加上
hookClass
本身,就可以将调用对象加在参数里进行传递
- 对象方法和静态方法的
hook
实现逻辑几乎没有区别
好,那就开整。
我们先描述一下我们的 hook 实现大体流程:
- 针对每个
class
文件,遍历class
的每个method
- 针对每个
method
,遍历操作指令,找出所有的方法指令 - 针对这些方法指令进行我们的
hook
过滤
owner
是我们的hookClass
owner
不能是我们的afterHookClass
owner
不能在我们的doNotHookPackages
里opcode
必须是INVOKESTATIC、INVOKEVIRTUAL、INVOKEINTERFACE
,opcode 参考methodName
匹配methodDesc
匹配methodFilter
不能过滤
- 针对每个需要
hook
的method
进行处理
opcode
统一替换成INVOKESTATIC
owner
统一替换成afterHookClass
- 如果是对象方法,在
desc
上,加上该对象参数
具体代码如下:
1 | kotlin复制代码//1. build.gradle 中 implementation 下 booster-api 的依赖 |
写在最后
至此,我们实现了一个「实现容易,使用容易」
的 hook 框架,想要 hook
一个方法仅需要如下两步:
1 | ini复制代码1. 在指定配置文件里加上 hook 配置 |
当然,由于是基础版本,实际上并没有实现的非常完善,比如:
- 未实现构造函数的
hook
- 只能
hook
项目代码,无法hook Framework
代码 - 无法
hook native
代码
但是,这并不影响这个 hook
框架非常好用哈。
展望
后续我会基于本文中描述的 hook 框架:
- 进行功能扩展,支持 hook 构造方法
- 分享相关的使用案例,如 so 动态下载、全局置灰等,当支持 hook 构造方法后,就可以用来做线程优化了。
- 其他…
你可能感兴趣
Android QUIC 实践 - 基于 OKHttp 扩展出 Cronet 拦截器 - 掘金 (juejin.cn)
Android启动优化实践 - 秒开率从17%提升至75% - 掘金 (juejin.cn)
如何科学的进行Android包体积优化 - 掘金 (juejin.cn)
Android稳定性:Looper兜底框架实现线上容灾(二) - 掘金 (juejin.cn)
基于 Booster ASM API的配置化 hook 方案封装 - 掘金 (juejin.cn)
记 AndroidStudio Tracer工具导致的编译失败 - 掘金 (juejin.cn)
Android 启动优化案例-WebView非预期初始化排查 - 掘金 (juejin.cn)
chromium-net - 跟随 Cronet 的脚步探索大致流程(1) - 掘金 (juejin.cn)
Android稳定性:可远程配置化的Looper兜底框架 - 掘金 (juejin.cn)
一类有趣的无限缓存OOM现象 - 掘金 (juejin.cn)
Android - 一种新奇的冷启动速度优化思路(Fragment极度懒加载 + Layout子线程预加载) - 掘金 (juejin.cn)
Android - 彻底消灭OOM的实战经验分享(千分之1.5 -> 万分之0.2) - 掘金 (juejin.cn)
本文转载自: 掘金