什么是字节码插桩

字节码插桩就是在构建的过程中,通过修改已经编译完成的字节码文件,也就是class文件,来实现功能的添加。

简单来讲,我们要实现无埋点对客户端的全量统计。这里的统计概括的范围比较广泛,常见的场景有:

  • 页面(Activity、Fragment)的打开事件
  • 各种点击事件的统计,包括但不限于Click LongClick TouchEvent
  • Debug期需要统计各个方法的耗时。注意这里的方法包括接入的第三方SDK的方法。
  • 待补充

要实现这些功能需要拥有哪些技术点呢?

  • 面向切面编程思想(AOP)
  • Android打包流程
  • 自定义Gradle插件
  • Java字节码
  • 字节码编织(ASM)
  • 结合自己的业务实现统计代码

面向切面编程思想(AOP)

AOP(Aspect Oriented Program)是一种面向切面编程的思想。这种编程思想是相对于OOP(ObjectOriented Programming)来说的。说破天,咱们要实现的功能还是统计嘛,大规模的重复统计行为是典型的AOP使用场景。所以搞懂什么是AOP以及为什么要用AOP变得很重要。

先来说一下大家熟悉的面向对象编程:面向对象的特点是继承、多态和封装。而封装就要求将功能分散到不同的对象中去,这在软件设计中往往称为职责分配。实际上也就是说,让不同的类设计不同的方法。这样代码就分散到一个个的类中去了。这样做的好处是降低了代码的复杂程度,使类可重用。

但是面向对象的编程天生有个缺点就是分散代码的同时,也增加了代码的重复性。比如我希望在项目里面所有的模块都增加日志统计模块,按照OOP的思想,我们需要在各个模块里面都添加统计代码,但是如果按照AOP的思想,可以将统计的地方抽象成切面,只需要在切面里面添加统计代码就OK了。

其实在服务端的领域AOP已经被各路大佬玩的风生水起,例如Spring这类跨时代的框架。我第一次接触AOP就是在学习Spring框架的的时候。最常见实现AOP的方式就是代理。

AOP 是一种编程思想,但是它的实现方式有很多,比如:Spring、AspectJ、JavaAssist、ASM 等。由于我是做 Android 开发的,所以会用 Android 中的一些例子。

  • JakeWharton 的 hugo 就是一个典型的应用,其利用了自定义 Gradle 插件 + AspectJ 的方式,将有特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试。
  • 最近在学习 Java 字节码和 ASM 方面的知识,所以也照猫画虎,写了一个TraceLog,实现了和 hugo同样的功能,将特定注解的方法的参数、返回结果和执行时间打印到 Logcat 中,方便开发调试,不过我使用的是 自定义 Gradle 插件 + ASM 的方式。后面会讲。

Android打包流程

详见 android Apk打包过程概述

自定义Gradle插件

详见 Gradle自定义插件

如何使用Transform API

因为是编译期间搞事情,所以首先要在编译期间找一个时间点,这也就是本节 Transform 的内容,找到“作案”地点后,接下来就是“作案对象”了,这里选择的是对编译后的 .class 字节码下手,要用到的工具就是后面要介绍的 ASM 了。

上面是官方出品的编译打包签名流程,我们要搞事情的位置就是 Java Compiler 编译成 .class Files 之到打包为 .dex Files 这之间。Google 官方在 Android Gradle 的 1.5.0 版本以后提供了 Transfrom API, 允许第三方自定义插件在打包 dex 文件之前的编译过程中操作 .class 文件,所以这里先要做的就是实现一个自定义的 Transform 进行.class文件遍历拿到所有方法,修改完成对原文件进行替换。

下面说一下如何引入 Transform 依赖,在 Android gradle 插件 1.5 版本以前,是有一个单独的 transform api 的;从 2.0 版本开始,就直接并入到 gradle api 中了。

Gradle 1.5:

Compile ‘com.android.tools.build:transfrom-api:1.5.0’

Gradle 2.0 开始:

implementation 'com.android.tools.build:gradle:3.5.2'

Transform是作用在.class编译后,打包成.dex前,可以对.class和resource进行再处理的部分。为了验证,我们建立一个项目Build的一次。

可以很清楚的看到,原生就带了一系列Transform供使用。那么这些Transform是怎么组织在一起的呢,我们用一张图表示:

每个Transform其实都是一个gradle task,Android编译器中的TaskManager将每个Transform串连起来,第一个Transform接收来自javac编译的结果,以及已经拉取到在本地的第三方依赖(jar. aar),还有resource资源,注意,这里的resource并非android项目中的res资源,而是asset目录下的资源。 这些编译的中间产物,在Transform组成的链条上流动,每个Transform节点可以对class进行处理再传递给下一个Transform。我们常见的混淆,Desugar等逻辑,它们的实现如今都是封装在一个个Transform中,而我们自定义的Transform,会插入到这个Transform链条的最前面。

