APM之原理篇
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之原理篇的更多相关文章
- Android 性能监控系列一(原理篇)
欢迎关注微信公众号:BaronTalk,获取更多精彩好文! 一. 前言 性能问题是导致 App 用户流失的罪魁祸首之一,如果用户在使用我们 App 的时候遇到诸如页面卡顿.响应速度慢.发热严重.流量电 ...
- Cesium原理篇:5最长的一帧之影像
如果把地球比做一个人,地形就相当于这个人的骨骼,而影像就相当于这个人的外表了.之前的几个系列,我们全面的介绍了Cesium的地形内容,详见: Cesium原理篇:1最长的一帧之渲染调度 Cesium原 ...
- Cesium原理篇:3最长的一帧之地形(2:高度图)
这一篇,接着上一篇,内容集中在高度图方式构建地球网格的细节方面. 此时,Globe对每一个切片(GlobeSurfaceTile)创建对应的TileTerrain类,用来维 ...
- Cesium原理篇:7最长的一帧之Entity(下)
上一篇,我们介绍了当我们添加一个Entity时,通过Graphics封装其对应参数,通过EntityCollection.Add方法,将EntityCollection的Entity传递到DataSo ...
- Esfog_UnityShader教程_遮挡描边(原理篇)
咳咳,有段时间没有更新了,最近有点懒!把不少精力都放在C++身上了.闲言少叙,今天要讲的可和之前的几篇有所不同了,这次是一个次综合应用.这篇内容中与之前不同主要体现在下面几点上. 1.之前我们写的都是 ...
- 【如何快速的开发一个完整的iOS直播app】(原理篇)
原文转自:袁峥Seemygo 感谢分享.自我学习 目录 [如何快速的开发一个完整的iOS直播app](原理篇) [如何快速的开发一个完整的iOS直播app](播放篇) [如何快速的开发一个完整的 ...
- iOS:app直播---原理篇
[如何快速的开发一个完整的iOS直播app](原理篇) 转载自简书@袁峥Seemygo:http://www.jianshu.com/p/7b2f1df74420 一.个人见解(直播难与易) 直播 ...
- 如何快速的开发一个完整的iOS直播app(原理篇)
目录 [如何快速的开发一个完整的iOS直播app](原理篇) [如何快速的开发一个完整的iOS直播app](播放篇) [如何快速的开发一个完整的iOS直播app](采集篇) 前言 大半年没写博客了,但 ...
- Tomcat 原理篇
TOMCAT 原理篇一.Tomcat 组成(Tomcat 由以下组件组成) 1.server a) Server是一个Catalina Servlet容器: b) Server 可以包含一个或多个se ...
随机推荐
- python+opencv 图像预处理
一 python 生成随机字符串序列+ 写入到图片上 from PIL import Image,ImageDraw,ImageFont import numpy as np import rando ...
- vue中插槽(slot)的使用
刚学vue的时候,曾经学习过slot插槽的使用,但是后面接触的不多,因为之前我还没使用element-ui... 但是使用了element-ui之后,里面的许多组件,有时候会使用插槽,为了巩固一下插槽 ...
- 7.26T3小游戏
小游戏 题目描述 有一个简单的小游戏.游戏的主人公是一个勇士,他要穿过一片黑森林,去 解救公主.黑森林的地图可以用一张 V 个点.E 条边的无向图来描述,起点是 1 号点,终点是 V 号点.勇士从起点 ...
- 常使用的VIM命令及文件颜色代表含义
编辑模式--->输入模式 i : insert 在光标所在处输入: a:append 在光标所在处后面输入: o:在当前光标所在行的下方打开一个新行: I:在当前光标所在行的行首输入: A:在当 ...
- jquery 页面input上传图后显示
<input type="file" id="otherfiles" name="otherfiles" class="up ...
- CentOS6.8上Docker配置阿里云镜像加速器
1.打开网站https://dev.aliyun.com,点击管理中心,登录阿里云账号(没有的可以注册,也可以用淘宝等第三方账号登录). 2.点击镜像加速器,复制加速器地址 3.配置本机Docker运 ...
- 如何使用纯js实现一个带有灰色半透明背景的弹出框
原文如何使用纯js实现一个带有灰色半透明背景的弹出框 // 加入透明背景 var body = document.body;var backgroundDiv = document.createEle ...
- Python3中_和__的用途和区别
访问可见性问题 对于上面的代码,有C++.Java.C#等编程经验的程序员可能会问,我们给Student对象绑定的name和age属性到底具有怎样的访问权限(也称为可见性).因为在很多面向对象编程语言 ...
- delphi 中如何从数据库中读取数据自生成TreeView,只有两个字段,数据库结构如下。急急!!
我的数据库结构如下:UnitId UnitName01 中国 (根节点)0101 河北省(二级树)010101 河北省沧州市(三级树)0101010 ...
- Linux 常用高效操作
空行处理 linux系统下删除空行,用vim底行模式'%s/^n$//g' 可以删除空行并真正修改文件,但文件数量太大时耗时不可预估,于是操作文件删除空行并重定向到一个新的文件是不错的选择. 常用特殊 ...