比如自动采集点击事件:其实就是在transform中遍历当前应用的所有.class文件,然后用ASM相关API去加载和解析相应的.class文件,然后找到满足特定条件的.class文件和相关方法,最后就是动态修改相应的方法以动态插入埋点字节码。 1:创建transform:
class DataTrackTransform extends Transform {
private static Project project
DataTrackTransform(Project project) {
this.project = project
}
@Override
String getName() {
return "DataTrackTransform"
}
@Override
Set<QualifiedContent.ContentType> getInputTypes() {
return TransformManager.CONTENT_CLASS
}
@Override
Set<? super QualifiedContent.Scope> getScopes() {
return TransformManager.SCOPE_FULL_PROJECT
}
@Override
boolean isIncremental() {
return false
}
@Override
void transform(Context context, Collection<TransformInput> inputs, Collection<TransformInput> referencedInputs, TransformOutputProvider outputProvider, boolean isIncremental) throws IOException, TransformException, InterruptedException {
if (!incremental) {
outputProvider.deleteAll()
}
inputs.each { TransformInput input ->
//遍历目录
input.directoryInputs.each { DirectoryInput directoryInput ->
//当前transform输出目录
File dest = outputProvider.getContentLocation(directoryInput.name, directoryInput.contentTypes, directoryInput.scopes, Format.DIRECTORY)
File dir = directoryInput.file
if (dir) {
HashMap<String, File> modifyMap = new HashMap<>()
//遍历以某个扩展名结尾的文件
dir.traverse(type: FileType.FILES, nameFilter: ~/.*\.class/) {
File classFile ->
if (SensorsAnalyticsClassModifier.isShouldModify(classFile.class.name)) {
File modified = SensorsAnalyticsClassModifier.modifyClassFile(dir, classFile, context.getTemporaryDir())
if (modified != null) {
//key为包名+类名
String ke = classFile.absolutePath.replace(dir.absolutePath, "")
modifyMap.put(ke, modified)
}
}
}
//先将整个目录拷贝
FileUtils.copyDirectory(directoryInput.file, dest)
//如果有修改的文件,则将修改后的文件直接替换到输出的文件夹中
modifyMap.entrySet().each {
Map.Entry<String, File> en ->
File target = new File(dest.absolutePath + en.getKey())
if (target.exists()) {
target.delete()
}
FileUtils.copyFile(en.getValue(), target)
en.getValue().delete()
}
}
}
input.jarInputs.each { JarInput jarInput ->
String destName = jarInput.file.name
//截取文件的md5值重命名输出文件,因为可能同名会覆盖
def hexName = DigestUtils.md5Hex(jarInput.file.absolutePath).substring(0, 8)
//获取jar名字
if (destName.endsWith(".jar")) {
destName = destName.substring(0, destName.length() - 4)
}
File dest = outputProvider.getContentLocation(destName + "_" + hexName, jarInput.contentTypes, jarInput.scopes, Format.JAR)
def modifyJar = SensorsAnalyticsClassModifier.modifyJar(jarInput.file, context.getTemporaryDir(), true)
if (modifyJar == null) {
modifyJar = jarInput.file
}
FileUtils.copyFile(modifyJar, dest)
}
}
}
}
这里的关键点主要有以下几个: .isShouldModify:以类名为条件过滤掉一部分.class文件,主要是为了性能考虑.
modifyClassFile:开始修改class文件。它的具体实现如下
static File modifyClassFile(File dir, File classFile, File tempDir) {
File modified = null
try {
//将绝对路径名中的/替换成.并且去掉文件名后缀.class
String className = path2ClassName(classFile.absolutePath.replace(dir.absolutePath + File.separator, ""))
//获取源文件的字节码
byte[] sourceClassBytes = IOUtils.toByteArray(new FileInputStream(classFile))
//修改字节码
byte[] modifiedClassBytes = modifyClass(sourceClassBytes)
if (modifiedClassBytes) {
modified = new File(tempDir, className.replace('.', '') + '.class')
if (modified.exists()) {
modified.delete()
}
modified.createNewFile()
new FileOutputStream(modified).write(modifiedClassBytes)
}
} catch (Exception e) {
e.printStackTrace()
modified = classFile
}
return modified
}
这里最重要的就是modifyClass方法了,它的实现也很简单,这里除了ClassVisitor需要定义之外其他的都是固定写法
private static byte[] modifyClass(byte[] srcClass) throws IOException {
/**
* ClassWriter.COMPUTE_MAXS:Flag to automatically compute the maximum stack size and the maximum
* number of local variables of methods.
*/
ClassWriter classWriter = new ClassWriter(ClassWriter.COMPUTE_MAXS)
ClassVisitor classVisitor = new SensorsAnalyticsClassVisitor(classWriter)
//解析源字节码
ClassReader cr = new ClassReader(srcClass)
cr.accept(classVisitor, ClassReader.EXPAND_FRAMES)
return classWriter.toByteArray()
}
在classVisitor中我们才开始真正的操作选定的类,它会首先执行visit方法,在这里我们会得到类的名字、修饰符(desc)、所实现的接口,父类的名字等等。然后会遍历类的变量和方法,这里重点说遍历方法,遍历到方法时会首先调用visitAnnotation(s,b)方法,在这里能得到注解类的修饰符(desc:)和是否运行时可见比如我定义了一个注解