但其实,上面这幅图,只是展示Transform的其中一种情况。而Transform其实可以有两种输入,一种是消费型的,当前Transform需要将消费型型输出给下一个Transform,另一种是引用型的,当前Transform可以读取这些输入,而不需要输出给下一个Transform,比如Instant Run就是通过这种方式,检查两次编译之间的diff的。

Transform解读

class TraceTransform extends Transform {@OverrideString getName() {return "TraceLog"    }@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS    }@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT    }@Overrideboolean isIncremental() {return true}@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation)......}

我们一项项分析:

(1)

@OverrideString getName() {return "TraceLog"    }

Name顾名思义,就是我们的Transform名称,再回到我们刚刚Build的流程里:

这个最终的名字是如何构成的呢?好像跟我们这边的定义的名字有区别。以transform开头,之后拼接ContentType,这个ContentType代表着这个Transform的输入文件的类型,类型主要有两种,一种是Classes,另一种是Resources,ContentType之间使用And连接,拼接完成后加上With,之后紧跟的就是这个Transform的Name,name在getName()方法中重写返回即可。

(2)

@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS    }

先来看代码注释,注释写的很清晰了,必须是CLASSES(0x01),RESOURCES(0x02)之一,相当于Transform需要处理的类型。

 /*** Returns the type(s) of data that is consumed by the Transform. This may be more than* one type.** <strong>This must be of type {@link QualifiedContent.DefaultContentType}</strong>*/@NonNullpublic abstract Set<ContentType> getInputTypes();----------------------------------/*** The type of of the content.*/enum DefaultContentType implements ContentType {/*** The content is compiled Java code. This can be in a Jar file or in a folder. If* in a folder, it is expected to in sub-folders matching package names.*/CLASSES(0x01),/** The content is standard Java resources. */RESOURCES(0x02);private final int value;DefaultContentType(int value) {this.value = value;}@Overridepublic int getValue() {return value;}}

(3)

@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT    }

先来看源码注释,这个的作用相当于用来Transform表明作用域

 /*** Returns the scope(s) of the Transform. This indicates which scopes the transform consumes.*/@NonNullpublic abstract Set<Scope> getScopes();
