APM,应用性能监控,有new relic等产品,对APM感兴趣的应该不会不知道它了。主要功能就是统计分析应用的CPU、内存、网络、数据库、UI等性能,并提供错误日志捕获。编码人员需要做的仅仅是使用它提供的插件和jar包,增加一两行代码即可。接下来,本文会以android端的APM为例,分析它到底是用什么技术实现的,涉及到具体相关业务的,只会简单介绍,不作深入分析。

  • ASM 
    ASM是一个字节码操作工具,可以用来改造class。使用方法和介绍可参考官方文档或者AOP 的利器:ASM 3.0 介绍。想要掌握ASM工具的使用,必须对JVM有一定的了解,特别是字节码、操作数栈等相关知识。另外,ASM还提供了一个eclipse插件Bytecode Outline plugin,用来查看字节码。对字节码不熟悉的可以先敲java代码,再用此插件查看对应的字节码。
  • dex和processClass方法 
    既然使用起来很简单,它背后肯定已经帮我们做了很多事了。实际上,APM的插件会在将class编译成dex文件的时候注入相关的代码。比如我想统计某个方法的执行时间,那我只需要在每个调用了这个方法的代码前后都加一个时间统计就可以了。关键点就在于在编译dex文件的时候注入代码。这个编译的过程是由dx(dx.bat)执行的,具体的类和方法是com.android.dx.command.dexer.Main#processClass。此方法的第二个参数就是class的byte数组,于是我们只需要在进入processClass方法的时候用ASM工具对class进行改造并替换掉第二个参数,最后生成的apk就是我们改造过后的了。现在新的难点来了,要让jvm在执行processClass之前先执行我们的代码,必须要对com.android.dx.command.dexer.Main(以下简称为dexer.Main)进行改造。如何才能达到这个目的?这时Instrumentation和VirtualMachine就登场了。
  • Instrumentation和VirtualMachine 
    VirtualMachine有个loadAgent方法,它指定的agent会在main方法前启动,并调用agent的agentMain方法,agentMain的第二个参数是Instrumentation,这样我们就能够给Instrumentation设置ClassFileTransformer来实现对dexer.Main的改造,同样也可以用ASM来实现。一般来说,APM工具包括三个部分,plugin、agent和具体的业务jar包。这个agent就是我们说的由VirtualMachine启动的代理。而plugin要做的事情就是调用loadAgent方法。对于Android Studio而言,plugin就是一个Gradle插件。 
    实现gradle插件可以用intellij创建一个gradle工程并实现Plugin< Project >接口,然后把tools.jar(在jdk的lib目录下)和agent.jar加入到Libraries中。在META-INF/gradle-plugins目录下创建一个properties文件,并在文件中加入一行内容“implementation-class=插件类的全限定名“。artifacs配置把源码和META-INF加上,但不能加tools.jar和agent.jar。 
    agent的实现相对plugin则复杂很多,首先需要提供agentmain(String args, Instrumentation inst)方法,并给Instrumentation设置ClassFileTransformer,然后在transformer里改造dexer.Main。当jvm成功执行到我们设置的transformer时,就会发现传进来的class根本就没有dexer.Main。坑爹呢这是。。。前面提到了,执行dexer.Main的是dx.bat,也就是说,它和plugin根本不在一个进程里。
  • ProcessBuilder 
    dx.bat其实是由ProcessBuilder的start方法启动的,ProcessBuilder有一个command成员,保存的是启动目标进程携带的参数,只要我们给dx.bat带上-javaagent参数就能给dx.bat所在进程指定我们的agent了。于是我们可以在执行start方法前,调用command方法获取command,并往其中插入-javaagent参数。参数的值是agent.jar所在的路径,可以使用agent.jar其中一个class类实例的getProtectionDomain().getCodeSource().getLocation().toURI().getPath()获得。可是到了这里我们的程序可能还是无法正确改造class。如果我们把改造类的代码单独放到一个类中,然后用ASM生成字节码调用这个类的方法来对command参数进行修改,就会发现抛出了ClassDefNotFoundError错误。这里涉及到了ClassLoader的知识。
  • ClassLoader和InvocationHandler 
    关于ClassLoader的介绍很多,这里不再赘述。ProcessBuilder类是由Bootstrap ClassLoader加载的,而我们自定义的类则是由AppClassLoader加载的。Bootstrap ClassLoader处于AppClassLoader的上层,我们知道,上层类加载器所加载的类是无法直接引用下层类加载器所加载的类的。但如果下层类加载器加载的类实现或继承了上层类加载器加载的类或接口,上层类加载器加载的类获取到下层类加载的类的实例就可以将其强制转型为父类,并调用父类的方法。这个上层类加载器加载的接口,部分APM使用InvocationHandler。还有一个问题,ProcessBuilder怎么才能获取到InvocationHandler子类的实例呢?有一个比较巧妙的做法,在agent启动的时候,创建InvocationHandler实例,并把它赋值给Logger的treeLock成员。treeLock是一个Object对象,并且只是用来加锁的,没有别的用途。但treeLock是一个final成员,所以记得要修改其修饰,去掉final。Logger同样也是由Bootstrap ClassLoader加载,这样ProcessBuilder就能通过反射的方式来获取InvocationHandler实例了。
  • APM业务功能实现 
    这点不细说,具体得跟实际需求结合。总的来说可能也就三种模式:一是方法替换,二是实例替换,三是重写。 
    举例来说,假设需要获取网络性能,那可以将每个调用HttpURLConnection相关方法的代码替换掉,改成调用自己方法。如果被替换方法不是静态的,这要求目标方法的首个参数必须是HttpURLConnection实例,后面的参数与原方法一致。这个顺序是jvm操作数栈决定的。如果被替换方法是构造方法,则要求目标方法的返回值是原方法对应的实例。 
    实例替换,替换调用某个方法后返回的对象实例,替换的后的实例对象是继承自源对象的。要求我们的方法入参和返回值类型都是源方法的返回值类型,在调用某个方法的后面加上我们的代码即可。这种可以说是方法替换的升级版,可以在我们的对象重写源对象的所有方法。 
    重写不是Override。对于Activity或者AsyncTask这类其方法是由系统回调的,我们无法通过以上两种方式来改造,只能在进入方法后或退出方法前加入我们自己的代码。如ASM的onMethodEnter和onMethodExit方法,即可实现此类需求。
  • 提示 
    由于调试基本不太可能,所以最好加入日志来跟踪,而且要有写入文件的日志,执行到dx.bat后,输出到控制台的日志是看不到的。为了方便在控制台日志和文件日志之间切换,可以通过java -D参数来设置System的properties,然后在agent中通过System.getProperties来获取。具体到Android Studio,可以修改gradlew(gradlew.bat),在”set DEFAULT_JVM_OPTS=“一行后面加上”-Dxxx=xxx”,然后通过gradlew clean或者gradlew build来进行编译。除此之外,还可以在gradlew后面加入–stacktrace等其他的参数来跟踪编译执行的异常堆栈或调试信息。
  • 欢迎关注微信公众号:shoshana
05-28 08:05