美文网首页APM框架分析
Argus-apm-gradle-asm 插件

Argus-apm-gradle-asm 插件

作者: David_zhou | 来源:发表于2019-11-28 15:19 被阅读0次

ArgusAPMTransform

ArgusAPMTransform这个类继承自Transform。这个类的主要函数如下:

override fun getInputTypes(): Set<QualifiedContent.ContentType> {
    return Sets.immutableEnumSet(QualifiedContent.DefaultContentType.CLASSES)!!
}

override fun isIncremental(): Boolean {
    return true
}

override fun getScopes(): MutableSet<in QualifiedContent.Scope> {
    return TransformManager.SCOPE_FULL_PROJECT
}

override fun transform(transformInvocation: TransformInvocation){...}

getInputTypes() 用于指明Transform的输入类型,可以作为输入过滤的手段 。如果子类没有复写的话,默认返回值等于Transform#getInputTypes()。 isIncremental() 用于表示是否支持增量编译。getScopes用于指明Transform的作用域,具体的类型在TransformManager定义,接下来就是Transform类型task的重点transform,其代码如下:

override fun transform(transformInvocation: TransformInvocation) {
    asmWeaver = ASMWeaver()

    if (!transformInvocation.isIncremental) {
        transformInvocation.outputProvider.deleteAll()
    }

    transformInvocation.inputs.forEach { input ->
        input.directoryInputs.forEach { dirInput ->
            val dest = transformInvocation.outputProvider.getContentLocation(dirInput.name,
                    dirInput.contentTypes, dirInput.scopes,
                    Format.DIRECTORY)
            FileUtils.forceMkdir(dest)
            if (transformInvocation.isIncremental) {
                val srcDirPath = dirInput.file.absolutePath
                val destDirPath = dest.absolutePath
                dirInput.changedFiles.forEach { (file, status) ->
                    val destFilePath = file.absolutePath.replace(srcDirPath, destDirPath)
                    val destFile = File(destFilePath)
                    when (status) {
                        Status.REMOVED -> {
                            FileUtils.deleteQuietly(destFile)
                        }
                        Status.CHANGED -> {
                            FileUtils.deleteQuietly(destFile)
                            asmWeaver.weaveClass(file, destFile)
                        }
                        Status.ADDED -> {
                            asmWeaver.weaveClass(file, destFile)
                        }
                        else -> {
                        }
                    }
                }
            } else {
                dirInput.file.eachFileRecurse { file ->
                    asmWeaver.weaveClass(file, File(file.absolutePath.replace(dirInput.file.absolutePath, dest.absolutePath)))
                }
            }

        }

        input.jarInputs.forEach { jarInput ->
            val dest = transformInvocation.outputProvider.getContentLocation(
                    jarInput.file.getUniqueJarName(),
                    jarInput.contentTypes,
                    jarInput.scopes,
                    Format.JAR)
            if (transformInvocation.isIncremental) {
                val status = jarInput.status
                when (status) {
                    Status.REMOVED -> {
                        FileUtils.deleteQuietly(dest)
                    }
                    Status.CHANGED -> {
                        FileUtils.deleteQuietly(dest)
                        asmWeaver.weaveJar(jarInput.file, dest)
                    }
                    Status.ADDED -> {
                        asmWeaver.weaveJar(jarInput.file, dest)
                    }
                    else -> {
                    }
                }
            } else {
                asmWeaver.weaveJar(jarInput.file, dest)
            }
        }
    }

    asmWeaver.start()
}

