理解本文需要一定的 Java 汇编指令基础,可以阅读笔者的另一篇文章:
利用 Android 字节码插桩技术可以很方便地帮助我们实现很多手术刀式的代码设计,如无埋点统计上报、轻量级 AOP 等。下面我们就通过一次实战,把这门技术真正用起来。
假设有这样一个需求,我们需要在本项目工程的所有组件(Activity/Receiver/Service/Provider)的 on 系列生命周期类方法执行时,调用一个我们写好的方法,传入组件的实例对象,来对组件的相关状态进行监测,如何实现?
一般的思路有两种:
上面的第一种方法比较麻烦,而且控制力较弱,也无法顾及我们所依赖的 Jar 或者 aar 中的组件,比如小米推送中自带的 Service 和 Receiver,是完全无法触及的。第二种方法则比较强大,但是需要考虑兼容性问题,技术实现上的成本也比较高,毕竟有一些生命周期的节点不好找,难免焦头烂额。
本文对此的实战即通过字节码插桩,在 class 文件编译成 dex 之前(同时也是 proguard 操作之前),遍历所有要编译的 class 文件并对其中符合条件的方法进行修改,注入我们要调用的监测方法的代码,从而实现这个需求。
是目前这方面比较完善的字节码插桩 Gradle 插件,目前最新的 1.2.4 版本支持通过通配符或正则表达式的方法来匹配目标类和目标方法,进行方法的批量插桩注入和修改,非常灵活易用。对于类似上文提出的需求,实现起来非常方便,唯一前提的仅仅是:知道所有组件的类的全名就可以了。
好,基于这些,正式开始实战,牛刀小试一下:
首先建立一个工程,为便于演示,我们引入小米推送(接入方式不再赘述,详见 ),然后完善代码到如下状态:
MainActivity 内容很简单,注册了小米推送,有一个 TextView 点击后可以跳转到 SecondActivity,仅此而已。具体如下:
SecondActivity 中一切从简:
至于 DemoMessageReceiver 这个类里完全依照小米推送接入文档中的配置,没有实质改动,不再贴出。
注意到还有一个 MonitorUtil 的类,内容如下:
其中的 monitorThis 的方法就是我们打算在各个生命周期方法里插入的调用方法。
下面我们就开始实现开头处提到的需求:通过字节码插桩的方法,本工程里的所有组件的生命周期方法 return 之前调用我们的 monitorThis 方法,传入组件实例等信息作为参数。
首先,要引入 插件:
然后在项目的根 build.gradle 下面增加 classpath 如下:
- classpath 'com.bryansharp:hibeaver:1.2.4'
随后为我们工程的 app/build.gradle 增加如下配置:
- apply plugin: 'hiBeaver'
- import com.bryansharp.gradle.hibeaver.utils.MethodLogAdapter
- import org.objectweb.asm.ClassVisitor
- import org.objectweb.asm.MethodVisitor
- import org.objectweb.asm.Opcodes
- hiBeaver {
- modifyMatchMaps = [
- //类名称匹配规则,*表示任意长度任意字符,|为分隔符,可以理解为或
- '*Activity|*Receiver|*Service|!android*': [
- //方法名匹配规则与类名类似,同时也支持正则表达式匹配(需要加r:);adapter后为一个闭包,进行具体的修改
- ['methodName': 'on**', 'methodDesc': null, 'adapter': {
- //下面这些为闭包传入的参数,可以帮助我们进行方法过滤,以及根据方法参数来调整字节码修改方式
- ClassVisitor cv, int access, String name, String desc, String signature, String[] exceptions ->
- //这里我们有了ClassVisitor实例,其实可以为类添加新的方法。
- MethodVisitor methodVisitor = cv.visitMethod(access, name, desc, signature, exceptions);
- MethodVisitor adapter = new MethodLogAdapter(methodVisitor) {
- @Override
- void visitCode() {
- super.visitCode();
- //实例对象入栈
- methodVisitor.visitVarInsn(Opcodes.ALOAD, 0);
- //下面两句我们将方法的名称和描述作为常量入栈
- methodVisitor.visitLdcInsn(name);
- methodVisitor.visitLdcInsn(desc);
- //调用我们的静态方法
- methodVisitor.visitMethodInsn(Opcodes.INVOKESTATIC,
- //下面这个MethodLogAdapter.className2Path(String)为
- // hibeaver插件提供的方法,可以将类名转为路径名
- MethodLogAdapter.className2Path("bruce.com.testhibeaver.MonitorUtil"),
- "monitorThis", "(Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;)V");
- }
- }
- return adapter;
- }]
- ]
- ]
- }
在类名和方法名的匹配上非常灵活,可以非常方便地实现批量匹配,除了完整匹配外,还支持通配符匹配和正则表达式匹配两种模式。通配符匹配模式中主要可以使用两种符号,即 | 和 表示任意长度(>0)的任意字符,而 | 表示分隔符,这里可以理解为或。因此,上面的:
- *Activity|*Receiver|*Service
可以理解为,匹配任意全类名以 Activity、Receiver 或 Service 结尾的类。
一般来讲,我们的 Android 组件在命名上都会遵从这个规范,即组件类名以相应的组件名结尾,对于个别不遵从这个原则的,也可以通过 | 分隔符来把特殊情况纳入进去。
除此之外,如果存在更复杂的匹配规则,上述通配符已经无法满足,hiBeaver 也支持正则表达式进行全类名匹配,只需要在表达式前加上 "r:" 就可以。比如:
- r:.*D[a-zA-Z]*Client
表示匹配符合 ".*D[a-zA-Z]*Client" 这个正则表达式的类名。
更进一步地, 未来 还将支持根据类的继承关系进行匹配,比如:
- >ext>android.support.v4.app.FragmentActivity
表示匹配所有继承 android.support.v4.app.FragmentActivity 的类,而:
- >imp>android.os.Handler.Callback
表示匹配所有实现 android.os.Handler.Callback 接口的类。
不过,目前这两个特性还没有支持,仅提上了其项目的 中。
回到刚刚的配置中,下面的 methodName 方法的匹配规则与类名匹配用法一样,** 和 * 是一样的效果,on** 即表示名字以 on 开头的方法。
好了,编译运行工程,过程中在 Gradle Console 中可以看到 hibeaver 进行字节码插桩输出如下(局部):
程序运行起来,插桩成功,成功调用了 monitorThis 方法,但赫然发现输出如下:
调用了三个 onCreate 和若干的 onCreateView!这是为什么?我们的 MainActivity 也没有这个 onCreateView 的方法啊!
结合之前 Gradle 编译日志,在仔细一琢磨,突然明白了:
原来,我们的 * Activity 规则会匹配所有的 Activity 结尾的类,包括一些 android v4 支持包中的类,什么 AppCompatActivity、FragmentActivity 等继承链上的 Activity 通通被 hook 了一遍,难怪会有那么多输出了,可辛苦了我们的 monitorThis 方法。
既然如此,如何是好?针对于当前的需求,我们当然不想匹配 v4 包里的组件类。
所幸的是, 中还有另一种排除匹配,运用! 符号改造如下即可:
- *Activity|*Receiver|*Service|!android*
这样就表示,匹配前三种之一(或的关系)且不匹配第四个 android * 的全类名。
改好后,再次运行,并点击跳转到 SecondActivity:
可以看到 log 输出一下子少多了,证明没有再注入 v4 包里的类,同时,小米的组件也被正常注入了,我把网断掉,可以看到小米的 Receiver 被唤起:
再开启调试,打开网,断点也可以正常进入:
同时,每次 进行字节码插桩后还会把修改过、实际使用的字节码保存到 build/HiBeaver 目录下,以便于查看:
如下图为修改后的 MainActivity 类:
修改后的小米推送里的某 Receiver:
这样,无论是进行节点控制还是研究其运行机制都大大地方便了。
来源: http://www.tuicool.com/articles/7ny2Yj6