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

APM之原理篇的更多相关文章

  1. Android 性能监控系列一(原理篇)

    欢迎关注微信公众号:BaronTalk,获取更多精彩好文! 一. 前言 性能问题是导致 App 用户流失的罪魁祸首之一,如果用户在使用我们 App 的时候遇到诸如页面卡顿.响应速度慢.发热严重.流量电 ...

  2. Cesium原理篇:5最长的一帧之影像

    如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了.之前的几个系列,我们全面的介绍了Cesium的地形内容,详见: Cesium原理篇:1最长的一帧之渲染调度 Cesium原 ...

  3. Cesium原理篇:3最长的一帧之地形(2:高度图)

           这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面.        此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维 ...

  4. Cesium原理篇:7最长的一帧之Entity(下)

    上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSo ...

  5. Esfog_UnityShader教程_遮挡描边(原理篇)

    咳咳,有段时间没有更新了,最近有点懒!把不少精力都放在C++身上了.闲言少叙,今天要讲的可和之前的几篇有所不同了,这次是一个次综合应用.这篇内容中与之前不同主要体现在下面几点上. 1.之前我们写的都是 ...

  6. 【如何快速的开发一个完整的iOS直播app】(原理篇)

    原文转自:袁峥Seemygo    感谢分享.自我学习 目录 [如何快速的开发一个完整的iOS直播app](原理篇) [如何快速的开发一个完整的iOS直播app](播放篇) [如何快速的开发一个完整的 ...

  7. iOS:app直播---原理篇

    [如何快速的开发一个完整的iOS直播app](原理篇) 转载自简书@袁峥Seemygo:http://www.jianshu.com/p/7b2f1df74420   一.个人见解(直播难与易) 直播 ...

  8. 如何快速的开发一个完整的iOS直播app(原理篇)

    目录 [如何快速的开发一个完整的iOS直播app](原理篇) [如何快速的开发一个完整的iOS直播app](播放篇) [如何快速的开发一个完整的iOS直播app](采集篇) 前言 大半年没写博客了,但 ...

  9. Tomcat 原理篇

    TOMCAT 原理篇一.Tomcat 组成(Tomcat 由以下组件组成) 1.server a) Server是一个Catalina Servlet容器: b) Server 可以包含一个或多个se ...

随机推荐

  1. 【线性代数】7-1:线性变换思想(The Idea of a Linear Transformation)

    title: [线性代数]7-1:线性变换思想(The Idea of a Linear Transformation) categories: Mathematic Linear Algebra k ...

  2. CSPS模拟69-72

    模拟69: T1,稍数学,主要还是dp(转移莫名像背包???),当C开到n2时复杂度为n4,考场上想了半天优化结果发现n是100,n4可过 #include<iostream> #incl ...

  3. Irrlicht引擎剖析二

  4. openpyxl模块(excel操作)

    openpyxl模块介绍 openpyxl模块是一个读写Excel 2010文档的Python库,如果要处理更早格式的Excel文档,需要用到额外的库,openpyxl是一个比较综合的工具,能够同时读 ...

  5. ccf 201803-2 碰撞的小球(Python)

    问题描述 数轴上有一条长度为L(L为偶数)的线段,左端点在原点,右端点在坐标L处.有n个不计体积的小球在线段上,开始时所有的小球都处在偶数坐标上,速度方向向右,速度大小为1单位长度每秒. 当小球到达线 ...

  6. jQuery源码解读----part 2

    分离构造器 通过new操作符构建一个对象,一般经过四步: A.创建一个新对象 B.将构造函数的作用域赋给新对象(所以this就指向了这个新对象) C.执行构造函数中的代码 D.返回这个新对象 最后一点 ...

  7. 数据分析 - Excel 配色, 绘图, 技巧

    美学 配色 画图本身是美学的展示, 出色的配色是必须的 虽然本身美学并不是数据分析的必要, 但是也不能太low 如果做的太丑展示也是很尴尬 配色网站 点击这里 配置 现版本的 excel 中已存在较为 ...

  8. 阶段5 3.微服务项目【学成在线】_day03 CMS页面管理开发_08-新增页面-前端-Api调用

    表单数据提交到后台 export const page_add = paramas => { return http.requestPost(apiUrl+'/cms/page/add',par ...

  9. Freemarker使用总结

    spring mvc 中 Freemarker 获取项目根目录,方便HTML页面配置各种路径 在SpringMVC框架中使用Freemarker试图时,要获取根路径的方式如下: <!-- Fre ...

  10. Path环境变量的作用

    作用: 当我们要求系统运行一个程序(例如a.exe)而没有告诉它程序所在的完整路径时,系统会先在当前目录寻找是否存在a.exe,如果找到,直接运行:如果没有找到,会去path路径下面找.设置path, ...