首先,初始化一个ASMWeaver对象asmWeaver,这个类主要用来asm插桩,稍后详细介绍。接下来根据是否支持增量,如果不支持,就删除outputProvider中的文件。然后就开始对输入transformInvocation进行遍历,里面的文件类型有目录和jar,需要进行不同的操作。下面先介绍对目录的操作,如果是目录,对于目录的每一项,先获取路径dest后。然后根据dest先创建目录,如果支持增量编译,对每个变动项根据状态进行不同的处理。如果是移除,则直接移除。如果是有修改,则将原有文件移除后,调用asmWeaver的weaveClass()。如果是新增文件,则直接调用asmWeaver的weaveClass()。如果不支持增量编译,则对dirInput的文件进行遍历,对每个文件调用asmWeaver的weaveClass()。上面介绍完了对目录的操作,下面开始介绍对jar的操作。对jar也是对是否支持增量编译进行分别处理,处理流程和对目录文件的大体类似,只是调用的方法变成了asmWeaver的weaveJar()。最后调用asmWeaver的start()。接下来介绍ASMWeaver的weaveClass()和weaveJar()。weaveClass的代码如下:

fun weaveClass(inputFile: File, outputFile: File) {
    taskManager.addTask(object : ITask {
        override fun call(): Any? {
            FileUtils.touch(outputFile)
            val inputStream = FileInputStream(inputFile)
            val bytes = weaveSingleClassToByteArray(inputStream)
            val fos = FileOutputStream(outputFile)
            fos.write(bytes)
            fos.close()
            inputStream.close()
            return null
        }
    })
}

ASMWeaver有个线程池taskManager,weaverCLass主要是向taskManager中添加task.添加的task是根据输入的文件调用weaveSingleClassToByteArray后,获得bytes后将bytes写入到weaveClass()的第二个参数的文件中。weaveSingleClassToByteArray会开始asm的插桩,代码如下:

private fun weaveSingleClassToByteArray(inputStream: InputStream): ByteArray {
    val classReader = ClassReader(inputStream)
    val classWriter = ExtendClassWriter(ClassWriter.COMPUTE_MAXS)
    var classWriterWrapper: ClassVisitor = classWriter

    if (PluginConfig.argusApmConfig().funcEnabled) {
        classWriterWrapper = FuncClassAdapter(Opcodes.ASM4, classWriterWrapper)
    }

    if (PluginConfig.argusApmConfig().netEnabled) {
        classWriterWrapper = NetClassAdapter(Opcodes.ASM4, classWriterWrapper)
    }

    if (PluginConfig.argusApmConfig().okhttpEnabled) {
        classWriterWrapper = OkHttp3ClassAdapter(Opcodes.ASM4, classWriterWrapper)
    }

    if (PluginConfig.argusApmConfig().webviewEnabled) {
        classWriterWrapper = WebClassAdapter(Opcodes.ASM4, classWriterWrapper)
    }

    classReader.accept(classWriterWrapper, ClassReader.EXPAND_FRAMES)
    return classWriter.toByteArray()
}

代码的主要逻辑是将单个类进行插桩最后返回byteArray。代码中涉及到asm的几个核心类ClassReader,ClassVisitor ,ClassWriter,下面简单介绍下。

    ClassReader类:分析以字节数组形式给出的已编译类,并针对在其 accept 方法参数中传送的 ClassVisitor 实例,调用相应的 visitXxx 方法。这个类可以看作一个事件产生器。 

    ClassVisitor 类:将它收到的所有方法调用都委托给另一个 ClassVisitor 类。这个类可以看作一个事件筛选器。 

    ClassWriter 类:是 ClassVisitor 抽象类的一个子类,它直接以二进制形式生成编译后的类。它会生成一个字节数组形式的输出,其中包含了已编译类,可以用 toByteArray 方法来提取。这个类可以看作一个事件使用器。 

FuncClassAdapter,NetClassAdapter,OkHttp3ClassAdapter,WebClassAdapter这个类继承自BaseClassVisitor,而BaseClassVisitor继承自ClassVisitor 。这四个Adapter都是重写了visitMethod,就是说明只在method进行插桩 。这四个Adapter根据argusApmConfig的开关决定是否开启相关代码的插桩,之后调用classReader.accept(),最后返回classWrite的byteArray。下面分析下FuncClassAdapter这个类,代码如下:

class FuncClassAdapter(api: Int, cv: ClassVisitor?) : BaseClassVisitor(api, cv) {
    override fun visitMethod(access: Int, name: String, desc: String, signature: String?, exceptions: Array<out String>?): MethodVisitor {
        if (isInterface || !isNeedWeaveMethod(className, access)) {
            return super.visitMethod(access, name, desc, signature, exceptions);
        }

        val mv = cv.visitMethod(access, name, desc, signature, exceptions)
        if ((isRunMethod(name, desc) || isOnReceiveMethod(name, desc)) && mv != null) {
            return FuncMethodAdapter(className.replace("/", "."), name, desc, api, access, desc, mv)
        }
        return mv
    }
}

主要是重写了visitMethod。isInterface的判断在BaseClassVisitor,根据access判断。如果是接口,或者不需要插桩,就不处理直接返回,这样不会就不会修改class文件。是否需要插桩的判断代码如下:

fun isNeedWeaveMethod(className: String, access: Int): Boolean {
    return isNeedWeave(className) && isNeedVisit(access)
}

fun isNeedWeave(className: String): Boolean {
    if (PluginConfig.argusApmConfig().whitelist.size > 0) {
        PluginConfig.argusApmConfig().whitelist.forEach {
            if (className.startsWith(it.replace(".", "/"))) {
                return true
            }
        }
        return false
    } else {
        PluginConfig.argusApmConfig().includes.forEach {
            if (className.startsWith(it.replace(".", "/"))) {
                return true
            }
        }

        PluginConfig.argusApmConfig().excludes.forEach {
            if (className.startsWith(it.replace(".", "/"))) {
                return false
            }
        }
        return true
    }
}
 private fun isNeedVisit(access: Int): Boolean {
            //不对抽象方法、native方法、桥接方法(编译器生成)、合成方法进行织入
            if (access and Opcodes.ACC_ABSTRACT !== 0
                    || access and Opcodes.ACC_NATIVE !== 0
                    || access and Opcodes.ACC_BRIDGE !== 0
                    || access and Opcodes.ACC_SYNTHETIC !== 0) {
                return false
            }
            return true
        }

如果需要插桩,并且不是特定的集中方法,就表示需要weave。是否需要weave可以由argusApmConfig来指定,就是我们在build.gradle的dsl中约定。

接下来继续分析FuncClassAdapter的visitMethod().如果是run或者onReceive方法,就调用FuncMethodAdapter进行插桩。分别在run和onReceive的函数执行前后进行插桩。代码如下:

private fun whenMethodEnter() {
    mv.visitMethodInsn(INVOKESTATIC, "java/lang/System", "currentTimeMillis", "()J", false);
    startTimeIndex = newLocal(Type.LONG_TYPE);
    mv.visitVarInsn(LSTORE, startTimeIndex);
}

 private fun whenOnReceiveMethodExit() {
        mv.visitVarInsn(LLOAD, startTimeIndex)
        mv.visitLdcInsn("method-execution")
        mv.visitLdcInsn("void $className.onReceive(Context context, Intent intent)")
        mv.visitVarInsn(ALOAD, 1)
        mv.visitVarInsn(ALOAD, 2)
        mv.visitVarInsn(ALOAD, 0)
        mv.visitVarInsn(ALOAD, 0)
        mv.visitLdcInsn("${className.substring(className.lastIndexOf(".") + 1)}.java:$lineNumber")
        mv.visitLdcInsn("execution(void $className.onReceive(Context context, Intent intent))")
        mv.visitLdcInsn("onReceive")
        mv.visitInsn(ACONST_NULL)
        mv.visitMethodInsn(INVOKESTATIC, "com/argusapm/android/core/job/func/FuncTrace", "dispatch", "(JLjava/lang/String;Ljava/lang/String;Landroid/content/Context;Landroid/content/Intent;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false)
    }

    private fun whenRunMethodExit() {
        mv.visitVarInsn(LLOAD, startTimeIndex)
        mv.visitLdcInsn("method-execution")
        mv.visitLdcInsn("void $className.run()")
        mv.visitInsn(ACONST_NULL)
        mv.visitVarInsn(ALOAD, 0)
        mv.visitVarInsn(ALOAD, 0)
        mv.visitLdcInsn("${className.substring(className.lastIndexOf(".") + 1)}.java:$lineNumber")
        mv.visitLdcInsn("execution(void $className.run())")
        mv.visitLdcInsn("run")
        mv.visitInsn(ACONST_NULL)
        mv.visitMethodInsn(INVOKESTATIC, "com/argusapm/android/core/job/func/FuncTrace", "dispatch", "(JLjava/lang/String;Ljava/lang/String;[Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/Object;)V", false)

    }

这些方法的代码很难看懂,实际写的时候可以使用asm-bytecode-outline这个AS插件生成。如果想要看asm插桩之后的class文件,可以使用TraceClassVisitor这个工具。其他adapter的操作也类似,只是涉及到的类和插桩的地方不一样。限于功力,暂时先跳过。

下面开始介绍ASMWeaver的另外一个方法weaveJar(),代码如下:

fun weaveJar(inputJar: File, outputJar: File) {
    taskManager.addTask(object : ITask {
        override fun call(): Any? {
            FileUtils.copyFile(inputJar, outputJar)
            if (isWeaveThisJar(inputJar.name)) {
                weaveJarTask(inputJar, outputJar)
            }
            return null
        }
    })
}

首先判断这个jar是否需要weave, 这个地方是由argusApmConfig来指定的。如果需要进调用weaveJarTask开始对jar进行weave.代码如下:

private fun weaveJarTask(input: File, output: File) {
    var zipOutputStream: ZipOutputStream? = null
    var zipFile: ZipFile? = null
    try {
        zipOutputStream = ZipOutputStream(BufferedOutputStream(Files.newOutputStream(output.toPath())))
        zipFile = ZipFile(input)
        val enumeration = zipFile.entries()
        while (enumeration.hasMoreElements()) {
            val zipEntry = enumeration.nextElement()
            val zipEntryName = zipEntry.name
            if (TypeUtil.isMatchCondition(zipEntryName) && TypeUtil.isNeedWeave(zipEntryName)) {
                val data = weaveSingleClassToByteArray(BufferedInputStream(zipFile.getInputStream(zipEntry)))
                val byteArrayInputStream = ByteArrayInputStream(data)
                val newZipEntry = ZipEntry(zipEntryName)
                ZipFileUtils.addZipEntry(zipOutputStream, newZipEntry, byteArrayInputStream)
            } else {
                val inputStream = zipFile.getInputStream(zipEntry)
                val newZipEntry = ZipEntry(zipEntryName)
                ZipFileUtils.addZipEntry(zipOutputStream, newZipEntry, inputStream)
            }
        }
    } catch (e: Exception) {
    } finally {
        try {
            if (zipOutputStream != null) {
                zipOutputStream.finish()
                zipOutputStream.flush()
                zipOutputStream.close()
            }
            zipFile?.close()
        } catch (e: Exception) {
            log("close stream err!")
        }
    }
}

主要思路是遍历jar中的每个zipEntry,如果满足特定的条件,并且需要weave,就调用上面的weaveSingleClassToByteArray得到ByteArray.然后生成新的ZipEntry,之后调用ZipFileUtils的addZipEntry,将压缩后的文件输出到weaveJarTask()的out参数位置,如果不满足条件或不需要weave,就直接压缩。可以看到对jar的操作,实际对其中的每个类的weave操作。

最终在ArgusAPMTransform的transform()的最后调用asmWeaver的start,启动AsmWeaver中的所有task.
有什么不对的地方,请大家不吝指教。
学习的过程中,参考了网上的资料,感谢先行者的经验总结和无私分享。

参考文献:
360 Argus APM 源码分析(3)—— argus-apm-aop源码分析

相关文章

网友评论

    本文标题:Argus-apm-gradle-asm 插件

    本文链接:https://www.haomeiwen.com/subject/dasbwctx.html