本文系转载,原文出处为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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
|
package
com
.
xxx
.
yyy
.
agent
.
android
.
osgi
.
framework
;
import
java
.
io
.
*
;
import
java
.
net
.
*
;
import
java
.
util
.
Enumeration
;
import
org
.
eclipse
.
osgi
.
baseadaptor
.
HookConfigurator
;
import
org
.
eclipse
.
osgi
.
baseadaptor
.
HookRegistry
;
import
org
.
eclipse
.
osgi
.
framework
.
adaptor
.
*
;
public
class
ToolsClassLoaderDelegate
implements
ClassLoaderDelegateHook
,
HookConfigurator
{
private
boolean
flag
;
private
URLClassLoader
theToolsLoader
;
public
ToolsClassLoaderDelegate
(
)
{
theToolsLoader
=
createURLClassloader
(
)
;
}
public
void
addHooks
(
HookRegistry
hookRegistry
)
{
hookRegistry
.
addClassLoaderDelegateHook
(
this
)
;
}
public
Class
postFindClass
(
String
name
,
BundleClassLoader
classLoader
,
BundleData
data
)
{
//if(data.getSymbolicName().startsWith("com.xxx")){
if
(
theToolsLoader
!=
null
&&
!
flag
)
{
if
(
name
.
startsWith
(
"com.sun.tools"
)
)
{
try
{
Class
class1
;
flag
=
true
;
class1
=
theToolsLoader
.
loadClass
(
name
)
;
return
class1
;
}
catch
(
Throwable
t
)
{
t
.
printStackTrace
(
)
;
}
finally
{
flag
=
false
;
}
}
}
//}
return
null
;
}
private
URLClassLoader
createURLClassloader
(
)
{
try
{
File
javaHome
;
try
{
javaHome
=
(
new
File
(
System
.
getProperty
(
"java.home"
)
)
)
.
getCanonicalFile
(
)
;
}
catch
(
IOException
e
)
{
throw
new
IllegalStateException
(
"Unable to locate java home"
,
e
)
;
}
if
(
!
javaHome
.
exists
(
)
)
throw
new
IllegalStateException
(
(
new
StringBuilder
(
"The java home '"
)
)
.
append
(
javaHome
.
getAbsolutePath
(
)
)
.
append
(
"' does not exist"
)
.
toString
(
)
)
;
File
jarFile
=
new
File
(
javaHome
.
getParent
(
)
,
"lib/tools.jar"
)
;
if
(
jarFile
.
exists
(
)
)
{
URL
url
=
jarFile
.
getCanonicalFile
(
)
.
toURI
(
)
.
toURL
(
)
;
return
new
URLClassLoader
(
new
URL
[
]
{
url
}
,
null
)
;
}
}
catch
(
Exception
e
)
{
e
.
printStackTrace
(
)
;
}
return
null
;
}
public
String
postFindLibrary
(
String
arg0
,
BundleClassLoader
arg1
,
BundleData
arg2
)
{
return
null
;
}
public
URL
postFindResource
(
String
arg0
,
BundleClassLoader
arg1
,
BundleData
arg2
)
throws
FileNotFoundException
{
return
null
;
}
public
Enumeration
postFindResources
(
String
arg0
,
BundleClassLoader
arg1
,
BundleData
arg2
)
throws
FileNotFoundException
{
return
null
;
}
public
Class
preFindClass
(
String
arg0
,
BundleClassLoader
arg1
,
BundleData
arg2
)
throws
ClassNotFoundException
{
return
null
;
}
public
String
preFindLibrary
(
String
arg0
,
BundleClassLoader
arg1
,
BundleData
arg2
)
throws
FileNotFoundException
{
return
null
;
}
public
URL
preFindResource
(
String
arg0
,
BundleClassLoader
arg1
,
BundleData
arg2
)
throws
FileNotFoundException
{
return
null
;
}
public
Enumeration
preFindResources
(
String
arg0
,
BundleClassLoader
arg1
,
BundleData
arg2
)
throws
FileNotFoundException
{
return
null
;
}
}
|
1.3.2、添加hookconfigurators.properties
1
|
hook
.
configurators
=
com
.
xxx
.
yyy
.
agent
.
android
.
osgi
.
framework
.
ToolsClassLoaderDelegate
|
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类代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
|
private
static
class
InvocationDispatcher
implements
InvocationHandler
{
private
final
Log
log
;
private
final
ClassRemapperConfig
config
;
private
final
InstrumentationContext
context
;
private
final
Map
invocationHandlers
;
private
boolean
writeDisabledMessage
;
private
final
String
agentJarPath
=
RewriterAgent
.
getAgentJarPath
(
)
;
private
boolean
disableInstrumentation
;
private
boolean
isInstrumentationDisabled
(
)
{
return
disableInstrumentation
||
System
.
getProperty
(
DISABLE_INSTRUMENTATION_SYSTEM_PROPERTY
)
!=
null
;
}
private
boolean
isExcludedPackage
(
String
packageName
)
{
for
(
Iterator
<String>
i
=
RewriterAgent
.
EXCLUDED_PACKAGES
.
iterator
(
)
;
i
.
hasNext
(
)
;
)
{
String
name
=
i
.
next
(
)
;
if
(
packageName
.
contains
(
name
)
)
return
true
;
}
return
false
;
}
public
Object
invoke
(
Object
proxy
,
java
.
lang
.
reflect
.
Method
method
,
Object
args
[
]
)
throws
Throwable
{
InvocationHandler
handler
=
(
InvocationHandler
)
invocationHandlers
.
get
(
proxy
)
;
if
(
handler
==
null
)
{
log
.
error
(
(
new
StringBuilder
(
)
)
.
append
(
"Unknown invocation type: "
)
.
append
(
proxy
)
.
append
(
". Arguments: "
)
.
append
(
Arrays
.
asList
(
args
)
)
.
toString
(
)
)
;
return
null
;
}
try
{
return
handler
.
invoke
(
proxy
,
method
,
args
)
;
}
catch
(
Throwable
t
)
{
log
.
error
(
(
new
StringBuilder
(
)
)
.
append
(
"Error:"
)
.
append
(
t
.
getMessage
(
)
)
.
toString
(
)
,
t
)
;
}
return
null
;
}
private
ClassData
visitClassBytes
(
byte
bytes
[
]
)
{
String
className
=
"an unknown class"
;
ClassReader
cr
;
ClassWriter
cw
;
ClassVisitor
cv
=
null
;
try
{
cr
=
new
ClassReader
(
bytes
)
;
cw
=
new
ClassWriter
(
cr
,
1
)
;
context
.
reset
(
)
;
cr
.
accept
(
new
PrefilterClassVisitor
(
context
,
log
)
,
7
)
;
className
=
context
.
getClassName
(
)
;
if
(
!
context
.
hasTag
(
"Lcom/apmbe/mtrix/agent/android/instrumentation/Instrumented;"
)
)
{
cv
=
cw
;
if
(
context
.
getClassName
(
)
.
startsWith
(
"com/apmbe/mtrix/agent/android"
)
)
{
cv
=
new
MtrixClassVisitor
(
cv
,
context
,
log
)
;
}
else
if
(
context
.
getClassName
(
)
.
startsWith
(
"android/support/"
)
)
{
cv
=
new
ActivityClassVisitor
(
cv
,
context
,
log
)
;
}
else
if
(
isExcludedPackage
(
context
.
getClassName
(
)
)
)
return
null
;
else
{
cv
=
new
AnnotatingClassVisitor
(
cv
,
context
,
log
)
;
cv
=
new
ActivityClassVisitor
(
cv
,
context
,
log
)
;
cv
=
new
AsyncTaskClassVisitor
(
cv
,
context
,
log
)
;
cv
=
new
TraceAnnotationClassVisitor
(
cv
,
context
,
log
)
;
cv
=
new
WrapMethodClassVisitor
(
cv
,
context
,
log
)
;
cv
=
new
ContextInitializationClassVisitor
(
cv
,
context
)
;
}
cr
.
accept
(
cv
,
12
)
;
}
else
{
log
.
warning
(
MessageFormat
.
format
(
"[{0}] class is already instrumented! skipping ..."
,
new
Object
[
]
{
context
.
getFriendlyClassName
(
)
}
)
)
;
}
}
catch
(
SkipException
ex
)
{
return
null
;
}
catch
(
HaltBuildException
e
)
{
throw
new
RuntimeException
(
e
)
;
}
catch
(
Throwable
t
)
{
log
.
error
(
(
new
StringBuilder
(
)
)
.
append
(
"Unfortunately, an error has occurred while processing "
)
.
append
(
className
)
.
append
(
". Please copy your build logs and the jar containing this class and send a message to support@mmtrix.com, thanks!\n"
)
.
append
(
t
.
getMessage
(
)
)
.
toString
(
)
,
t
)
;
return
new
ClassData
(
bytes
,
false
)
;
}
return
context
.
newClassData
(
cw
.
toByteArray
(
)
)
;
}
public
InvocationDispatcher
(
final
Log
log
)
throws
FileNotFoundException
,
IOException
,
ClassNotFoundException
,
URISyntaxException
{
writeDisabledMessage
=
true
;
disableInstrumentation
=
false
;
this
.
log
=
log
;
config
=
new
ClassRemapperConfig
(
log
)
;
context
=
new
InstrumentationContext
(
config
,
log
)
;
invocationHandlers
=
new
HashMap
<
Object
,
InvocationHandler
>
(
)
{
private
static
final
long
serialVersionUID
=
1L
;
{
put
(
getProxyInvocationKey
(
"com/android/dx/command/dexer/Main"
,
"processClass"
)
,
new
InvocationHandler
(
)
{
public
Object
invoke
(
Object
proxy
,
java
.
lang
.
reflect
.
Method
method
,
Object
args
[
]
)
throws
Throwable
{
byte
bytes
[
]
=
(
byte
[
]
)
args
[
1
]
;
if
(
isInstrumentationDisabled
(
)
)
{
if
(
writeDisabledMessage
)
{
writeDisabledMessage
=
false
;
log
.
info
(
"Instrumentation disabled, no agent present"
)
;
}
return
bytes
;
}
writeDisabledMessage
=
true
;
ClassData
classData
=
visitClassBytes
(
bytes
)
;
if
(
classData
!=
null
&&
classData
.
getMainClassBytes
(
)
!=
null
&&
classData
.
isModified
(
)
)
return
classData
.
getMainClassBytes
(
)
;
else
return
bytes
;
}
}
)
;
}
}
;
}
}
|
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实例注入;
1
2
3
4
5
|
private
static
void
createInvocationDispatcher
(
Log
log
)
throws
Exception
{
Field
field
=
java
.
util
.
logging
.
Logger
.
class
.
getDeclaredField
(
treeLock
)
;
field
.
setAccessible
(
true
)
;
field
.
set
(
null
,
new
InvocationDispatcher
(
log
)
)
;
}
|
DexClassTransformer:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
private
static
final
class
DexClassTransformer
implements
ClassFileTransformer
{
private
Log
log
;
private
final
Map
classVisitors
;
public
boolean
modifies
(
Class
clazz
)
{
Type
t
=
Type
.
getType
(
clazz
)
;
return
classVisitors
.
containsKey
(
t
.
getInternalName
(
)
)
;
}
public
byte
[
]
transform
(
ClassLoader
classLoader
,
String
className
,
Class
clazz
,
ProtectionDomain
protectionDomain
,
byte
bytes
[
]
)
throws
IllegalClassFormatException
{
ClassVisitorFactory
factory
=
(
ClassVisitorFactory
)
classVisitors
.
get
(
className
)
;
if
(
factory
!=
null
)
{
if
(
clazz
!=
null
&&
!
factory
.
isRetransformOkay
(
)
)
{
log
.
error
(
(
new
StringBuilder
(
)
)
.
append
(
"Cannot instrument "
)
.
append
(
className
)
.
toString
(
)
)
;
return
null
;
}
log
.
debug
(
(
new
StringBuilder
(
)
)
.
append
(
"Patching "
)
.
append
(
className
)
.
toString
(
)
)
;
try
{
ClassReader
cr
=
new
ClassReader
(
bytes
)
;
ClassWriter
cw
=
new
PatchedClassWriter
(
3
,
classLoader
)
;
ClassAdapter
adapter
=
factory
.
create
(
cw
)
;
cr
.
accept
(
adapter
,
4
)
;
return
cw
.
toByteArray
(
)
;
}
catch
(
SkipException
ex
)
{
}
catch
(
Exception
ex
)
{
log
.
error
(
(
new
StringBuilder
(
)
)
.
append
(
"Error transforming class "
)
.
append
(
className
)
.
toString
(
)
,
ex
)
;
}
}
return
null
;
}
public
DexClassTransformer
(
final
Log
log
)
throws
URISyntaxException
{
final
String
agentJarPath
;
try
{
agentJarPath
=
RewriterAgent
.
getAgentJarPath
(
)
;
}
catch
(
URISyntaxException
e
)
{
log
.
error
(
"Unable to get the path to the New Relic class rewriter jar"
,
e
)
;
throw
e
;
}
this
.
log
=
log
;
classVisitors
=
new
HashMap
<
String
,
ClassVisitorFactory
>
(
)
{
private
static
final
long
serialVersionUID
=
1L
;
{
put
(
"com/android/dx/command/dexer/Main"
,
new
ClassVisitorFactory
(
true
)
{
public
ClassAdapter
create
(
ClassVisitor
cv
)
{
return
RewriterAgent
.
createDexerMainClassAdapter
(
cv
,
log
)
;
}
}
)
;
}
}
;
}
}
private
static
ClassAdapter
createDexerMainClassAdapter
(
ClassVisitor
cw
,
final
Log
log
)
{
return
new
ClassAdapterBase
(
log
,
cw
,
new
HashMap
<
Method
,
MethodVisitorFactory
>
(
)
{
private
static
final
long
serialVersionUID
=
1L
;
{
put
(
new
Method
(
"processClass"
,
"(Ljava/lang/String;[B)Z"
)
,
new
MethodVisitorFactory
(
)
{
public
MethodVisitor
create
(
MethodVisitor
mv
,
int
access
,
String
name
,
String
desc
)
{
return
new
RewriterAgent
.
BaseMethodVisitor
(
mv
,
access
,
name
,
desc
)
{
protected
void
onMethodEnter
(
)
{
log
.
debug
(
"Found onMethodEnter in processClass"
)
;
builder
.
loadInvocationDispatcher
(
)
.
loadInvocationDispatcherKey
(
RewriterAgent
.
getProxyInvocationKey
(
"com/android/dx/command/dexer/Main"
,
methodName
)
)
.
loadArgumentsArray
(
methodDesc
)
.
invokeDispatcher
(
false
)
;
checkCast
(
Type
.
getType
(
"[B"
)
)
;
storeArg
(
1
)
;
}
}
;
}
}
)
;
}
}
)
;
}
|
premain方法;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
public
static
void
premain
(
String
agentArgs
,
Instrumentation
instrumentation
)
{
Throwable
argsError
=
null
;
try
{
agentOptions
=
parseAgentArgs
(
agentArgs
)
;
}
catch
(
Throwable
t
)
{
argsError
=
t
;
}
String
logFileName
=
(
String
)
agentOptions
.
get
(
"logfile"
)
;
Log
log
=
(
(
Log
)
(
logFileName
!=
null
?
(
(
Log
)
(
new
FileLogImpl
(
agentOptions
,
logFileName
)
)
)
:
(
(
Log
)
(
new
SystemErrLog
(
agentOptions
)
)
)
)
)
;
if
(
argsError
!=
null
)
log
.
error
(
(
new
StringBuilder
(
)
)
.
append
(
"Agent args error: "
)
.
append
(
agentArgs
)
.
toString
(
)
,
argsError
)
;
try
{
createInvocationDispatcher
(
log
)
;
DexClassTransformer
dexClassTransformer
=
new
DexClassTransformer
(
log
)
;
instrumentation
.
addTransformer
(
dexClassTransformer
,
true
)
;
List
<Class>
classes
=
new
ArrayList
<Class>
(
)
;
Class
arr
[
]
=
instrumentation
.
getAllLoadedClasses
(
)
;
int
len
=
arr
.
length
;
for
(
int
i
=
0
;
i
<
len
;
i
++
)
{
Class
clazz
=
arr
[
i
]
;
if
(
dexClassTransformer
.
modifies
(
clazz
)
)
{
classes
.
add
(
clazz
)
;
}
}
if
(
!
classes
.
isEmpty
(
)
)
if
(
instrumentation
.
isRetransformClassesSupported
(
)
)
{
log
.
debug
(
(
new
StringBuilder
(
)
)
.
append
(
"Retransform classes: "
)
.
append
(
classes
)
.
toString
(
)
)
;
instrumentation
.
retransformClasses
(
(
Class
[
]
)
classes
.
toArray
(
new
Class
[
classes
.
size
(
)
]
)
)
;
}
else
{
log
.
error
(
(
new
StringBuilder
(
)
)
.
append
(
"Unable to retransform classes: "
)
.
append
(
classes
)
.
toString
(
)
)
;
}
redefineClass
(
instrumentation
,
dexClassTransformer
,
java
.
lang
.
ProcessBuilder
.
class
)
;
}
catch
(
Throwable
ex
)
{
System
.
out
.
println
(
"Agent startup error"
)
;
throw
new
RuntimeException
(
ex
)
;
}
}
|
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前面已经说过;
主要的代码如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
import
com
.
sun
.
tools
.
attach
.
VirtualMachine
;
import
java
.
io
.
File
;
import
java
.
io
.
IOException
;
import
java
.
lang
.
management
.
ManagementFactory
;
import
org
.
eclipse
.
ui
.
IStartup
;
public
class
Bootstrap
implements
IStartup
{
public
Bootstrap
(
)
{
}
public
void
earlyStartup
(
)
{
String
nameOfRunningVM
=
ManagementFactory
.
getRuntimeMXBean
(
)
.
getName
(
)
;
int
p
=
nameOfRunningVM
.
indexOf
(
'@'
)
;
String
pid
=
nameOfRunningVM
.
substring
(
0
,
p
)
;
String
jarFilePath
;
try
{
jarFilePath
=
com
.
apmbe
.
mtrix
.
agent
.
compile
.
RewriterAgent
.
class
.
getProtectionDomain
(
)
.
getCodeSource
(
)
.
getLocation
(
)
.
getPath
(
)
.
toString
(
)
;
jarFilePath
=
(
new
File
(
jarFilePath
)
)
.
getCanonicalPath
(
)
;
}
catch
(
IOException
e
)
{
throw
new
RuntimeException
(
e
)
;
}
try
{
VirtualMachine
vm
=
VirtualMachine
.
attach
(
pid
)
;
vm
.
loadAgent
(
jarFilePath
,
"debug=true;"
)
;
vm
.
detach
(
)
;
}
catch
(
NoClassDefFoundError
e
)
{
Util
.
dialogMessage
(
"Unfortunately, apmbe was unable to load properly. It's likely you're attempting to run Eclipse using the JRE instead of the JDK. To run Eclipse using the JDK, you can either prepend the JDK to your system PATH or launch Eclipse with the -vm <path/to/jdk/javaw.exe> argument. For more information, see http://www.apmbe.com/help."
)
;
throw
new
RuntimeException
(
e
)
;
}
catch
(
Exception
e
)
{
String
message
=
(
new
StringBuilder
(
"Unfortunately, xxx was unable to load properly. Please contact support@mtrix.com and include the following error message: "
)
)
.
append
(
e
.
getMessage
(
)
)
.
toString
(
)
;
Util
.
dialogMessage
(
message
)
;
throw
new
RuntimeException
(
e
)
;
}
Util
.
NRSingleton
.
getInstance
(
)
.
setBooted
(
)
;
}
}
|
3)开发IObjectActionDelegate实现xxx.android.jar的自动安装
右键install会将xxx.android.jar安装到工程libs中;主要代码如下;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
public
class
Install
implements
IObjectActionDelegate
{
final
String
localAgentVersion
;
private
ISelection
selection
;
IProject
project
;
public
Install
(
)
{
localAgentVersion
=
com
.
apmbe
.
mtrix
.
agent
.
android
.
Agent
.
getVersion
(
)
;
}
public
void
run
(
IAction
action
)
{
if
(
selection
instanceof
IStructuredSelection
)
{
Object
selected
=
(
(
IStructuredSelection
)
selection
)
.
getFirstElement
(
)
;
if
(
selected
instanceof
IProject
)
project
=
(
IProject
)
selected
;
else
if
(
selected
instanceof
IAdaptable
)
project
=
(
IProject
)
(
(
IAdaptable
)
selected
)
.
getAdapter
(
org
.
eclipse
.
core
.
resources
.
IProject
.
class
)
;
else
return
;
if
(
!
Util
.
NRSingleton
.
getInstance
(
)
.
isBooted
(
)
)
{
Util
.
dialogMessage
(
"Unfortunately, apmbe was unable to load and cannot continue. Please run Eclipse with the -consoleLog option and forward the log to support@mtrix.com"
)
;
return
;
}
try
{
File
jarFile
=
new
File
(
com
.
apmbe
.
mtrix
.
agent
.
android
.
xxx
.
class
.
getProtectionDomain
(
)
.
getCodeSource
(
)
.
getLocation
(
)
.
getPath
(
)
.
toString
(
)
)
;
File
libFile
=
project
.
getLocation
(
)
.
append
(
"libs"
)
.
append
(
"apmbe.android.jar"
)
.
toFile
(
)
;
addNature
(
)
;
if
(
libFile
.
exists
(
)
)
{
String
currentVersion
=
jarVersion
(
libFile
)
;
if
(
isOtherVersion
(
libFile
)
)
{
copyFile
(
jarFile
,
libFile
)
;
Util
.
dialogMessage
(
(
new
StringBuilder
(
"Update complete, I found version "
)
)
.
append
(
currentVersion
)
.
append
(
" of the agent and replaced it with "
)
.
append
(
localAgentVersion
)
.
toString
(
)
)
;
}
else
{
copyFile
(
jarFile
,
libFile
)
;
Util
.
dialogMessage
(
(
new
StringBuilder
(
"Hurray! It looks like you already have the latest version ("
)
)
.
append
(
localAgentVersion
)
.
append
(
") of the apmbe Agent installed."
)
.
toString
(
)
)
;
}
}
else
{
File
libDir
=
project
.
getLocation
(
)
.
append
(
"libs"
)
.
toFile
(
)
;
if
(
!
libDir
.
exists
(
)
)
libDir
.
mkdirs
(
)
;
copyFile
(
jarFile
,
libFile
)
;
Util
.
dialogMessage
(
(
new
StringBuilder
(
"Congratulations! Version "
)
)
.
append
(
localAgentVersion
)
.
append
(
" of the apmbe Android Agent has been installed in your libs directory. "
)
.
append
(
"Please check that apmbe.android.jar is included on your build path. Additionally, make sure you've added the code necessary "
)
.
append
(
"to boot the agent to your app's initilization section and we look forward to seeing you at http://www.apmbe.com."
)
.
toString
(
)
)
;
}
project
.
refreshLocal
(
2
,
null
)
;
}
catch
(
IOException
e
)
{
Util
.
dialogMessage
(
(
new
StringBuilder
(
"Woops, something went wrong while trying to copy the apmbe agent jar: "
)
)
.
append
(
e
.
getMessage
(
)
)
.
toString
(
)
)
;
e
.
printStackTrace
(
)
;
}
catch
(
CoreException
e
)
{
Util
.
dialogMessage
(
(
new
StringBuilder
(
"Woops, something went wrong while trying to add the apmbe nature to your project: "
)
)
.
append
(
e
.
getMessage
(
)
)
.
toString
(
)
)
;
e
.
printStackTrace
(
)
;
}
}
}
|
改进:
后续主要从稳定性和监测指标覆盖面来优化android sdk,指标业务化和定制化,更好的服务于用户系统和业务层面;