涉及知识点:APM, java Agent, plugin, bytecode, asm, InvocationHandler, smail
一. 背景介绍
APM : 应用程序性能管理。 2011年时国外的APM行业 NewRelic 和 APPDynamics 已经在该领域拔得头筹,国内近些年来也出现一些APM厂商,如: 听云, OneAPM, 博睿(bonree) 云智慧,阿里百川码力。 (据分析,国内android端方案都是抄袭NewRelic公司的,由于该公司的sdk未混淆,业界良心)
能做什么: crash监控,卡顿监控,内存监控,增加trace,网络性能监控,app页面自动埋点,等。
二. 方案介绍
性能监控其实就是hook 代码到项目代码中,从而做到各种监控。常规手段都是在项目中增加代码,但如何做到非侵入式的,即一个sdk即可。
1. 如何hook
切面编程-- AOP。
我们的方案是AOP的一种,通过修改app class字节码的形式将我们项目的class文件进行修改,从而做到嵌入我们的监控代码。
![](https://i-blog.csdnimg.cn/blog_migrate/64419ea39ec67785b5d1b2128ec2c203.webp?x-image-process=image/format,png)
通过查看Adnroid编译流程图,可以知道编译器会将所有class文件打包称dex文件,最终打包成apk。那么我们就需要在class编译成dex文件的时候进行代码注入。比如我想统计某个方法的执行时间,那我只需要在每个调用了这个方法的代码前后都加一个时间统计就可以了。关键点就在于编译dex文件时候注入代码,这个编译过程是由dx执行,具体类和方法为com.android.dx.command.dexer.Main#processClass
。此方法的第二个参数就是class的byte数组,于是我们只需要在进入processClass方法的时候用ASM工具对class进行改造并替换掉第二个参数,最后生成的apk就是我们改造过后的了。
类:com.android.dx.command.dexer.Main
新的难点: 要让jvm在执行processClass之前先执行我们的代码,必须要对com.android.dx.command.dexer.Main(以下简称为dexer.Main)进行改造。如何才能达到这个目的?这时Instrumentation和VirtualMachine就登场了,参考第三节。
2. hook 到哪里
一期主要是网络性能监控。如何能截获到网络数据
通过调研发现目前有下面集中方案:
- root手机,通过adb 命令进行截获。
- 建立vpn,将所有网络请求进行截获。
-
参考听云,newrelic等产品,针对特定库进行代理截获。
也许还有其他的方式,需要继续调研。
目前我们参考newrelic等公司产品,针对特定网络请求库进行代理的的方式进行网络数据截获。比如okhtt3, httpclient, 等网络库。
三. Java Agent
In general, a javaagent is a JVM “plugin”, a specially crafted .jar file, that utilizes the Instrumentation API that the JVM provides.
http://www.infoq.com/cn/articles/javaagent-illustrated/
由于我们要修改Dexer 的Main类, 而该类是在编译时期由java虚拟机启动的, 所以我们需要通过agent来修改dexer Main类。
javaagent的主要功能如下:
- 可以在加载class文件之前作拦截,对字节码做修改
- 可以在运行期对已加载类的字节码做变化
JVMTI:JVM Tool Interface,是JVM暴露出来的一些供用户扩展的接口集合。JVMTI是基于事件驱动的,JVM每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。
instrument agent: javaagent功能就是它来实现的,另外instrument agent还有个别名叫JPLISAgent(Java Programming Language Instrumentation Services Agent),这个名字也完全体现了其最本质的功能:就是专门为Java语言编写的插桩服务提供支持的。
两种加载agent的方式:
- 在启动时加载, 启动JVM时指定agent类。这种方式,Instrumentation的实例通过agent class的premain方法被传入。
- 在运行时加载,JVM提供一种当JVM启动完成后开启agent机制。这种情况下,Instrumention实例通过agent代码中的的agentmain传入。
参考例子instrumentation 功能介绍(javaagent)
有了javaagent, 我们就可以在编译app时重新修改dex 的Main类,对应修改processClass方法。
4. Java Bytecode
如何修改class文件? 我们需要了解java字节码,然后需要了解ASM开发。通过ASM编程来修改字节码,从而修改class文件。(也可以使用javaassist来进行修改)
在介绍字节代码指令之前,有必要先来介绍 Java 虚拟机执行模型。我们知道,Java 代码是 在线程内部执行的。每个线程都有自己的执行栈,栈由帧组成。每个帧表示一个方法调用:每次 调用一个方法时,会将一个新帧压入当前线程的执行栈。当方法返回时,或者是正常返回,或者 是因为异常返回,会将这个帧从执行栈中弹出,执行过程在发出调用的方法中继续进行(这个方 法的帧现在位于栈的顶端)。
每一帧包括两部分:一个局部变量部分和一个操作数栈部分。局部变量部分包含可根据索引 以随机顺序访问的变量。由名字可以看出,操作数栈部分是一个栈,其中包含了供字节代码指令 用作操作数的值。
字节代码指令
字节代码指令由一个标识该指令的操作码和固定数目的参数组成:
- 操作码是一个无符号字节值——即字节代码名
- 参数是静态值,确定了精确的指令行为。它们紧跟在操作码之后给出.比如GOTO标记 指令(其操作码的值为 167)以一个指明下一条待执行指令的标记作为参数标记。不要 将指令参数与指令操作数相混淆:参数值是静态已知的,存储在编译后的代码中,而 操作数值来自操作数栈,只有到运行时才能知道。
参考: https://en.wikipedia.org/wiki/Java_bytecode_instruction_listings
常见指令:
- const 将什么数据类型压入操作数栈。
- push 表示将单字节或短整型的常量压入操作数栈。
- ldc 表示将什么类型的数据从常量池中压入操作数栈。
- load 将某类型的局部变量数据压入操作数栈顶。
- store 将操作数栈顶的数据存入指定的局部变量中。
- pop 从操作数栈顶弹出数据
- dup 复制栈顶的数据并将复制的值也压入栈顶。
- swap 互换栈顶的数据
- invokeVirtual 调用实例方法
- invokeSepcial 调用超类构造方法,实例初始化,私有方法等。
- invokeStatic 调用静态方法
- invokeInterface 调用接口
- getStatic
- getField
- putStatic
- putField
- New
查看demo:
Java源代码
public static void print(String param) {
System.out.println("hello " + param);
new TestMain().sayHello();
}
public void sayHello() {
System.out.println("hello agent");
}
字节码
// access flags 0x9
public static print(Ljava/lang/String;)V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "hello "
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
ALOAD 0
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
NEW com/paic/agent/test/TestMain
DUP
INVOKESPECIAL com/paic/agent/test/TestMain.<init> ()V
INVOKEVIRTUAL com/paic/agent/test/TestMain.sayHello ()V
RETURN
public sayHello()V
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
LDC "hello agent"
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V RETURN
5. ASM 开发
由于程序分析、生成和转换技术的用途众多,所以人们针对许多语言实现了许多用于分析、 生成和转换程序的工具,这些语言中就包括 Java 在内。ASM 就是为 Java 语言设计的工具之一, 用于进行运行时(也是脱机的)类生成与转换。于是,人们设计了 ASM1库,用于处理经过编译 的 Java 类。
ASM 并不是惟一可生成和转换已编译 Java 类的工具,但它是最新、最高效的工具之一,可 从 http://asm.objectweb.org 下载。其主要优点如下:
- 有一个简单的模块API,设计完善、使用方便。
- 文档齐全,拥有一个相关的Eclipse插件。
- 支持最新的 Java 版本——Java 7。
- 小而快、非常可靠。
- 拥有庞大的用户社区,可以为新用户