网易NAPM Andorid SDK实现原理--转
原文地址:https://neyoufan.github.io/2017/03/10/android/NAPM%20Android%20SDK/ NAPM 是网易的应用性能管理平台,采用非侵入的方式获取应用性能数据,可以实时展示多个维度的分析结果。本文主要给大家分享一下Android端SDK的实现原理。 APM(Application Performance Management),应用性能管理,主要是为了解决应用上线之后,性能问题难以发现、难以定位的问题,通过接入APM,可以实时了解应用在运行过程中的性能表现,快速定位和修复问题。 目前国内外有不少的应用性能管理平台,例如国外的 New Relic、AppDynamics,国内的听云、OneAPM,国内各大公司也都有自己的性能监控体系。 我们也开发了自己的平台?NAPM?供公司内部的产品使用,移动端目前主要采集了网络性能、交互性能和数据(数据库、JSON、Image)处理性能数据,网络性能目前主要采集了Http请求过程中的一些性能指标,比如响应时间、首包时间、DNS时间等,同时再结合机型、版本、地理位置、运营商、网络环境等多个维度,就可以使用户方便地了解应用在各种状态下的性能表现,从而及时发现问题,做出适当的调整,达到优化用户体验的目的。 下图是NAPM平台某个应用的多维分析展示界面 接下来主要给大家分享一下网易NAPM Android端SDK的实现原理。 简单来说,一个APM平台的工作流程大致如下:在各端(移动端、前端、后端)采集性能数据,然后上传到后端进行建模、存储,由平台进行分析、挖掘,最后通过可视化的方式展示给用户。 移动端SDK实际上只是一个数据采集系统,负责收集并上传终端上产生的性能数据,大致可以划分为三个模块,最底层是数据采集模块,负责采集各种性能数据,采集到的数据经过简单的处理之后存储在内存或者数据库中,最上层是数据的消费模块,通常会将采集到的数据上传到后台,供平台存储、分析和展示,同时我们也支持将采集到的性能数据交给用户处理,方便用户挖掘有用信息。 这里我们使用到了数据库,主要是因为存在一些情况,会导致采集到的数据不能实时发送至后台
因此我们需要将数据进行存储,在合适的时机上传到后台,尽量保证数据的完整。 APM SDK的难点是数据的采集,手动埋点的方式无疑是行不通的,一方面代价太大且容易产生错误,另一方面对于没有源代码的第三方库我们无法直接修改,因而不能满足我们的需求。参考New Relic,我们选择在应用构建期间通过修改字节码的方式来进行代码插桩。 首先我们看一下应用构建的过程: 可以看到,应用中所有的class文件包括引用的第三方库中的class,都会经由dex过程,被转化为一个或者多个dex文件,正因为所有的class文件都会在dex这一步被处理,所以我们选择在这里进行字节码插桩。 dex的过程是在dx程序中进行,而dx程序是由java实现的,这里我们使用到了javaagent技术,它可以使我们在JVM加载class文件前对字节码作出修改,这里简单介绍一下用法,主要分为两步
javaagent的形式是一个jar包,根据javaagent的不同加载方式,对它的实现也有不同的要求。 如果javaagent是在虚拟机启动之后加载的,我们需要在它的manifest文件中指定Agent-Class属性,它的值是javaagent的实现类,这个实现类需要实现一个agentmain方法
agentmain会成为javaagent的入口,它会在javaagent被加载时调用。 但是如果javaagent是在JVM启动时通过命令行参数加载的,情况会不太一样,需要在它的manifest文件中指定Premain-Class属性,它的值是javaagent的实现类,这个实现类需要实现一个premain方法。
我们知道,一个java程序的入口是main方法,而如果javaagent是在JVM启动时通过命令行参数加载的,虚拟机会在应用的main方法执行之前调用javaagent的premain方法,这应该也是premain方法名字的由来吧。 如果要支持两种加载方式,那么上述的条件需要同时满足。并且如果通过命令行参数在JVM启动时加载,agentmain方法不会再被调用。而在这个时候,应用中的类还没有被加载到虚拟机,所以给我们修改字节码带来了便利,因为一个类被加载之后,修改它的字节码会比较麻烦。 我们看到premain方法的第二个参数是一个Instrumentation的实例,Instrumentation接口有一个方法
它会在虚拟机中注册一个ClassFileTransformer,transformer会在类加载时对类进行处理,ClassFileTransformer接口只定义了一个方法
而这个方法的作用就是修改一个类的字节码,className是这个类的名称,classfileBuffer是这个类原本的字节码,而返回值是修改过后的字节码,如果没有修改,可以直接返回null。 因此,如果我们想在程序运行前改变一个类的字节码,可以在javaagent的premain方法中调用Instrumentation的实例的addTransformer方法,添加一个自定义的ClassFileTransformer。伪代码如下:
<h4 id="加载javaagent">加载javaagent 前边已经提到了javaagent有两种加载方式 1) JVM启动时通过命令行参数加载javaagent
2) JVM启动后动态加载javaagent
具体使用细节可参考VirtualMachine介绍 借助javaagent,我们可以将代码插桩的工作分为两个步骤:首先是获取到应用中所有的字节码,然后是对应用的字节码进行修改。 首先从要解决的问题出发,上边提到我们会在dex的这一步去获取字节码,通过查看dx程序的代码,我们发现,在dex的过程中所有的class文件会经由com.android.dx.command.dexer.Main的processClass()方法进行处理,processClass()的代码如下:
第一个参数是应用中一个类的名字,第二个参数就是这个类的字节码了,应用中所有的类,都会经过这个函数进行处理。 所以我们打算修改com.android.dx.command.dexer.Main的processClass()方法,从而获取到应用中的字节码,那么现在的问题就变成了如何修改com.android.dx.command.dexer.Main的processClass()方法。 掌握了javaagent,想要修改dx程序中com.android.dx.command.dexer.Main的字节码就变得比较容易了,我们需要实现一个javaagent,在其中注册一个ClassFileTransformer,在ClassFileTransformer的transform()方法中对com.android.dx.command.dexer.Main的字节码进行修改,最后在dx程序启动时将这个javaagent加载进去就好了。
如果你是通过命令行来手动构建应用的,到这里已经可以用上边的方式获取到应用中的字节码了,然而大多数人在开发Android的时候,并不会通过命令行去手动构建,而是通过使用一些构建工具,来完成自动化构建,而dx程序则是由构建工具启动的,所以我们面临的问题就是如何将javaagent加载到dx进程。 我们目前支持了ant构建和gradle构建,通过查看ant和gradle的代码,我们发现最终它们都会通过java.lang.ProcessBuilder的start()方法来启动dx进程。 通过查看java.lang.ProcessBuilder的代码,我们发现它有一个成员
它是用来保存的是启动目标进程的命令和参数,我们需要做的就是在调用start()方法启动dx进程时,将加载javaagent的参数(-javaagent:jarpath[=options])添加到command中。 这里我们仍然使用javaagent来完成这个工作,我们需要实现另外一个javaagent,在其中注册一个另一个ClassFileTransformer,在它的transform方法中对java.lang.ProcessBuilder的字节码进行修改。
那么最终问题就变成了如何把这个javaagent加载到ant进程和gradle进程。 它们对应到了javaagent的两种加载方式
因此,获取字节码的流程,大致如下图所示: 这个过程中主要使用了两个javaagent,一个用来修改ProcessBuilder类,另一个用来修改Main类,涉及到的进程是ant构建进程或者gradle构建进程,以及由它们启动的dx进程。 对于gradle构建方式,需要注意一点,gradle plugin 在2.1.0之后的版本,支持dx in-process,它使得dx的过程可以直接在当前的gradle进程中执行,而不需要额外启动一个dx进程,从而缩短应用构建的时间。如果你在使用Android Studio构建应用的时候看到To run dex in process,the Gradle daemon needs a larger heap. It currently has 910 MB这样的一句话,它就是指导用户通过配置gradle daemon进程的堆大小来开启dx in-process特性的。 而这个新的特性,会给我们设置javaagent带来麻烦,不启动dx进程使得我们无法对dx进程设置javaagent,而在gradle进程中动态加载javaagent时,com.android.dx.command.dexer.Main类早已经加载过了,所以通过javaagent方式来获取字节码会变得十分困难。 幸运的是,gradle plugin 在1.5.0之后,提供了一个Transform API,它允许第三方插件操作编译后的class文件,而修改的时机正是在将这些字节码转换为dex文件之前,这里就不在展开讲解了,感兴趣的同学可以参考下这篇文章。 通过javaagent修改com.android.dx.command.dexer.Main和java.lang.ProcessBuilder,以及最终修改应用的字节码进行插桩,都需要对.class文件的格式以及java虚拟机有比较深入的了解,另外需要使用字节码操作工具来帮助我们对字节码进行改造,这里不详细讲解,只是推荐一些有用的的字节码操作框架和工具,后边可能会有同事做相关的分享。
本文重点介绍了使用javaagent在应用打包过程中修改com.android.dx.command.dexer.Main和java.lang.ProcessBuilder的字节码,从而获取到应用的字节码,进行插桩的基本原理,并没有涉及so hook相关的原理,以后有机会的话会再做一次分享。 (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |