本文系转载,原文出处为http://www.apmbe.com/android-sdk的设计与实现/

背景:

移动APP的应用性能监测需求,需要持续性的监测移动APP的性能(内存,cpu)趋势,方法栈的性能瓶颈(一些列的方法调用过程中哪一个性能最慢),acitvity的周期内的性能瓶颈(activity周期方法的性能),事务的性能监测(http request到response过程中的性能瓶颈),网络请求的性能监测,数据库的查询性能监测,io的性能监测,地域对于网络的性能监测等。

需求:

1、对开发人员(用户)透明,无需手动打点目标方法/类;
2、监测指标全,从系统层到用户交互;
3、监测方式全,从函数到事务;

设计&实现:

android sdk的整体实现主要分成osgi.fragment,rewriter.agent,xxx.agent,plugin四个部分;


图1.0 sdk+plugin设计方案

1)osgi.fragment
用来给eclipse equinox的plugin打patch,实现runtime加载tools.jar中的VirtualMachine类;
2)rewriter.agent
将自定义InvocationDispatcher注入到android.jar来进行指定类的动态代理,使用asm字节码框架在字节码层面hook android.jar中的类方法和apk开发者自定义的类方法;
3)xxx.agent
收集各种系统、数据库、网络数据,被hook类方法的性能数据,地理信息,异常数据,activity栈数据,线程栈等数据;数据预统计后回传后端服务器;
4)plugin
eclipse plugin的实现,在plugin earlyStartup时使用Attach api向jvm中附着上Instrumentation代理,其中Instrumentation的代理即为rewriter.agent;在plugin IObjectActionDelegate时将xxx.agent安装到apk开发者的android工程库目录中;

1、osgi.fragment

sdk项目的eclipse插件中需要用到tools.jar,使用tools.jar中com.sun.tools.attach.VirtualMachine的loadAgent方法实现运行时动态加载自己的代理jar。默认equinox中没有加载tools.jar且插件中不能打包tools.jar,只能通过给eclipse plugin写fragment的方式来实现runtime加载tools.jar中的类。

1.1、ClassLoaderDelegateHook控制类加载原理

Eclipse的内核使用OSGI框架,而Equinox是OSGI R4的一个实现。Equinox的设计非常经典,其在扩展方面提供了很多的支持,同样包括类加载方面的控制,除了通过标准的org.osgi.framework.bootdelegation以及equinox提供的osgi.parentClassLoader这两个属性来简单的控制类加载之外,还可通过实现ClassLoaderDelegateHook来更为灵活的控制类加载。

1.1.1、原理

EclipseStarter的构造器中创建通过获取HookRegistry来增加自行实现的ClassLoaderDelegateHook类,在HookRegistry的initialize方法中,会去加载配置的osgi.hook.configurators、osgi.hook.configurators.include和osgi.hook.configurators.exclude三个属性值或指定的hookconfigurators.properties文件,最后合并形成需要加载执行的HookConfigurator类;因此,只用在自行实现的ClassLoaderDelegateHook类上再增加HookConfigurator接口的实现,并将其注册到HookRegistry中,最后在osgi.hook.configurators中配置这个类即可;由于equinox内部已经有配置了一些HookConfigurator的,要么就需要把增加的HookConfigurator加到equinox jar的hookconfigurators.properties中。

1.1.2、ClassLoaderDelegateHook实现方法

我们通过开发equinox的plugin fragment来实现上面的想法,plguin fragment继承equinox的ClassLoaderDelegateHook实现类并增加自定义的ClassLoaderDelegateHook实现类(在其中控制我们需要的tools.jar中类的加载,自定义的ClassLoaderDelegateHook实现类配置在fragment的hookconfigurators.properties中);

1.2、Equinox的classloader查找和加载class的顺序

Equinox的ClassLoadingHook在createClassLoader后就完成了ClassLoader的创建,开始加载class,其顺序如下;
1)判断是否交由parent classloader去完成加载
2) 尝试调用Equinox提供的ClassLoaderDelegateHook的扩展来加载
3) 判断是否在import-package中,如在则交由相应的PackageSource去加载
4) 尝试从require-bundle中加载
5) 尝试从当前Bundle中加载
6) 尝试从DynamicImport-Package中加载
7) 再次尝试调用Equinox提供的ClassLoaderDelegateHook的扩展来加载
8) 尝试使用eclipse的buddy机制来加载
9) 判断一定的条件,如符合则从parent classloader中加载
10)经过以上所有步骤后,仍然未找到需要加载的class,则抛出ClassNotFoundException

因此我们将tools.jar中class的加载逻辑放到ClassLoaderDelegateHook接口的postFindClass方法中实现;

1.3、实现细节