开发一共可以选如下几种:/*** The scope of the content.** <p>* This indicates what the content represents, so that Transforms can apply to only part(s)* of the classes or resources that the build manipulates.*/enum Scope implements ScopeType {/** Only the project (module) content */PROJECT(0x01),/** Only the sub-projects (other modules) */SUB_PROJECTS(0x04),/** Only the external libraries */EXTERNAL_LIBRARIES(0x10),/** Code that is being tested by the current variant, including dependencies */TESTED_CODE(0x20),/** Local or remote dependencies that are provided-only */PROVIDED_ONLY(0x40),/*** Only the project's local dependencies (local jars)** @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}*/@Deprecated        PROJECT_LOCAL_DEPS(0x02),/*** Only the sub-projects's local dependencies (local jars).** @deprecated local dependencies are now processed as {@link #EXTERNAL_LIBRARIES}*/@Deprecated        SUB_PROJECTS_LOCAL_DEPS(0x08);

一般来说如果是要处理所有class字节码,Scope我们一般使用TransformManager.SCOPE_FULL_PROJECT。即

public static final Set<Scope> SCOPE_FULL_PROJECT =Sets.immutableEnumSet(Scope.PROJECT,Scope.SUB_PROJECTS,Scope.EXTERNAL_LIBRARIES);

(4)

@Override
boolean isIncremental() {return true
}

增量编译开关。当我们开启增量编译的时候,相当input包含了changed/removed/added三种状态,实际上还有notchanged。需要做的操作如下:

  • NOTCHANGED: 当前文件不需处理,甚至复制操作都不用;
  • ADDED、CHANGED: 正常处理,输出给下一个任务;
  • REMOVED: 移除outputProvider获取路径对应的文件。

(5)

@Override
void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {super.transform(transformInvocation)......
}

先来看一下源码注释,它是Transform处理文件的核心代码:

 /*** Executes the Transform.** <p>The inputs are packaged as an instance of {@link TransformInvocation}* <ul>*     <li>The <var>inputs</var> collection of {@link TransformInput}. These are the inputs*     that are consumed by this Transform. A transformed version of these inputs must*     be written into the output. What is received is controlled through*     {@link #getInputTypes()}, and {@link #getScopes()}.</li>*     <li>The <var>referencedInputs</var> collection of {@link TransformInput}. This is*     for reference only and should be not be transformed. What is received is controlled*     through {@link #getReferencedScopes()}.</li>* </ul>** A transform that does not want to consume anything but instead just wants to see the content* of some inputs should return an empty set in {@link #getScopes()}, and what it wants to* see in {@link #getReferencedScopes()}.** <p>Even though a transform's {@link Transform#isIncremental()} returns true, this method may* be receive <code>false</code> in <var>isIncremental</var>. This can be due to* <ul>*     <li>a change in secondary files ({@link #getSecondaryFiles()},*     {@link #getSecondaryFileOutputs()}, {@link #getSecondaryDirectoryOutputs()})</li>*     <li>a change to a non file input ({@link #getParameterInputs()})</li>*     <li>an unexpected change to the output files/directories. This should not happen unless*     tasks are improperly configured and clobber each other's output.</li>*     <li>a file deletion that the transform mechanism could not match to a previous input.*     This should not happen in most case, except in some cases where dependencies have*     changed.</li>* </ul>* In such an event, when <var>isIncremental</var> is false, the inputs will not have any* incremental change information:* <ul>*     <li>{@link JarInput#getStatus()} will return {@link Status#NOTCHANGED} even though*     the file may be added/changed.</li>*     <li>{@link DirectoryInput#getChangedFiles()} will return an empty map even though*     some files may be added/changed.</li>* </ul>** @param transformInvocation the invocation object containing the transform inputs.* @throws IOException if an IO error occurs.* @throws InterruptedException* @throws TransformException Generic exception encapsulating the cause.*/public void transform(@NonNull TransformInvocation transformInvocation)throws TransformException, InterruptedException, IOException {// Just delegate to old method, for code that uses the old API.//noinspection deprecationtransform(transformInvocation.getContext(), transformInvocation.getInputs(),transformInvocation.getReferencedInputs(),transformInvocation.getOutputProvider(),transformInvocation.isIncremental());}

大致意思如下,具体大家一定要仔细看注释:

  • 如果拿取了getInputs()的输入进行消费,则transform后必须再输出给下一级
  • 如果拿取了getReferencedInputs()的输入,则不应该被transform。
  • 是否增量编译要以transformInvocation.isIncremental()为准。

在 transform 方法中主要做的事情就是把 Inputs 保存到 outProvider 提供的位置去。生成的位置见下图:

后面会讲到代码,主要有两个 transform 方法,一个 transformJar 就是简单的拷贝,另一个 transformDirectory,我们就是在这里用 ASM 对字节码进行修改的。

Transform注册和使用

在gradle插件中注册

class TracePlugin implements Plugin<Project>{@Overridevoid apply(Project project) {println "------trace plugin begin-------"def android = project.extensions.findByType(AppExtension.class)android.registerTransform(new TraceTransform(project))println "------trace plugin end-------"}}

参考 Transform详解

Java字节码

详见 《深入理解Java虚拟机》第6章 类文件结构

Java字节码编织框架——ASM

什么是ASM?

ASM 是一个 Java 字节码操控框架。它能被用来动态生成类或者增强既有类的功能。ASM 可以直接产生二进制 class 文件,也可以在类被加载入 Java 虚拟机之前动态改变类行为。Java class 被存储在严格格式定义的 .class 文件里,这些类文件拥有足够的元数据来解析类中的所有元素:类名称、方法、属性以及 Java 字节码(指令)。ASM 从类文件中读入信息后,能够改变类行为,分析类信息,甚至能够根据用户要求生成新类。

为什么选择ASM来进行字节码编织?

有前人做了实验。参考网易乐得团队的实验结果:

通过上表可见,ASM的效率更高。不过效率高的前提是该库的语法更接近字节码层面。所以上面的虚拟机相关知识显得更加重要。

ASM 库是一款基于 Java 字节码层面的代码分析和修改工具。ASM 可以直接生产二进制的 class 文件,也可以在类被加载入 JVM 之前动态修改类行为。
ASM 库的结构如下所示:

  • Core:为其他包提供基础的读、写、转化Java字节码和定义的API,并且可以生成Java字节码和实现大部分字节码的转换,在 访问者模式和
    ASM 中介绍的几个重要的类就在 Core API 中:ClassReader、ClassVisitor 和 ClassWriter 类.
  • Tree:提供了 Java 字节码在内存中的表现
  • Commons:提供了一些常用的简化字节码生成、转换的类和适配器
  • Util:包含一些帮助类和简单的字节码修改类,有利于在开发或者测试中使用
  • XML:提供一个适配器将XML和SAX-comliant转化成字节码结构,可以允许使用XSLT去定义字节码转化

Core API 介绍

(1)ClassVisitor 抽象类

如下所示,在 ClassVisitor 中提供了和类结构同名的一些方法,这些方法会对类中相应的部分进行操作,而且是有顺序的:visit [ visitSource ] [ visitOuterClass ] ( visitAnnotation | visitAttribute )* (visitInnerClass | visitField | visitMethod )* visitEnd
public abstract class ClassVisitor {

    ......public void visit(int version, int access, String name, String signature, String superName, String[] interfaces);
public void visitSource(String source, String debug);
public void visitOuterClass(String owner, String name, String desc);
public AnnotationVisitor visitAnnotation(String desc, boolean visible);
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String desc, boolean visible);
public void visitAttribute(Attribute attr);
public void visitInnerClass(String name, String outerName, String innerName, int access);
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value);
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions);
public void visitEnd();}
  • void visit(int version, int access, String name, String signature, String superName, String[] interfaces) 该方法是当扫描类时第一个调用的方法,主要用于类声明使用。下面是对方法中各个参数的示意:visit( 类版本 , 修饰符 , 类名 ,泛型信息 , 继承的父类 , 实现的接口)
  • AnnotationVisitor visitAnnotation(String desc, boolean visible) 该方法是当扫描器扫描到类注解声明时进行调用。下面是对方法中各个参数的示意:visitAnnotation(注解类型 , 注解是否可以在JVM 中可见)。
  • FieldVisitor visitField(int access, String name, String desc, String signature, Object value) 该方法是当扫描器扫描到类中字段时进行调用。下面是对方法中各个参数的示意:visitField(修饰符 , 字段名 , 字段类型 , 泛型描述 , 默认值)
  • MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) 该方法是当扫描器扫描到类的方法时进行调用。下面是对方法中各个参数的示意:visitMethod(修饰符 , 方法名 , 方法签名 ,泛型信息 , 抛出的异常)
  • void visitEnd() 该方法是当扫描器完成类扫描时才会调用,如果想在类中追加某些方法

