前面我们了解了自定义插件的基础流程
我们现在利用ASM字节码框架在每个方法里面自动插入计时方法
引入依赖库
implementation 'org.ow2.asm:asm:7.1'
implementation 'org.ow2.asm:asm-commons:7.1'
自定义Transform
class AmsTransform : Transform() {
//该Transform最终会生成一个Task,这个名字就是Task名字
override fun getName(): String {
return "AmsTransform"
}
/**
* 指定 Transform 处理的数据,
* CONTENT_CLASS 表示处理 java class 文件,
* CONTENT_RESOURCES, 表示处理 java 的资源
*/
override fun getInputTypes(): MutableSet {
return mutableSetOf(QualifiedContent.DefaultContentType.CLASSES)
}
// 是否增量编译
override fun isIncremental(): Boolean {
return true
}
/**
* Transform 要操作的内容范围
* 1.PROJECT 只有项目内容
* 2.SUB_PROJECTS 只有子项目内容
* 3.EXTERNAL_LIBRARIES 只有外部库
* 4.TESTED_CODE 当前变量(包括依赖项)测试的代码
* 5.PROVIDED_ONLY 本地或者员村依赖项
*/
override fun getScopes(): MutableSet {
return mutableSetOf(QualifiedContent.Scope.PROJECT)
}
}
处理的方法在transform,主要参数有input.directoryInputs :表示项目源码类型,input.jarInputs:表示是jar包类型,由于我们在项目代码中插入,因此主要看handleDirecotoryInput方法
@Throws(IOException::class, TransformException::class, InterruptedException::class)
override fun transform(transformInvocation: TransformInvocation?) {
super.transform(transformInvocation)
println("**************transform start******************")
transformInvocation?.let {
it.outputProvider.deleteAll()
val inputs = it.inputs
inputs.forEach { input ->
//遍历文件目录
input.directoryInputs.forEach { directoryInput ->
handleDirecotoryInput(directoryInput,it.outputProvider)
}
//遍历jar
input.jarInputs.forEach { jarInput ->
val dest = it.outputProvider.getContentLocation(jarInput.name,
jarInput.contentTypes, jarInput.scopes, Format.JAR)
}
}
}
println("**************transform end******************")
}
由于我们不处理外部jar文件,因此jar的就直接copy过去
@Throws(IOException::class)
private fun transformJar(srcJar: File, destJar: File, status: Status) {
logger.warn("srcJar:" + srcJar.absolutePath)
logger.warn("destJar:" + destJar.absolutePath)
if (!destJar.parentFile.exists()) {
destJar.parentFile.mkdirs()
}
FileUtils.copyFile(srcJar, destJar)
}
AmsTransform #handleDirecotoryInput,dest 是输出文件路径,isIncremental是判断当前是否是增量编译,如果是增量,则获取改变的文件的状态。如果有新增的,或者是修改的,则重新处理。最后调用transformSingleFile方法处理。如果不是增量,则调用transformDir获取所有文件,最后也是调用transformSingleFile
@Throws(IOException::class)
private fun handleDirecotoryInput(directoryInput: DirectoryInput, outputProvider: TransformOutputProvider) {
//输出路径
val dest = outputProvider.getContentLocation(
directoryInput.name,
directoryInput.contentTypes, directoryInput.scopes,
Format.DIRECTORY
)
FileUtils.forceMkdir(dest)
if (isIncremental) {
val srcDirPath = directoryInput.file.absolutePath
val destDirPath = dest.absolutePath
val fileStatusMap = directoryInput.changedFiles
for ((inputFile, status) in fileStatusMap) {
val destFilePath = inputFile.absolutePath.replace(srcDirPath, destDirPath)
val destFile = File(destFilePath)
logger.warn("destFilePath:$destFilePath")
when (status) {
Status.NOTCHANGED -> {
}
Status.REMOVED -> if (destFile.exists()) {
destFile.delete()
}
Status.ADDED, Status.CHANGED -> {
try {
FileUtils.touch(destFile)
} catch (e: IOException) {
//maybe mkdirs fail for some strange reason, try again.
// FileUtils.forceMkdirParent(destFile);
}
//处理单个文件
transformSingleFile(inputFile, destFile, srcDirPath)
}
}
}
} else {
transformDir(directoryInput.file, dest)
}
}
AmsTransform #transformSingleFile,判断如果是class类型文件,则调用traceClass方法。其他文件则不处理。不然编译会出错
@Throws(IOException::class)
private fun transformSingleFile(inputFile: File, outputFile: File, srcBaseDir: String) {
logger.warn("inputFile.getName():" + inputFile.name)
if (inputFile.name.endsWith("class")) {
traceClass(inputFile, outputFile)
} else {
FileUtils.copyFile(inputFile, outputFile)
}
}
AmsTransform #traceClass。主要的处理方法就是 cr.accept(LifecycClassVisitor(cw), ClassReader.EXPAND_FRAMES),而LifecycClassVisitor是什么呢?
//处理文件
fun traceClass(input: File,output :File ){
println("traceClass:"+input.absolutePath)
println("output:"+output.absolutePath)
if(!output.parentFile.exists()){
output.parentFile.mkdirs()
}
val fis = FileInputStream(input)
val cr = ClassReader(fis)
val cw = ClassWriter(ClassWriter.COMPUTE_FRAMES)
//真正处理字节码,LifecycClassVisitor类里面有被处理类的信息
cr.accept(LifecycClassVisitor(cw), ClassReader.EXPAND_FRAMES)
//处理后的字节码
val bytes = cw.toByteArray()
val fos = FileOutputStream(output)
fos.write(bytes)
fos.close()
}
LifecycClassVisitor继承ClassVisitor类。解析class文件时先是调用visit方法。在该方法里,我们可以获取到类的一些信息,比如类名,父类名,接口信息。解析到类方法时会回调visitMethod方法,一般我们都是在方法上处理,因此主要也是关注这个方法。因为构造器方法
class LifecycClassVisitor(cv: ClassVisitor):ClassVisitor(Opcodes.ASM5,cv),Opcodes {
lateinit var className :String
var isHandler = true
//拿到类的信息, 然后对满足条件的类进行过滤
override fun visit(version: Int, access: Int, name: String?, signature: String?, superName: String?, interfaces: Array?) {
super.visit(version, access, name, signature, superName, interfaces)
println("visit $name,superName:$superName")
className = name!!
}
//类的方法信息, 拿到需要修改的方法,然后进行修改操作
override fun visitMethod( access: Int, name: String?, desc: String?, signature: String?, exceptions: Array?): MethodVisitor {
println("visitMethod name:$name,className:${className}")
val mv = cv.visitMethod(access,name,desc,signature,exceptions)
name?.let {
n->
isHandler = (n!=""&&n!="")
}
if (!isHandler){
return mv
}
return TraceMethodVisitor(Opcodes.ASM5,mv,access,name!!,desc!!,className)
}
override fun visitEnd() {
println("$className ....visitEnd")
super.visitEnd()
}
}
ClassVisitor中还有其他方法,其执行顺序为
visit visitSource? visitOuterClass?(visitAnntation | visitAttribut)
(visitInnerClass | visitField | visitMethod)
visitEnd
TraceMethodVisitor类继承AdviceAdapter类。里面有两个方法onMethodEnter和onMethodExit,看名字就知道。方法执行行和方法执行后回调这两个方法。我们要插入的计时方法就在两个方法里面处理。
但是要如何知道要操作的字节码?
首先我们把要插入的代码先写好,比如我们现在要插入计时方法
long start = System.currentTimeMillis ()
//方法的代码执行
//打印的方法
Log.d("“TAG”,(System.currentTimeMillis () -start)+"ms")
如果看对应的字节码?
如果是Kotlin,在as里面的Tools->kotlin->show kotlin bytecode就可以看到对应的字节码。如果是java文件,则要用javap -c class文件路径就可以显示出字节码
每条字节码ASM都有对应的方法
如下方法
long start = System.currentTimeMillis ()
对应的字节码
INVOKESTATIC java/lang/System.currentTimeMillis ()J //调用静态方法
LSTORE 1 //存储在局部变量表中
对于XSTORE N字节码,X表示类型,N表示存储在局部变量表中的 哪个位置。比如上面那个LSTORE 1,表示存储Long,在局部变量表第1个位置,对于成员方法,因为第0个位置是this。但是我们用字节码插桩时不能直接写死。newLocal(Type)方法返回值就是个局部变量表位置的索引值,至于在哪个索引值,ASM框架会自动去计算
class TraceMethodVisitor(api:Int?,
methodVisitor : MethodVisitor?,
access:Int?,
name:String?,
descriptor:String?,val className:String) :AdviceAdapter(api!!, methodVisitor, access!!, name, descriptor){
var timeLocalIndex = 0
/**
/*
* INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 1
*/
override fun onMethodEnter() {
super.onMethodEnter()
println("onMethodEnter name:$name,className:${className}")
//INVOKESTATIC java/lang/System.currentTimeMillis ()J
invokeStatic(Type.getType("Ljava/lang/System;"),
Method("currentTimeMillis","()J"))
timeLocalIndex = newLocal(Type.LONG_TYPE)
//LSTORE
storeLocal(timeLocalIndex, Type.LONG_TYPE)
}
XLOAD N 字节码和XSTORE N一样,只不过store是把数据存储至局部变量表,load是把局部变量表的数据推送至操作数栈顶。
还有注意的就是构造器方法,INVOKESPECIAL java/lang/StringBuilder.
/**
INVOKESTATIC java/lang/System.currentTimeMillis ()J
LSTORE 3
LDC "TAG"
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder. ()V
LLOAD 3
LLOAD 1
LSUB
INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
LDC "ms"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I
POP
RETURN
*/
override fun onMethodExit(opcode: Int) {
super.onMethodExit(opcode)
//INVOKESTATIC java/lang/System.currentTimeMillis ()J
// 调用System.currentTimeMillis()
invokeStatic(Type.getType("Ljava/lang/System;"),
Method("currentTimeMillis","()J"))
val index = newLocal(Type.LONG_TYPE)
//存入局部变量表index 位置
storeLocal(index,Type.LONG_TYPE)
//LDC "TAG"
//TAG字符串推送至栈顶
visitLdcInsn("TAG")
// NEW java/lang/StringBuilder
newInstance(Type.getType("Ljava/lang/StringBuilder;"))
dup()
//INVOKESPECIAL java/lang/StringBuilder. ()V 构造器
invokeConstructor(Type.getType("Ljava/lang/StringBuilder;"),Method("","()V"))
//LLOAD 3
//这里LLOAD就是我们之前存储的index位置,把index位置值推送至栈顶
loadLocal(index,Type.LONG_TYPE)
//LLOAD 1
//把timeLocalIndex位置值推送至栈顶
loadLocal(timeLocalIndex,Type.LONG_TYPE)
//LSUB
//栈顶两个值相减
math(SUB,Type.LONG_TYPE)
// INVOKEVIRTUAL java/lang/StringBuilder.append (J)Ljava/lang/StringBuilder;
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),Method("append","(J)Ljava/lang/StringBuilder;"))
// LDC "ms"
visitLdcInsn("ms")
//INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),Method("append","(Ljava/lang/String;)Ljava/lang/StringBuilder;"))
//INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
invokeVirtual(Type.getType("Ljava/lang/StringBuilder;"),Method("toString","()Ljava/lang/String;"))
//INVOKESTATIC android/util/Log.d (Ljava/lang/String;Ljava/lang/String;)I
invokeStatic(Type.getType("Landroid/util/Log;"),Method("d","(Ljava/lang/String;Ljava/lang/String;)I"))
//POP
pop()
}
}
添加插件,编译完在app->build->intermediates->transform->AmsTransform目录下面的class文件里面就可以方法已经被插入了计时代码
Java字节码指令大全
深入探索编译插桩技术(四、ASM 探秘)
Hunter