1.3.1、ClassLoaderDelegateHook类实现

1.3.2、添加hookconfigurators.properties

2、rewriter.agent

2.1、rewriter的字节码hook切入时点(android的dx工具打包时)

在eclipse中,当android工程中的java代码被编译成class后,eclipse会调用dx工具将java class文件打包并转换成.dex文件,这个转换的工作在com.android.dx.command.dexer.Main的processClass方法中进行,因此我们只需要在这个时间点切入完成我们的字节码层的hook工作(在目标方法中嵌入xxx.agent的监测函数)即可;
另外,为了更好的理解dx的工作,我们可以看一下android的构建过程,如下图所示;

图2.1 android的build process

2.2、InvocationHandler动态代理类

InvocationHandler实现类InvocationDispatcher用来将指定的代理类(这里指com.android.dx.command.dexer.Main的processClass)实例做字节码的hook,其中java字节码的hook工作使用asm字节码框架来进行(关于如何使用asm请查看我以前的文章“java asm库的原理与使用方法(一)”和“java asm库的原理与使用方法(二)”);
visitClassBytes方法中使用了几个自定义的xxxClassVisitor用来分别hook android.jar中的annotation,activity,AsyncTask,网络,sqllite,exception等类方法;
InvocationDispatcher类代码如下:

2.3、Instrumentation代理(rewriter.agent)

具体的Instrumentation代理实现涉及到下面几个部分;
1)agentmain方法
Instrumentation的最大作用,就是类定义动态改变和操作。在JDK6以后,将类动态操作逻辑放到agentmain方法中,可以实现让Instrumentation代理在main函数运行前执行;
2)ClassFileTransformer实现类
对Java类文件的操作,可以理解为对一个byte数组的操作(将类文件的二进制字节流读入一个byte数组)。开发者可以在“ClassFileTransformer”的transform方法当中得到,操作并最终返回一个类的定义(一个byte数组);Instrumentation的addTransformer方法指明要转换哪个类。转换发生在premain方法中,main函数执行之前,这时每装载一个类,transform方法就会执行一次,看看是否需要转换,所以,在transform(Transformer 类)方法中,可用className.equals(“xxxClass”) 来判断当前的类是否需要转换。
3)Attach API
具体的实现上还需要使用Attach API;Attach API很简单,只有2个主要的类,都在com.sun.tools.attach包里面:VirtualMachine代表一个Java虚拟机,也就是程序需要监控的目标虚拟机,提供了JVM枚举,Attach动作和 Detach动作(Attach动作的相反行为,从JVM上面解除一个代理)等等; VirtualMachineDescriptor则是一个描述虚拟机的容器类,配合VirtualMachine类完成各种功能。

实现过程为:
1)Instrumentation代理的agentmain作为入口方法;
2)通过反射将InvocationDispatcher实例赋值给android.jar中java.util.logging.Logger.class的treeLock(Logger类已被装载到runtime,这里的treeLock用作同步的object无其他用途);
3)instrumentation实例的addTransformer方法加载ClassFileTransformer实现类;ClassFileTransformer实现类中的transform方法会根据当前传入的className,调用不同的ClassAdapter;其中在ClassAdapter中将触发InvocationDispatcher实例(java.util.logging.Logger.class的treeLock)中代理类的invoke方法;

比如:当装载com/android/dx/command/dexer/Main类时,transform会找到Main类对应的ClassAdapter,当ClassReader遍历到processClass方法时,则会执行MethodVisitor的onMethodEnter方法,方法内触发InvocationDispatcher实例(java.util.logging.Logger.class的treeLock)中代理类的invoke方法,invoke方法中将使用com/android/dx/command/dexer/Main代理类的InvocationHandler invoke方法,触发visitClassBytes方法做字节码的hook工作;(其中visitClassBytes方法中使用了几个自定义的xxxClassVisitor用来分别hook android.jar中的annotation,activity,AsyncTask,网络,sqllite,exception等类方法)

关键功能代码;
InvocationDispatcher实例注入;

DexClassTransformer:

premain方法;

3、xxx.agent

xxx.agent的整体框架主要分成android api注入,数据采集器,数据任务队列,measurement生产者消费者模型,数据预处理和发送模块;
xxx.agent模块的整体框架如下图所示;

图2.2 agent的模块框架

3.1、android api注入

android api的注入工作在rewriter.agent中完成,根据type_map.properties文件中配置的目标api注入方式进行相应的替换(REPLACE_CALL_SITE)和包裹(WRAP_METHOD)注入;

3.2、数据采集器

采集器采集的数据种类主要有StatsEngine数据,sampler数据,TransactionState数据,activityTrace数据,threadLoadTrace数据,HttpTransaction数据,HttpError数据,Activity生命周期数据;
TraceMachine作为sampler,TransactionState,activityTrace,threadLoadTrace数据,HttpTransaction数据,HttpError数据,Activity生命周期数据的采集器;