(2)ClassReader 类

这个类会将 .class 文件读入到 ClassReader 中的字节数组中,它的 accept 方法接受一个 ClassVisitor 实现类,并按照顺序调用 ClassVisitor 中的方法。

(3)ClassWriter 类

ClassWriter 是一个 ClassVisitor 的子类,是和 ClassReader 对应的类,ClassReader 是将 .class 文件读入到一个字节数组中,ClassWriter 是将修改后的类的字节码内容以字节数组的形式输出。

(4)MethodVisitor & AdviceAdapter

MethodVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Method 时就转入 MethodVisitor 接口处理。
AdviceAdapter 是 MethodVisitor 的子类,使用 AdviceAdapter 可以更方便的修改方法的字节码。其中比较重要的几个方法如下:

  • void visitCode():表示 ASM 开始扫描这个方法
  • void onMethodEnter():进入这个方法
  • void onMethodExit():即将从这个方法出去
  • void onVisitEnd():表示方法扫码完毕

(5)FieldVisitor 抽象类

FieldVisitor 是一个抽象类,当 ASM 的 ClassReader 读取到 Field 时就转入 FieldVisitor 接口处理。和分析 MethodVisitor 的方法一样,也可以查看源码注释进行学习,这里不再详细介绍。

操作流程

  1. 需要创建一个 ClassReader 对象,将 .class 文件的内容读入到一个字节数组中
  2. 然后需要一个 ClassWriter 的对象将操作之后的字节码的字节数组回写
  3. 需要事件过滤器 ClassVisitor。在调用 ClassVisitor 的某些方法时会产生一个新的 XXXVisitor
    对象,当我们需要修改对应的内容时只要实现自己的 XXXVisitor 并返回就可以了
input.directoryInputs.each { DirectoryInput directoryInput ->if (directoryInput.file.isDirectory()) {directoryInput.file.eachFileRecurse { File file ->def name = file.nameif (name.endsWith(".class") && !(name == ("R.class"))&& !name.startsWith("R\$") && !(name == ("BuildConfig.class"))) {ClassReader reader = new ClassReader(file.bytes)ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)ClassVisitor visitor = new TraceVisitor(writer)reader.accept(visitor, ClassReader.EXPAND_FRAMES)byte[] code = writer.toByteArray()def classPath = file.parentFile.absolutePath + File.separator + nameFileOutputStream fos = new FileOutputStream(classPath)fos.write(code)fos.close()}}}

这个库也没什么可展开描述的,值得参考的资源:
AOP 的利器:ASM 3.0 介绍
ASM 库的介绍和使用

虽然有了ASM这种框架,可以很方便的修改class文件,但是如果不熟悉框架的使用,写起来还是有点吃力
人类总是懒惰的,试图找出一些捷径,于是有了一款Idea插件——ASM Bytecode Outline

ASM Bytecode Outline

详见 【我的Android进阶之旅】Android Studio 使用 ASM Bytecode Outline 插件来研究Java字节码
插件ASM Bytecode Outline,可以把java代码转为ASM框架的代码,那么我们可以先修改好一个类的代码,把代码转为ASM框架的代码,然后把需要的代码复制到插件中,这样就可以在自定义的gradle plugin中批量自动去修改目标类了。

TraceLog

使用自定义 Gradle 插件 + ASM 的方式实现了和 JakeWharton 的 hugo 库同样的功能的库,将特定注解的方法的传入参数、返回结果和执行时间打印到 Logcat 中,方便开发调试。

整个工程分3个模块,主模块是调用方,就是使用@TraceLog的地方。plugin模块是自定义的gradle插件。因为打印日志和业务无关性,这里把打印日志的功能单独拆分成一个模块tracelibrary。plugin模块依赖traceLibrary,在字节码插桩时调用traceLibrary里面的方法打印日志。

自定义gradle plugin

