一次Java字节码插桩实战

理解本文须要必定的Java字节码指令基础,能够阅读笔者的另外一篇文章: 大话+图说:Java字节码指令——只为让你懂
利用Android字节码插桩技术能够很方便地帮助咱们实现不少手术刀式的代码设计,如无埋点统计上报、轻量级AOP等。下面咱们就经过一次实战,把这门技术真正用起来。

奇葩需求

假设有这样一个需求,咱们须要在本项目工程的全部组件(Activity/Receiver/Service/Provider)的on系列生命周期类方法执行时,调用一个咱们写好的方法,传入组件的实例对象,来对组件的相关状态进行监测,如何实现?
通常的思路有两种:java

  1. 经过Java继承体系,为咱们实现的四大组件分别创建基类,在基类父方法里对监测方法进行调用。
  2. 经过Android API Hook技术,即经过动态代理等方法替换关键节点,抓住组件的节点方法并调用咱们的监测方法。

上面的第一种方法比较麻烦,并且控制力较弱,也没法顾及咱们所依赖的Jar或者aar中的组件,好比小米推送中自带的Service和Receiver,是彻底没法触及的。第二种方法则比较强大,可是须要考虑兼容性问题,技术实现上的成本也比较高,毕竟有一些生命周期的节点很差找,不免焦头烂额。android

本文对此的实战即经过字节码插桩,在class文件编译成dex以前(同时也是proguard操做以前),遍历全部要编译的class文件并对其中符合条件的方法进行修改,注入咱们要调用的监测方法的代码,从而实现这个需求。git

HiBeaver 是目前这方面比较完善的字节码插桩Gradle插件,目前最新的1.2.4版本支持经过通配符或正则表达式的方法来匹配目标类和目标方法,进行方法的批量插桩注入和修改,很是灵活易用。对于相似上文提出的需求,实现起来很是方便,惟一前提的仅仅是:知道全部组件的类的全名就能够了。github

准备工做

好,基于这些,正式开始实战,牛刀小试一下:
首先创建一个工程,为便于演示,咱们引入小米推送(接入方式再也不赘述,详见小米推送文档),而后完善代码到以下状态:web

图片描述

MainActivity内容很简单,注册了小米推送,有一个TextView点击后能够跳转到SecondActivity,仅此而已。具体以下:正则表达式

图片描述

SecondActivity中一切从简:segmentfault

图片描述

至于DemoMessageReceiver这个类里彻底依照小米推送接入文档中的配置,没有实质改动,再也不贴出。
注意到还有一个MonitorUtil的类,内容以下:闭包

图片描述

其中的monitorThis的方法就是咱们打算在各个生命周期方法里插入的调用方法。app

开始实战

下面咱们就开始实现开头处提到的需求:经过字节码插桩的方法,本工程里的全部组件的生命周期方法return以前调用咱们的monitorThis方法,传入组件实例等信息做为参数。ide

首先,要引入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目录下,以便于查看:

clipboard.png

以下图为修改后的MainActivity类:

clipboard.png

修改后的小米推送里的某Receiver:

clipboard.png

这样,不管是进行节点控制仍是研究其运行机制都大大地方便了。

HiBeaver
相关文章
相关标签/搜索