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