build.gradle

apply plugin: 'groovy'
//使用该插件,才能使用uploadArchives
apply plugin: 'maven'repositories {jcenter()
}dependencies {//使用gradle sdkcompile gradleApi()//使用groovy sdkcompile localGroovy()implementation 'com.android.tools.build:gradle:3.5.2'implementation 'org.ow2.asm:asm-all:5.2'
}sourceCompatibility = "1.8"
targetCompatibility = "1.8"uploadArchives {repositories.mavenDeployer {pom.version = '1.0.0'pom.artifactId = 'tracePlugin'pom.groupId = 'com.example.watson.plugin'repository(url: "file:///D:/repository/")}
}

TracePlugin.groovy

class TracePlugin implements Plugin<Project>{@Overridevoid apply(Project project) {println "------trace plugin begin-------"def android = project.extensions.findByType(AppExtension.class)android.registerTransform(new TraceTransform(project))println "------trace plugin end-------"}}

TraceTransform.groovy

class TraceTransform extends Transform {Project projectTraceTransform(Project project) {this.project = project}@OverrideString getName() {return "TraceLog"}@OverrideSet<QualifiedContent.ContentType> getInputTypes() {return TransformManager.CONTENT_CLASS}@OverrideSet<? super QualifiedContent.Scope> getScopes() {return TransformManager.SCOPE_FULL_PROJECT}@Overrideboolean isIncremental() {return false}@Overridevoid transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {transformInvocation.inputs.each { TransformInput input ->input.directoryInputs.each { DirectoryInput directoryInput ->if (directoryInput.file.isDirectory()) {directoryInput.file.eachFileRecurse { File file ->def name = file.nameif (name.endsWith(".class") && !(name == ("R.class"))&& !name.startsWith("R\$") && !(name == ("BuildConfig.class"))) {ClassReader reader = new ClassReader(file.bytes)ClassWriter writer = new ClassWriter(reader, ClassWriter.COMPUTE_MAXS)ClassVisitor visitor = new TraceVisitor(writer)reader.accept(visitor, ClassReader.EXPAND_FRAMES)byte[] code = writer.toByteArray()def classPath = file.parentFile.absolutePath + File.separator + nameFileOutputStream fos = new FileOutputStream(classPath)fos.write(code)fos.close()}}}def dest = transformInvocation.outputProvider.getContentLocation(directoryInput.name,directoryInput.contentTypes, directoryInput.scopes,Format.DIRECTORY)FileUtils.copyDirectory(directoryInput.file, dest)}input.jarInputs.each { JarInput jarInput ->def jarName = jarInput.namedef md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())if (jarName.endsWith(".jar")) {jarName = jarName.substring(0, jarName.length() - 4)}def dest = transformInvocation.outputProvider.getContentLocation(jarName + md5Name,jarInput.contentTypes, jarInput.scopes, Format.JAR)FileUtils.copyFile(jarInput.file, dest)}}}
}

TraceVisitor.groovy

class TraceVisitor extends ClassVisitor {private String mClassNameTraceVisitor(ClassVisitor classVisitor) {super(Opcodes.ASM5, classVisitor)}@Overridevoid visit(int version, int access, String name, String signature, String superName, String[] interfaces) {super.visit(version, access, name, signature, superName, interfaces);this.mClassName = name}@OverrideMethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {MethodVisitor methodVisitor = super.visitMethod(access, name, desc, signature, exceptions)methodVisitor = new TraceMethodVisitor(Opcodes.ASM5, methodVisitor, access, mClassName, name, desc)return methodVisitor}
}

TraceMethodVisitor.groovy

