美文网首页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