3.2.1、性能数据采集方式

主要有以下几种;
1)method的性能数据采集
将enterMethod和exitMethod方法注入(注入工作在rewriter.agent中完成)到替换android api的api开始和结束处,hook的目的用来采集该api执行的性能数据;其中sampler,SummaryMetricMeasurementConsumer都实现了TraceLifecycleAware接口并在TraceMachine中注册,当已被hook api触发TraceMachine.enterMethod和exitMethod方法时,会启动和结束TraceMachine中已注册类实例的性能数据采样工作;
比如,当应用执行sqlite的query方法时会触发TraceMachine.enterMethod(“SQLiteDatabase#query”),在enterMethod方法中启动Trace跟踪和调用sampler的onEnterMethod方法启动内存数据的采样。当sqlite结束时会触发TraceMachine.exitMethod(),在exitMethod方法中结束trace跟中和调用sampler的onExitMethod方法停止内存采样;最后将trace放到TaskQueue中,TaskQueue会负责将trace分发到对应的Measurement中;
2)事务的性能数据采集
将TransactionState实例注入(注入工作在rewriter.agent中完成)到替换android api的api中,TransactionState实例会记录本次事务过程中产生的性能数据;
比如,当应用执行HttpRequest的execute方法时会被替换成使用HttpInstrumentation中的execute方法,其中会使用TransactionState实例记录整个request到response过程中的数据,数据包括request uri,运营商信息,header,response code,response data等;
3)activity的性能数据采集
主要hook了activity生命周期内的onStart,activityStarted和onStop方法;在这些方法中注入enterMethod和exitMethod方法,在执行时会通过registerNewTrace方法创建childTrace并加入到TraceMachine的rootActivityTrace中;
4)StatsEngine异常数据采集
StatsEngine用来记录newrlic.agent本身的异常数据,采集在与后端服务器数据交互的周期内agent本身发生的一些异常信息,它继承了HarvestAdapter类,因此会在Harvester发生CONNECTIONED状态时被触发onHarvest方法->populateMetrics方法将异常数据包装成Metric通过TaskQueue发送给对应的HarvestData中machineMeasurements;

3.3、任务队列

TaskQueue类为xxx.agent中的任务队列,它在Measurements中实例化,用来接收不同数据采集器采集到的性能数据,并根据类型将他们分别转发到下游的Measurements工厂和Harvest中;其中Measurements为生产者和消费者模型,进行性能数据预处理、统计和组装工作;

3.4、Measurements”工厂”

Measurements作为所有MeasurementProducer和MeasurementConsumer的管理者,通过MeasurementEngine的MeasurementPool增删producer和consumer,它主要负责生产和消费不同种类性能数据;
比如,当收到TaskQueue的Trace数据时,会使用methodMeasurementProducer的produceMeasurement方法包装trace数据并通过MeasurementPool的broadcastMeasurements方法通知对应的trace数据消费者methodMeasurementConsumer,消费者使用consumeMeasurement将处理后的数据加入到自己的MetricStore中;

3.5、Harvest

Harvest负责对所有数据最终收集、验证、打包和回传服务器;HarvestTimer作为定时器会周期性的触发数据收集和回传服务器的动作,具体的过程为,HarvestTimer定时器触发Harvester的execute方法,若CONNECTED状态则会触发fireOnHarvestBefore,fireOnHarvest,fireOnHarvestFinalize方法,fireOnXXX方法中会遍历所有已实现HarvestLifecycleAware接口并且注册在Harvester的XXXMeasurementConsumer,XXXMeasurementConsumer中的onHarvestXXX方法会将采集、处理过的数据统一添加到HarvestData中并打包;最后通过Harvester的connected方法发送本周期的性能数据到服务器上;

4、plugin部分

这部分比较简单,按照正常的plugin开发流程写就是了;
主要步骤;
1)在plugin.xml中把导出的class.rewriter.jar和xxx.android.jar加入到Classpath中;
2)开发实现IStartup接口的Bootstrap类,并实现earlyStartup方法;在这里需要通过tools.jar的com.sun.tools.attach.VirtualMachine动态加载Instrumentation代理(class.rewriter.jar中的rewriter.agent),另外,runtime中装载tools.jar前面已经说过;
主要的代码如下;

3)开发IObjectActionDelegate实现xxx.android.jar的自动安装
右键install会将xxx.android.jar安装到工程libs中;主要代码如下;

 改进:

后续主要从稳定性和监测指标覆盖面来优化android sdk,指标业务化和定制化,更好的服务于用户系统和业务层面;