class TraceMethodVisitor extends AdviceAdapter {private static final String COST_ANNOTATION_DESC = "Lcom/example/tracelibrary/TraceLog;"private boolean isInjected = falseprivate int startTimeIdprivate int methodIdprivate String classNameprivate String methodNameprivate String descprivate boolean isStaticMethodprivate Type[] argumentArraysTraceMethodVisitor(int api, MethodVisitor mv, int access, String className, String methodName, String desc) {super(api, mv, access, methodName, desc)this.className = classNamethis.methodName = methodNamethis.desc = descargumentArrays = Type.getArgumentTypes(desc)isStaticMethod = ((access & Opcodes.ACC_STATIC) != 0)}@OverrideAnnotationVisitor visitAnnotation(String desc, boolean visible) {if (COST_ANNOTATION_DESC.equals(desc)) {isInjected = true}return super.visitAnnotation(desc, visible)}@Overrideprotected void onMethodEnter() {if (isInjected) {methodId = newLocal(Type.INT_TYPE)mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "request", "()I", false)mv.visitIntInsn(ISTORE, methodId)for (int i = 0; i < argumentArrays.length; i++) {Type type = argumentArrays[i]int index = isStaticMethod ? i : (i + 1)switch (type.getSort()) {case Type.BOOLEAN:case Type.CHAR:case Type.BYTE:case Type.SHORT:case Type.INT:mv.visitVarInsn(ILOAD, index)box(type)breakcase Type.FLOAT:mv.visitVarInsn(FLOAD, index)box(type)breakcase Type.LONG:mv.visitVarInsn(LLOAD, index)box(type)breakcase Type.DOUBLE:mv.visitVarInsn(DLOAD, index)box(type)breakcase Type.ARRAY:case Type.OBJECT:mv.visitVarInsn(ALOAD, index)box(type)break}mv.visitVarInsn(ILOAD, methodId)visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "addMethodArgument","(Ljava/lang/Object;I)V", false)}startTimeId = newLocal(Type.LONG_TYPE)mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false)mv.visitIntInsn(LSTORE, startTimeId)}}@Overrideprotected void onMethodExit(int opcode) {if (isInjected) {if (opcode == RETURN) {visitInsn(ACONST_NULL)} else if (opcode == ARETURN || opcode == ATHROW) {dup()} else {if (opcode == LRETURN || opcode == DRETURN) {dup2()} else {dup()}box(Type.getReturnType(this.methodDesc))}mv.visitLdcInsn(className)mv.visitLdcInsn(methodName)mv.visitLdcInsn(desc)mv.visitVarInsn(LLOAD, startTimeId)mv.visitVarInsn(ILOAD, methodId)mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache", "updateMethodInfo","(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;JI)V", false)mv.visitVarInsn(ILOAD, methodId)mv.visitMethodInsn(INVOKESTATIC, "com/example/tracelibrary/core/MethodCache","printMethodInfo", "(I)V", false)}}
}

第三方库文件

看到,自定义gradle插件的TraceMethodVisitor会在方法执行前后织入需要的功能,这些功能就是第三方库的内容。

build.gradle

apply plugin: 'com.android.library'
//使用该插件,才能使用uploadArchives
apply plugin: 'maven'android {compileSdkVersion 29buildToolsVersion "29.0.2"defaultConfig {minSdkVersion 15targetSdkVersion 29versionCode 1versionName "1.0"testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"consumerProguardFiles 'consumer-rules.pro'}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}}dependencies {compile 'org.ow2.asm:asm-all:5.2'
}uploadArchives {repositories.mavenDeployer {pom.version = '1.0.0'pom.artifactId = 'traceLibrary'pom.groupId = 'com.example.watson.library'repository(url: "file:///D:/repository/")}
}

MethodCache.java

public class MethodCache {/*** 方法缓存默认大小*/private static final int INIT_CACHE_SIZE = 1024;/*** 方法名缓存*/private static Vector<MethodInfo> mCacheMethods = new Vector<>(INIT_CACHE_SIZE);/*** 占位并生成方法ID** @return 返回 方法 Id*/public static int request() {mCacheMethods.add(new MethodInfo());return mCacheMethods.size() - 1;}public static void addMethodArgument(Object argument, int id) {MethodInfo methodInfo = mCacheMethods.get(id);methodInfo.addArgument(argument);}public static void updateMethodInfo(Object result, String className, String methodName, String methodDesc, long startTime, int id) {MethodInfo methodInfo = mCacheMethods.get(id);methodInfo.setCost((System.currentTimeMillis() - startTime));methodInfo.setResult(result);methodInfo.setMethodDesc(methodDesc);methodInfo.setClassName(className);methodInfo.setMethodName(methodName);}public static void printMethodInfo(int id) {MethodInfo methodInfo = mCacheMethods.get(id);Printer.printMethodInfo(methodInfo);}
}

MethodInfo.java

public class MethodInfo {private static final String OUTPUT_FORMAT = "The method's name is %s ,the cost is %dms and the result is ";private String mClassName;              // 类名private String mMethodName;             // 方法名private String mMethodDesc;             // 方法描述符private Object mResult;                 // 方法执行结果private long mCost;                     // 方法执行耗时private List<Object> mArgumentList;     // 方法参数列表MethodInfo() {mArgumentList = new ArrayList<>();}@Overridepublic String toString() {return String.format(Locale.CHINA, OUTPUT_FORMAT, getMethodName(), mCost) + mResult;}/*** @param className 设置类名*/public void setClassName(String className) {mClassName = className;}/*** @return 返回类名*/public String getClassName() {mClassName = mClassName.replace("/", ".");return mClassName;}/*** @param methodName 设置方法名*/public void setMethodName(String methodName) {mMethodName = methodName;}/*** @return 返回方法名*/public String getMethodName() {StringBuilder msg = new StringBuilder();Type[] argumentTypes = Type.getArgumentTypes(mMethodDesc);msg.append('(');for (int i = 0; i < argumentTypes.length; i++) {msg.append(argumentTypes[i].getClassName());if (i != argumentTypes.length - 1) {msg.append(", ");}}msg.append(')');mMethodName = mMethodName + msg.toString();return mMethodName;}/*** @param cost 设置方法执行耗时*/public void setCost(long cost) {this.mCost = cost;}/*** @return 返回方法执行耗时*/public long getCost() {return mCost;}/*** @param result 设置方法执行结果*/public void setResult(Object result) {this.mResult = result;}/*** @return 返回方法执行结果*/public Object getResult() {return mResult;}/*** @param methodDesc 设置方法描述符*/public void setMethodDesc(String methodDesc) {this.mMethodDesc = methodDesc;}/*** 添加方法参数** @param argument 方法参数*/public void addArgument(Object argument) {mArgumentList.add(argument);}/*** @return 得到方法参数列表*/public List<Object> getArgumentList() {return mArgumentList;}
}

Printer.java

public class Printer {private static final String TAG = "TraceLog";private static final char TOP_LEFT_CORNER = '┌';private static final char BOTTOM_LEFT_CORNER = '└';private static final char HORIZONTAL_LINE = '│';private static final String DOUBLE_DIVIDER = "───────────────────────────────────------";private static final String TOP_BORDER = TOP_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;private static final String BOTTOM_BORDER = BOTTOM_LEFT_CORNER + DOUBLE_DIVIDER + DOUBLE_DIVIDER;private static final String CLASS_NAME_FORMAT = "%s The class's name: %s";private static final String METHOD_NAME_FORMAT = "%s The method's name: %s";private static final String ARGUMENT_FORMAT = "%s The arguments: ";private static final String RESULT_FORMAT = "%s The result: ";private static final String COST_TIME_FORMAT = "%s The cost time: %dms";public static void printMethodInfo(MethodInfo methodInfo) {Log.i(String.valueOf(0) + TAG, TOP_BORDER);Log.i(String.valueOf(1) + TAG, String.format(CLASS_NAME_FORMAT, HORIZONTAL_LINE, methodInfo.getClassName()));Log.i(String.valueOf(2) + TAG, String.format(METHOD_NAME_FORMAT, HORIZONTAL_LINE, methodInfo.getMethodName()));Log.i(String.valueOf(3) + TAG, String.format(ARGUMENT_FORMAT, HORIZONTAL_LINE) + methodInfo.getArgumentList());Log.i(String.valueOf(4) + TAG, String.format(RESULT_FORMAT, HORIZONTAL_LINE) + methodInfo.getResult());Log.i(String.valueOf(5) + TAG, String.format(Locale.CHINA, COST_TIME_FORMAT, HORIZONTAL_LINE, methodInfo.getCost()));Log.i(String.valueOf(6) + TAG, BOTTOM_BORDER);}
}

最后是注解的定义:

@Target(ElementType.METHOD)
public @interface TraceLog {
}

主Module

主Module是使用方,使用方式:

(1)项目工程的gradle.build添加gradle编译脚本依赖:

buildscript {repositories {maven {url uri('D:/repository')}}dependencies {classpath 'com.example.watson.plugin:tracePlugin:1.0.0'}
}

这里我使用的仓库是本地文件夹,以后可以上传服务器,做到远程依赖

(2)在需要使用的 module 中的 build.gradle 中应用插件:

apply plugin: com.example.watson.plugin.TracePlugin

同时添加第三方库依赖,这里同样使用的仓库是本地文件夹,以后可以上传服务器,做到远程依赖

repositories {maven {url uri('D:/repository')}
}
...
implementation 'com.example.watson.library:traceLibrary:1.0.0'

(3)添加注解

在需要被hook的方法上添加@TraceLog注解

public class MainActivity extends AppCompatActivity {private static final String TAG = "MainActivity";@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_main);findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {printPerson(new Person(66, "watson"), 100, true, (byte) 0, 'W');}});}@TraceLogprivate Person printPerson(Person person, int x, boolean flag, byte time, char temp) {Log.i(TAG, "flag is " + flag);Log.i(TAG, "time is " + time);Log.i(TAG, "temp is " + temp);person.setName("jack");person.setAge(x);try {Thread.sleep(1000);} catch (Exception e) {e.printStackTrace();}return person;}
}