class MyAnnotationVisitor extends AnnotationVisitor {
public String typeName
public String id
MyAnnotationVisitor(AnnotationVisitor av) {
super(Opcodes.ASM6, av)
}
/**
* 读取注解类型的值
* @param name
* @param value
*/
@Override
void visit(String name, Object value) {
super.visit(name, value)
if (name == "value")
typeName = value
else if (name == "id") {
id = value
}
}
/**
* 注解枚举类型的值
* @param name
* @param desc
* @param value
*/
@Override
void visitEnum(String name, String desc, String value) {
super.visitEnum(name, desc, value)
}
@Override
AnnotationVisitor visitAnnotation(String name, String desc) {
return super.visitAnnotation(name, desc)
}
/**
* 注解数组类型的值
* @param name
* @return
*/
@Override
AnnotationVisitor visitArray(String name) {
return super.visitArray(name)
}
@Override
void visitEnd() {
super.visitEnd()
}
}
从上面的代码我们可以看出如果想要获取之前定义的value的值,直接在visit方法中就可以获得。接下来我们一般会关注方法的结束和开始,比如这里在结束时打点,实现onMethodExit方法
@Override
protected void onMethodExit(int opcode) {
super.onMethodExit(opcode)
if (mInterfaces != null && mInterfaces.length > 0) {
if ((mInterfaces.contains('android/view/View$OnClickListener') && nameDesc == 'onClick(Landroid/view/View;)V') ||
desc == '(Landroid/view/View;)V') {
methodVisitor.visitVarInsn(ALOAD, 1)
methodVisitor.visitMethodInsn(INVOKESTATIC, SDK_API_CLASS, "trackViewOnClick", "(Landroid/view/View;)V", false)
}
}
}
这里的visitMethodInsn的第一个参数的取值解释如下:
| 指令 | 说明 |
|---|---|
| invokeinterface | 用以调用接口方法,在运行时搜索一个实现了这个接口方法的对象,找出适合的方法进行调用。(Invoke interface method) |
| invokevirtual | 指令用于调用对象的实例方法,根据对象的实际类型进行分派(Invoke instance method; dispatch based on class) |
| invokestatic | 用以调用类方法(Invoke a class (static) method ) |
| invokespecial | 指令用于调用一些需要特殊处理的实例方法,包括实例初始化方法、私有方法和父类方法。(Invoke instance method; special handling for superclass, private, and instance initialization method invocations ) |
| invokedynamic | JDK1.7新加入的一个虚拟机指令,相比于之前的四条指令,他们的分派逻辑都是固化在JVM内部,而invokedynamic则用于处理新的方法分派:它允许应用级别的代码来确定执行哪一个方法调用,只有在调用要执行的时候,才会进行这种判断,从而达到动态语言的支持。(Invoke dynamic method) |
比如下面的例子
public class JVMInstructionTest implements Runnable {
public JVMInstructionTest() {
System.out.println("constructor method");
}
private void s() {
System.out.println("private method");
}
static void print() {
System.out.println("static method");
}
void p() {
System.out.println("instance method");
}
public void d(String str) {
System.out.println("for method handle " + str);
}
static void ddd(String str) {
System.out.println("static method for method handle " + str);
}
public static void main(String[] args) throws Throwable {
/**
* invoke special
*/
JVMInstructionTest test = new JVMInstructionTest();
/**
* invoke special
*/
test.s();
/**
* invoke virtual
*/
test.p();
/**
* invoke static
*/
print();
/**
* invoke interface
*/
Runnable r = new JVMInstructionTest();
r.run();
/**
* Java 8中,lambda表达式和默认方法时,底层会生成和使用invoke dynamic
* invoke dynamic
*/
List<Integer> list = Arrays.asList(1, 2, 3, 4);
list.stream().forEach(System.out::println);
}
@Override
public void run() {
System.out.println("interface method");
}
}
上面onMethodExit方法中的那些ifelse则是用于判断当前类所实现接口的方法,并且调用自己写的类中的相关方法,比如注解方式所调用的方法为
public static void trackViewOnClick(String value,String id) {
try {
JSONObject jsonObject = new JSONObject();
jsonObject.put("$element_type", "Annotation");
jsonObject.put("$element_id", id);
if (!TextUtils.isEmpty(value))
jsonObject.put("$element_content", value);
SensorsDataAPI.getInstance().track("$AppClick", jsonObject);
} catch (Exception e) {
e.printStackTrace();
}
}
这里是将注解的值用于埋点,其他同理。 2.创建plugin,步骤同之前 3.引入插件 4.构建应用实现埋点