在编译后,我们可以找到被织入代码后的类文件方法:

D:\project\plugin-master\app\build\intermediates\transforms\TraceLog\debug\28\com\example\watson\MainActivity.class

@TraceLog
private Person printPerson(Person person, int x, boolean flag, byte time, char temp) {int var6 = MethodCache.request();MethodCache.addMethodArgument(person, var6);MethodCache.addMethodArgument(new Integer(x), var6);MethodCache.addMethodArgument(new Boolean(flag), var6);MethodCache.addMethodArgument(new Byte(time), var6);MethodCache.addMethodArgument(new Character(temp), var6);long var7 = System.currentTimeMillis();Log.i("MainActivity", "flag is " + flag);Log.i("MainActivity", "time is " + time);Log.i("MainActivity", "temp is " + temp);person.setName("jack");person.setAge(x);try {Thread.sleep(1000L);} catch (Exception var10) {var10.printStackTrace();}MethodCache.updateMethodInfo(person, "com/example/watson/MainActivity", "printPerson", "(Lcom/example/watson/Person;IZBC)Lcom/example/watson/Person;", var7, var6);MethodCache.printMethodInfo(var6);return person;
}

点击按钮,测试结果:

DEMO下载地址

参考:
Android字节码插桩——详细讲解 附带Demo
从 Java 字节码到 ASM 实践

Android字节码插桩相关推荐

  1. Android 字节码插桩全流程解析

    在Android进阶宝典 – Handler应用于线上卡顿监控中,我简单介绍了一下关于ASM实现字节码插桩来实现方法耗时的监控,但是当时只是找了一个特定的class文件,针对某个特定的方法进行插桩,但 ...

  2. 关于android字节码插桩

    转自:https://www.jianshu.com/p/c202853059b4 基于字节码插桩可以实现面向切面的编程, 实际是在字节码中插入要执行的相关程序. 通过非侵入的方式实现切面编程. (1 ...

  3. 【字节码插桩】Android 打包流程 | Android 中的字节码操作方式 | AOP 面向切面编程 | APT 编译时技术

    文章目录 一.Android 中的 Java 源码打包流程 1.Java 源码打包流程 2.字符串常量池 二.Android 中的字节码操作方式 一.Android 中的 Java 源码打包流程 Ja ...

  4. Android AOP之字节码插桩

    背景   本篇文章基于<网易乐得无埋点数据收集SDK>总结而成,关于网易乐得无埋点数据采集SDK的功能介绍以及技术总结后续会有文章进行阐述,本篇单讲SDK中用到的Android端AOP的实 ...

  5. Android程序员的硬通货——ASM字节码插桩

    作者:享学课堂Lance老师 转载请声明出处! 一.什么是插桩 QQ空间曾经发布的<热修复解决方案>中利用 Javaassist库实现向类的构造函数中插入一段代码解决 CLASS_ISPR ...

  6. 【字节码插桩】AOP 技术 ( “字节码插桩“ 技术简介 | AspectJ 插桩工具 | ASM 插桩工具 )

    文章目录 一." 字节码插桩 " 技术简介 二.AspectJ 插桩工具 三.ASM 插桩工具 一." 字节码插桩 " 技术简介 性能优化 , 插件化 , 热修 ...

  7. 字节码插桩(javassist)之插入代码块|IOC框架(Hilt)之对象注入~研究

    Hilt对象注入 | javassist插桩 研究 Hilt对象注入 javassist字节码插桩 创建buildSrc的module 重写Transform 熟悉TransformInvocatio ...

  8. 看完这一篇,你也可以自如地掌握字节码插桩

    /   今日科技快讯   / 近日,一些国家的黑客频繁对俄罗斯发动网络攻击,以阻止它们正常运行.未来几天,俄罗斯可能与全球互联网断开.针对网络威胁,俄罗斯政府准备启动自己的"大局域网&quo ...

  9. 字节码插桩(四): AST

    我们通过 AndroidStudio 生成Bean对象一般是通过注解来实现自动生成getter/setter方法.equals()和hashCode()方法,其中类(或接口)要符合驼式命名法,首字母大 ...

最新文章

  1. ASP.NET XML读取、增加、修改和删除操作
  2. pygame从入门到提高(2)-平铺背景
  3. sql日期相关函数的使用方法
  4. unlegal android,百度地图定位 Cordova 插件 cordova-plugin-baidumaplocation
  5. 使用笛卡尔积 cross join解决傻傻的问题
  6. redis 队列_Redis系列5实现简单消息队列
  7. android 什么是9.png
  8. [转]最常用的15大Eclipse开发快捷键技巧
  9. 了解linux常用的命令,常用的linux命令(1)-了解常用命令
  10. __proto__和prototype 1
  11. 如何将Oracle卸载干净
  12. 一个非常好的学习方法总结
  13. 怎么将CAD图纸转化为PDF格式呢?教你两个妙招搞定!
  14. 谷粒学苑 —— 3、后台系统前端项目创建
  15. 微信手写板 android,微信小程序:手写板功能实现(canvas)
  16. Base64 编码整理
  17. 网吧模式一台服务器拖显示器,摆500台机器太傻 网咖显示器如何配置?
  18. 有中国电信手机一定要看。CTWAP和CTNET是什么意思?有什么区别?
  19. 系统时不变性与因果性的判断总结
  20. it企业实习_it公司实习心得体会

热门文章

  1. centos7 pe系统安装_如何用U盘安装CentOS7系统
  2. 外卖领券CPS的可玩性探索
  3. Spring4 实战笔记(3):面向切面编程
  4. winserver修改计算机用户名,windows10系统更改账户名称的方法
  5. 接线端子01——常见接线端子介绍
  6. dvcs-ripper的安装使用
  7. 数学课本五大奇人【zhuan】
  8. 三字棋Java程序设计_六子棋Java程序设计.docx
  9. 2018------书籍电影和音乐
  10. 组成原理-lab1难点之流水线