ArgusAPM 源码解析与学习

722 阅读14分钟

ArgusAPM 移动性能监控平台是 360 开源的一款 APM 监测工具。 目前已经停止免费的服务端接入,但是对于我们从客户端的角度来搭建 APM 监测还是有不错的学习参照意义。

ArgusAPM目前支持如下性能指标:

  • 交互分析:分析 Activity生命周期耗时,帮助提升页面打开速度,优化用户 UI体验
  • 网络请求分析:监控流量使用情况,发现并定位各种网络问题
  • 内存分析:全面监控内存使用情况,降低内存占用
  • 进程监控:针对多进程应用,统计进程启动情况,发现启动异常(耗电、存活率等)
  • 文件监控:监控APP私有文件大小/变化,避免私有文件过大导致的卡顿、存储空间占用等问题
  • 卡顿分析:监控并发现卡顿原因,代码堆栈精准定位问题,解决明显的卡顿体验
  • ANR 分析:捕获 ANR 异常,解决 APP 的“未响应”问题

本文也将围绕 ArgusAPM总体框架和支持的功能点进行分析。

ArgusAPM项目分为三个目录,分别是:

  • argus-apm:
  • argus-apm-gradle:
  • argus-apm-gradle-asm:插件工程,定义了插码的实现

1. argus-apm-gradle-asm 工程介绍

argus-apm-gradle工程定义了一个名为 com.argusapm.gradle.ArgusAPMPlugingradle plugin,主要有以下两个作用:

  • 支持 AOP编程,方便 ArgusAPM能够在编译期织入一些性能采集的代码;
  • 通过 Gradle插件来管理依赖库,使用户接入 ArgusAPM更简单。

关于实现一个 Gradle Plugin的过程这里就不赘述了,下面看一下整体的框架。

通过上面的时序图可以看到,对于字节码的核心处理逻辑是在 ASMWeaver类中,

  • FuncClassAdapter :对方法插码
  • NetClassAdapter:对网络插码
  • OkHttp3ClassAdapter:对 okhttp插码
  • WebClassAdapter:对 WebView插码处理

通过以上针对单独业务的插码处理,最终完成所需要的数据采集。

1.1 插件注册

通过上面的时序图得知,插件的注册在 ArgusAPMPlugin 类当中。

internal class ArgusAPMPlugin : Plugin<Project> {
    private lateinit var mProject: Project
    override fun apply(project: Project) {
        mProject = project
        // 创建插件配置项
        project.extensions.create(AppConstant.USER_CONFIG, ArgusApmConfig::class.java)
        PluginConfig.init(project)
        //自定义依赖库管理
        project.gradle.addListener(ArgusDependencyResolutionListener(project))
        
        if (project.plugins.hasPlugin(AppPlugin::class.java)) {
            //监听每个任务的执行时间
            project.gradle.addListener(BuildTimeListener())

            val android = project.extensions.getByType(AppExtension::class.java)
            android.registerTransform(ArgusAPMTransform(project))
        }
    }
}

在上面的代码中,首先创建插件的配置项 argusApmAjxConfig,然后进行插件配置的初始化。在 PluginConfig.init 中有一个点可以学习,判断当前项目是否是 AppLibrary项目。

1)判断当前项目是否是 AppLibrary项目

fun init(project: Project) {
    val hasAppPlugin = project.plugins.hasPlugin(AppPlugin::class.java)
    val hasLibPlugin = project.plugins.hasPlugin(LibraryPlugin::class.java)
    if (!hasAppPlugin && !hasLibPlugin) {
        throw  GradleException("argusapm: The 'com.android.application' or 'com.android.library' plugin is required.")
    }
    Companion.project = project
}

通过判断 project.plugins 中是否包含对应的插件来完成检测。

2)监听依赖变化

通过 project.gradle.addListener方法添加监听来实现,对于依赖的监听处理需要实现 DependencyResolutionListener接口。

class ArgusDependencyResolutionListener(val project: Project) : DependencyResolutionListener {
    override fun beforeResolve(dependencies: ResolvableDependencies?) {
        if (PluginConfig.argusApmConfig().dependencyEnabled) {//如果开启 dependencyEnabled
        	// 如果没有自定义依赖
            if (PluginConfig.argusApmConfig().debugDependencies.isEmpty() && PluginConfig.argusApmConfig().moduleDependencies.isEmpty()) {
                project.compatCompile("com.qihoo360.argusapm:argus-apm-main:${AppConstant.VER}")
                if (PluginConfig.argusApmConfig().okhttpEnabled) {
                    project.compatCompile("com.qihoo360.argusapm:argus-apm-okhttp:${AppConstant.VER}")
                }
            } else {
                //配置本地Module库,方便断点调试
                if (PluginConfig.argusApmConfig().moduleDependencies.isNotEmpty()) {
                    PluginConfig.argusApmConfig().moduleDependencies.forEach { moduleLib: String ->
                        project.compatCompile(project.project(moduleLib))
                    }
                }

                //发布Release版本之前,可以使用Debug库测试
                if (PluginConfig.argusApmConfig().debugDependencies.isNotEmpty()) {
                    project.repositories.mavenLocal()
                    //方便在测试的时候使用,不再需要单独的Gradle发版本
                    PluginConfig.argusApmConfig().debugDependencies.forEach { debugLib: String ->
                        project.compatCompile(debugLib)
                    }
                }
            }
        }
        project.gradle.removeListener(this)
    }

    override fun afterResolve(dependencies: ResolvableDependencies?) {
    }
}

通过监听根据需要在插件中完成添加 dependencies依赖。

3)任务耗时统计

任务耗时是我们优化编译任务的一个很重要参考依据。实现的方式同样是通过 project.gradle.addListener添加 TaskExecutionListenerBuildListener监听来实现。

class BuildTimeListener : TaskExecutionListener, BuildListener {
    private var startTime: Long = 0L
    private var times = mutableListOf<Pair<Long, String>>()

    override fun buildStarted(gradle: Gradle) {}
    override fun settingsEvaluated(settings: Settings) {}
    override fun projectsLoaded(gradle: Gradle) {}
    override fun projectsEvaluated(gradle: Gradle) {}

    override fun buildFinished(result: BuildResult) {
        log("Task spend time:")
        times.filter { it.first > 50 }
                .forEach { log("%7sms\t%s".format(it.first, it.second)) }
    }

    override fun beforeExecute(task: Task) {
        startTime = System.currentTimeMillis()
    }

    override fun afterExecute(task: Task, state: TaskState) {
        val ms = System.currentTimeMillis() - startTime
        times.add(Pair(ms, task.path))
        task.project.logger.warn("${task.path} spend ${ms}ms")
    }
}

当然对于任务耗时,我们也可以通过 ./gradlew --profile --rerun-tasks assembleDebug指令来查看。指令执行完毕后,在目录 /build/reports/profile中查看统计。

1.2 Transform 处理

在自定义 Transform的处理中,通常的套路都是在 transform方法中对输入文件进行插桩操作,然后再写入到对应的文件中。

override fun transform(transformInvocation: TransformInvocation) {
    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) {
              // 增量编译处理
            } else {
               // 非增量编译处理
            }
        }

        input.jarInputs.forEach { jarInput ->
            // 遍历处理 jar
        }
    }
    // 开启处理任务
    asmWeaver.start()
}

ArgusAPM项目中最终通过 ASMWeaver类来完成字节码的修改。

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
        }
    })
}

在扫描到一个文件时,创建一个插桩任务放到任务队列中。当所有文件扫描完毕后,则开始执行插桩任务,调用 weaveSingleClassToByteArray函数实现插桩。

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()
}

通过代码可以看到,weaveSingleClassToByteArray的处理模式是一个责任链的模式,上一级处理的结果作为下一次处理的输入,这点也是依托 ClassVisitor的设计。根据 PluginConfig 配置项的开关状态,然后执行不同的插码处理。

1)FuncClassAdapter 处理器

实现对 Runnable接口以及 onReceive(Context context, Intent intent)接口的插桩。

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
    }
}

FuncClassAdapter的实现中,首先判断是否是接口类型(isInterface)或者 isNeedWeaeMethod是否是需要插码的方法,在 这里使用黑白名单来配置哪些类需要插码或者忽略。最终通过 FuncMethodAdapter实现插码。

FuncMethodAdapter 类实现

FuncMethodAdapter类的功能主要完成对于方法耗时的监听,统计方法耗时。

class FuncMethodAdapter(private val className: String, private val methodName: String, private val methodDesc: String, api: Int, access: Int, desc: String?, mv: MethodVisitor?) : LocalVariablesSorter(api, access, desc, mv) {

    private var startTimeIndex = 0
    private var lineNumber = 0

    override fun visitLineNumber(line: Int, start: Label?) {
        this.lineNumber = line
        super.visitLineNumber(line, start)
    }

    override fun visitCode() {
        super.visitCode()
        if (TypeUtil.isRunMethod(methodName, methodDesc)) {
            whenMethodEnter()
        } else if (TypeUtil.isOnReceiveMethod(methodName, methodDesc)) {
            whenMethodEnter()
        }
    }

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

    override fun visitInsn(opcode: Int) {
        if (((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW)) {
            if (TypeUtil.isRunMethod(methodName, methodDesc)) {
                whenRunMethodExit()
            } else if (TypeUtil.isOnReceiveMethod(methodName, methodDesc)) {
                whenOnReceiveMethodExit()
            }
        }
        super.visitInsn(opcode);
    }
    // 略具体方法实现
}

首先在开始访问方法的时候 visitCode监听中,在 run()onReceive()方法实现中,先通过 whenMethodEnter方法插入 System.currerntTimeMillis来记录开始时间戳。

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

然后通过 visitInsn监听方法退出的指令,插入对应的方法。对于 run()方法使用 whenRunMethodExit插码,对于 onReceive()使用 whenOnReceiveMethodExit插码。以 whenRunMethodExit方法为例。

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)
}

最终都是插入 FuncTrace类中的 dispatch重载方法。

2)NetClassAdapter 处理器

NetClassAdapter用于完成对网络请求方法的插码监听。

class NetClassAdapter(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 || !TypeUtil.isNeedWeaveMethod(className, access)) {
            return super.visitMethod(access, name, desc, signature, exceptions);
        }

        val mv = cv.visitMethod(access, name, desc, signature, exceptions)
        if (mv != null) {
            return NetMethodAdapter(api, access, desc, mv)
        }
        return mv
    }
}

首先判断是否属于可以插码的方法,如果可以则使用 NetMthodAdapter 完成插码。

class NetMethodAdapter(api: Int, access: Int, desc: String?, mv: MethodVisitor?) : LocalVariablesSorter(api, access, desc, mv) {

    override fun visitMethodInsn(opcode: Int, owner: String?, name: String?, desc: String?, itf: Boolean) {
        if (owner == NetConstans.HTTPCLIENT && name == NetConstans.EXECUTE) {
            when (desc) {
                NetConstans.REQUEST -> {
                }
                NetConstans.REQUEST_CONTEXT -> {
                }
                NetConstans.REQUEST_RESPONSEHANDLER -> {
                }
                NetConstans.REQUEST_RESPONSEHANDLER_CONTEXT -> {
                }
                NetConstans.HOST_REQUEST -> {
                }
                NetConstans.HOST_REQUEST_CONTEXT -> {
                }
                NetConstans.HOST_REQUEST_RESPONSEHANDLER -> {
                }
                NetConstans.HOST_REQUEST_RESPONSEHANDLER_CONTEXT -> {
                }
                else -> super.visitMethodInsn(opcode, owner, name, desc, itf)
            }
        } else if (owner == NetConstans.URL && name == NetConstans.OPEN_CONNECTION) {
            when (desc) {
                NetConstans.URL_CONNECTION -> {
                }
                NetConstans.URL_CONNECTION_PROXY -> {
                }
                else -> super.visitMethodInsn(opcode, owner, name, desc, itf)
            }
        } else {
            super.visitMethodInsn(opcode, owner, name, desc, itf)
        }
    }
}

这里分别对 HttpClientURLConnection两种网络请求方式进行插码。其中根据不同的阶段进行插码。此处以 NetConstans.URL_CONNECTION为例。

NetConstans.URL_CONNECTION -> {
    mv.visitMethodInsn(INVOKESTATIC,
            "com/argusapm/android/core/job/net/i/QURL",
            "openConnection",
            "(Ljava/net/URL;)Ljava/net/URLConnection;",
            false)
}

这里插入 QURL类中的 openConnection方法。

3)OkHttp3MethodAdapter

用于对 OkHttp请求框架插码操作。

class Okhttp3MethodAdapter(private val methodName: String, api: Int, access: Int, private val desc: String, mv: MethodVisitor?) : LocalVariablesSorter(api, access, desc, mv) {
    override fun visitInsn(opcode: Int) {
        if (isReturn(opcode) && TypeUtil.isOkhttpClientBuild(methodName, desc)) {
            mv.visitVarInsn(ALOAD, 0)
            mv.visitFieldInsn(GETFIELD, "okhttp3/OkHttpClient$Builder", "interceptors", "Ljava/util/List;")
            mv.visitMethodInsn(Opcodes.INVOKESTATIC, "com/argusapm/android/okhttp3/OkHttpUtils", "insertToOkHttpClientBuilder", "(Ljava/util/List;)V", false)
        }
        super.visitInsn(opcode)
    }

    private fun isReturn(opcode: Int): Boolean {
        return ((opcode >= Opcodes.IRETURN && opcode <= Opcodes.RETURN) || opcode == Opcodes.ATHROW)
    }
    // From:TypeUtil.isOkhttpClientBuild
    fun isOkhttpClientBuild(methodName: String, methodDesc: String): Boolean {
            return ("<init>" == methodName && ("()V" == methodDesc || "(Lokhttp3/OkHttpClient;)V" == methodDesc))
    }
}

visitInsn方法中,当方法是 OkHttpClient的构造函数并且在方法结束时,进行插码,插码调用函数 OkHttpUtils.insertToOkHttpClientBuilder

4)WebClassAdapter

用于对 WebView进行插码,监听页面加载完成 onPageFinished

class WebClassAdapter(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 || !TypeUtil.isNeedWeaveMethod(className, access)) {
            return super.visitMethod(access, name, desc, signature, exceptions);
        }

        val mv = cv.visitMethod(access, name, desc, signature, exceptions)
        if (TypeUtil.isOnPageFinishedMethod(name, desc)) {
            if (mv != null) {
                return WebMethodAdapter(name, desc, api, access, desc, mv)
            }
        }
        return mv
    }
    fun isOnPageFinishedMethod(methodName: String, methodDesc: String): Boolean {
        return methodName == "onPageFinished" && methodDesc == "(Landroid/webkit/WebView;Ljava/lang/String;)V"
    }
}

1.3 小结

至此,完成了 argus-apm-gradle-asm工程的介绍,从中可以看到主要是对耗时、网络和 WebView加载进行插码统计。

2. argus-apm-gradle 工程

argus-apm-gradle工程中定义了一个名为 com.argusapm.gradle.AspectJPlugin的插件。该插件用于对 AspectJ的代码插码。

2.1 插件注册

插件注册的基本流程就不细展开,核心是在 AppExtension.registerTransform

internal class AspectJPlugin : Plugin<Project> {
    private lateinit var mProject: Project
    override fun apply(project: Project) {
        mProject = project
        // 步骤 1:注册插件配置
        project.extensions.create(AppConstant.USER_CONFIG, ArgusApmConfig::class.java)

        // 步骤 2:公共配置初始化,方便获取插件信息
        PluginConfig.init(project)

        // 步骤 3:自动依赖库管理
        project.gradle.addListener(ArgusDependencyResolutionListener(project))
        project.repositories.mavenCentral()
        project.compatCompile("org.aspectj:aspectjrt:1.8.9")
        if (project.plugins.hasPlugin(AppPlugin::class.java)) {
            // 步骤 4:注册插件耗时监听
            project.gradle.addListener(BuildTimeListener())
            // 步骤 5:注册 Transform
            val android = project.extensions.getByType(AppExtension::class.java)
            android.registerTransform(AspectJTransform(project))
        }
    }
}

上面的核心逻辑如下:

  • 步骤 1:注册插件配置,用于对插件的插桩进行控制。配置项名称为 argusApmAjxConfig,包含的配置项:插件的开关、插码的黑白名单、依赖配置;
  • 步骤 2:公共配置初始化,在 PluginConfig中进行插件配置信息的管理;
  • 步骤 3:自动依赖库管理,实践中很实用的一个功能,方便客户进行集成使用;
  • 步骤 4:注册插件耗时监听,用于监听插件的耗时;
  • 步骤 5:注册自定义 Transform,用于完成插码处理。

1) 自动添加依赖库

这里实现对于 argus-apm-mainargus-apm-aoprgus-apm-okhttp三个仓库的依赖添加。

class ArgusDependencyResolutionListener(val project: Project) : DependencyResolutionListener {
    override fun beforeResolve(dependencies: ResolvableDependencies?) {
        if (PluginConfig.argusApmConfig().dependencyEnabled) {
            if (PluginConfig.argusApmConfig().debugDependencies.isEmpty() && PluginConfig.argusApmConfig().moduleDependencies.isEmpty()) {
                project.compatCompile("com.qihoo360.argusapm:argus-apm-main:${AppConstant.VER}")
                project.compatCompile("com.qihoo360.argusapm:argus-apm-aop:${AppConstant.VER}")

                if (PluginConfig.argusApmConfig().okhttpEnabled) {
                    project.compatCompile("com.qihoo360.argusapm:argus-apm-okhttp:${AppConstant.VER}")
                }
            } else {
                //配置本地Module库,方便断点调试
                if (PluginConfig.argusApmConfig().moduleDependencies.isNotEmpty()) {
                    PluginConfig.argusApmConfig().moduleDependencies.forEach { moduleLib: String ->
                        project.compatCompile(project.project(moduleLib))
                    }
                }

                //发布Release版本之前,可以使用Debug库测试
                if (PluginConfig.argusApmConfig().debugDependencies.isNotEmpty()) {
                    project.repositories.mavenLocal()
                    //方便在测试的时候使用,不再需要单独的Gradle发版本
                    PluginConfig.argusApmConfig().debugDependencies.forEach { debugLib: String ->
                        project.compatCompile(debugLib)
                    }
                }
            }
        }
        project.gradle.removeListener(this)
    }
}

2.2 Transform 处理

internal class AspectJTransform(private val project: Project) : Transform() {
	...
    override fun transform(transformInvocation: TransformInvocation) {
        val transformTask = transformInvocation.context as TransformTask
        LogStatus.logStart(transformTask.variantName)

        //第一步:对输入源Class文件进行切割分组
        val fileFilter = FileFilter(project, transformTask.variantName)
        val inputSourceFileStatus = InputSourceFileStatus()
        InputSourceCutter(transformInvocation, fileFilter, inputSourceFileStatus).startCut()

        //第二步:如果含有AspectJ文件,则开启织入;否则,将输入源输出到目标目录下
        if (PluginConfig.argusApmConfig().enabled && fileFilter.hasAspectJFile()) {
            AjcWeaverManager(transformInvocation, inputSourceFileStatus).weaver()
        } else {
            outputFiles(transformInvocation)
        }

        LogStatus.logEnd(transformTask.variantName)
    }
}

Transform中的核心包含两步:第一步将文件进行分类筛选,筛选出需要进行插码的类文件。第二步是进行插码。当然这里还有一种处理思路:在扫描类文件的过程中进行插码。

1)文件切割分组

文件切割分组的目的是对插码目标文件进行筛选,这里 FileFilter完成实际筛选动作,InputSourceCutter完成文件的扫描。

InputSourceCutter

internal class InputSourceCutter(val transformInvocation: TransformInvocation, val fileFilter: FileFilter, val inputSourceFileStatus: InputSourceFileStatus) {
    private val taskManager = ThreadPool()

    init {
        //如果是增量编译
        if (transformInvocation.isIncremental) {
            LogStatus.isIncremental("true")
            LogStatus.cutStart()
            // 遍历输入的 jar 和 directory
            transformInvocation.inputs.forEach { input ->
                input.directoryInputs.forEach { dirInput ->
                    whenDirInputsChanged(dirInput)
                }

                input.jarInputs.forEach { jarInput ->
                    whenJarInputsChanged(jarInput)
                }
            }

            LogStatus.cutEnd()
        } else {
            LogStatus.isIncremental("false")
            LogStatus.cutStart()
            transformInvocation.outputProvider.deleteAll()
            // 全量遍历 jar 和 directory
            transformInvocation.inputs.forEach { input ->
                input.directoryInputs.forEach { dirInput ->
                    cutDirInputs(dirInput)
                }

                input.jarInputs.forEach { jarInput ->
                    cutJarInputs(jarInput)
                }
            }
            LogStatus.cutEnd()
        }
    }
    ....
 }

在类初始化时,则在 init方法中对输入的 JarInputsDirectoryInputs进行扫描,完成对文件的遍历和任务添加。开启增量编译时,则执行 whenDirInputsChangedwhenJarInputsChanged方法,内部只对发生改变的文件进行处理。如果没开启增量编译,则先全部删除已有的输出文件,然后执行 cutDirInputscutJarInputs方法进行分割。这里以 cutJarInputs方法为例。

private fun cutJarInputs(jarInput: JarInput) {
    taskManager.addTask(object : ITask {
        override fun call(): Any? {
            fileFilter.filterAJClassFromJar(jarInput)
            fileFilter.filterClassFromJar(transformInvocation, jarInput)
            return null
        }
    })
}

这里构建一个自定义的 ITask类型任务,然后添加到 TaskManager任务管理器中。任务的实际执行就是调用 FileFilter类的 filterAJClassFromJarfilterClassFromJar方法执行切割。

fun filterAJClassFromJar(jarInput: JarInput) {
    val jarFile = JarFile(jarInput.file)
    val entries = jarFile.entries()
    while (entries.hasMoreElements()) {
        val jarEntry = entries.nextElement()
        val entryName = jarEntry.name
        if (!(jarEntry.isDirectory || !isClassFile(entryName))) {
            val bytes = ByteStreams.toByteArray(jarFile.getInputStream(jarEntry))
            val cacheFile = File(aspectPath + File.separator + entryName)
            if (isAspectClass(bytes)) {
                cache(bytes, cacheFile)
            }
        }
    }

    jarFile.close()

}

通过 jarFile.entries获取所有文件进行遍历,然后判断如果是 class文件,则创建缓存文件并调用 cache方法缓存。这里的 isAspectClass的实现是通过自定义 ClassVisitor对文件扫描,判断是否包含 Aspect注解来判断是否是 AspectJ相关的类。

fun isAspectClass(bytes: ByteArray): Boolean {
    if (bytes.isEmpty()) {
        return false
    }
    try {
        val classReader = ClassReader(bytes)
        val classWriter = ClassWriter(classReader, ClassWriter.COMPUTE_MAXS or ClassWriter.COMPUTE_FRAMES)
        val aspectJClassVisitor = AspectJClassVisitor(classWriter)
        classReader.accept(aspectJClassVisitor, ClassReader.EXPAND_FRAMES)
        return aspectJClassVisitor.isAspectClass
    } catch (e: Exception) {}
    return false
}

class AspectJClassVisitor(classWriter: ClassWriter) : ClassVisitor(Opcodes.ASM5, classWriter) {
    var isAspectClass = false

    override fun visitAnnotation(desc: String, visible: Boolean): AnnotationVisitor {
        isAspectClass = (desc == "Lorg/aspectj/lang/annotation/Aspect;")
        return super.visitAnnotation(desc, visible)
    }
}

通过代码可以看到,通过判断是否 Aspect注解来判断是否属于 AspectJ标识的类。如果是则调用 cache方法进行缓存。

2)插码织入

在完成对文件的切割分组之后,则进行插码织入。

//第二步:如果含有AspectJ文件,则开启织入;否则,将输入源输出到目标目录下
if (PluginConfig.argusApmConfig().enabled && fileFilter.hasAspectJFile()) {
    AjcWeaverManager(transformInvocation, inputSourceFileStatus).weaver()
} else {
    outputFiles(transformInvocation)
}

如果插件开关可用且含有 AspectJ文件,则开启织入。否则,将输入源输出到目标目录下。此处调用 AjcWeaverManager类的 weaver方法进行代码织入。

fun weaver() {
    System.setProperty("aspectj.multithreaded", "true")
    // 步骤一:创建任务
    if (transformInvocation.isIncremental) {
        createIncrementalTask()
    } else {
        createTask()
    }
    log("AjcWeaverList.size is ${threadPool.taskList.size}")

    aspectPath.add(getAspectDir())
    classPath.add(getIncludeFileDir())
    classPath.add(getExcludeFileDir())
    // 步骤二:任务执行
    threadPool.taskList.forEach { ajcWeaver ->
        ajcWeaver as AjcWeaver
        ajcWeaver.encoding = PluginConfig.encoding
        ajcWeaver.aspectPath = aspectPath
        ajcWeaver.classPath = classPath
        ajcWeaver.targetCompatibility = PluginConfig.targetCompatibility
        ajcWeaver.sourceCompatibility = PluginConfig.sourceCompatibility
        ajcWeaver.bootClassPath = PluginConfig.bootClassPath
        ajcWeaver.ajcArgs = PluginConfig.argusApmConfig().ajcArgs
    }
    threadPool.startWork()
}

weaver()方法中,根据是否开启增量编译创建不同的任务:createIncrementalTaskcreateTask

private fun createTask() {
    val ajcWeaver = AjcWeaver()
    val includeJar = transformInvocation.outputProvider.getContentLocation("include", contentTypes as Set<QualifiedContent.ContentType>, scopes, Format.JAR)
    if (!includeJar.parentFile.exists()) {
        FileUtils.forceMkdir(includeJar.parentFile)
    }
    FileUtils.deleteQuietly(includeJar)
    ajcWeaver.outputJar = includeJar.absolutePath
    ajcWeaver.inPath.add(getIncludeFileDir())
    addAjcWeaver(ajcWeaver)

    transformInvocation.inputs.forEach { input ->
        input.jarInputs.forEach { jarInput ->
            classPath.add(jarInput.file)
            //如果该Jar参与AJC织入的话,则进行下面操作
            if (filterJar(jarInput, PluginConfig.argusApmConfig().includes, PluginConfig.argusApmConfig().excludes, PluginConfig.argusApmConfig().excludeJars)) {
                val tempAjcWeaver = AjcWeaver()
                tempAjcWeaver.inPath.add(jarInput.file)

                val outputJar = transformInvocation.outputProvider.getContentLocation(jarInput.name, jarInput.contentTypes,
                        jarInput.scopes, Format.JAR)
                if (!outputJar.parentFile?.exists()!!) {
                    outputJar.parentFile?.mkdirs()
                }

                tempAjcWeaver.outputJar = outputJar.absolutePath
                addAjcWeaver(tempAjcWeaver)
            }
        }
    }
}

从中可以看到,主要是创建 AjcWeaver对象,该对象实现 ITask接口,并重写 call()方法来实现具体的处理。

3. argus-apm 工程介绍

该工程中包含了插码以及采集的核心实现。

  • argus-apm-okhttp用于 okhttp的插码监听,完成对于网络的监听;
  • argus-apm-main主工程,包含对于耗时卡顿等多方的监听
  • argus-apm-cloud网络上报处理

3.1 核心主流程

在使用 argus-apm时,我们使用 attach方法进行初始化,并调用 startWork开始收集工作。

在这个过程中,这里主要关注两个过程:registerTask()注册任务和 startWorkTasks()执行任务。

1)registerTask 注册任务

// TaskManager 注册任务
public void registerTask() {
    if (Env.DEBUG) {
        LogX.d(Env.TAG, "TaskManager", "registerTask " + getClass().getClassLoader());
    }
    if (Build.VERSION.SDK_INT >= 16) {
        taskMap.put(ApmTask.TASK_FPS, new FpsTask());
    }
    taskMap.put(ApmTask.TASK_MEM, new MemoryTask());
    taskMap.put(ApmTask.TASK_ACTIVITY, new ActivityTask());
    taskMap.put(ApmTask.TASK_NET, new NetTask());
    taskMap.put(ApmTask.TASK_APP_START, new AppStartTask());
    taskMap.put(ApmTask.TASK_ANR, new AnrLoopTask(Manager.getContext()));
    taskMap.put(ApmTask.TASK_FILE_INFO, new FileInfoTask());
    taskMap.put(ApmTask.TASK_PROCESS_INFO, new ProcessInfoTask());
    taskMap.put(ApmTask.TASK_BLOCK, new BlockTask());
    taskMap.put(ApmTask.TASK_WATCHDOG, new WatchDogTask());
}

从中可以看到,在主工程中对于收集的性能通过自定义 ITask进行任务的执行,这里分别注册了 FpsNetAnrProcessInfo等任务,这些任务共同完成对于性能的统计。

2)startWorkTasks 执行任务

public void startWorkTasks() {
    if (taskMap == null) {
        LogX.d(Env.TAG, SUB_TAG, "taskMap is null ");
        return;
    }
    if (taskMap.get(ApmTask.TASK_ACTIVITY).isCanWork()) {
        // 云控为TaskConfig.ACTIVITY_TYPE_NONE,则本地开关优先
        int type = ArgusApmConfigManager.getInstance().getArgusApmConfigData().controlActivity;
        if (type == TaskConfig.ACTIVITY_TYPE_NONE) {
            if (Manager.getInstance().getConfig().isEnabled(ApmTask.FLAG_COLLECT_ACTIVITY_INSTRUMENTATION)) {
                LogX.o("activity local INSTRUMENTATION");
                InstrumentationHooker.doHook();
            } else {
                LogX.o("activity local aop");
            }
        } else if (type == TaskConfig.ACTIVITY_TYPE_INSTRUMENTATION) {
            LogX.o("activity cloud INSTRUMENTATION");
            InstrumentationHooker.doHook();
        } else {
            LogX.o("activity cloud type(" + type + ")");
        }

    }
    List<ITask> taskList = getAllTask();
    for (ITask task : taskList) {
        if (!task.isCanWork()) {
            continue;
        }
        if (DEBUG) {
            LogX.d(Env.TAG, SUB_TAG, "start task " + task.getTaskName());
        }
        task.start();
    }
}	

任务执行的逻辑分为两大部分:对于 ActivityHook监听和其它统计任务的执行。

3.2 Activity 耗时统计

这里主要是分析 Activity生命周期耗时情况,对应的源码在 argus-apm-main工程下的 argus-apm-main/src/main/java/com/argusapm/android/core/job/activity目录下。

通过 hook Instrumentation 类实现对 Activity 的耗时统计。

1)InstrumentationHooker.doHook()

TaskManager.startWorkTasks()执行时,通过 InstrumentationHooker.doHook()Activity进行 hook

private static void hookInstrumentation() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException, NoSuchFieldException {
    Class<?> c = Class.forName("android.app.ActivityThread");
    Method currentActivityThread = c.getDeclaredMethod("currentActivityThread");
    boolean acc = currentActivityThread.isAccessible();
    if (!acc) {
        currentActivityThread.setAccessible(true);
    }
    Object o = currentActivityThread.invoke(null);
    if (!acc) {
        currentActivityThread.setAccessible(acc);
    }
    Field f = c.getDeclaredField("mInstrumentation");
    acc = f.isAccessible();
    if (!acc) {
        f.setAccessible(true);
    }
    Instrumentation currentInstrumentation = (Instrumentation) f.get(o);
    Instrumentation ins = new ApmInstrumentation(currentInstrumentation);
    f.set(o, ins);
    if (!acc) {
        f.setAccessible(acc);
    }
}

通过反射获取 ActivityThread类,然后通过 currentActivityThread方法获取当前的 ActivityThread对象,最后在获取 Instrumentation对象,然后使用自定义的 ApmInstrumentation替换系统的 Instrumentation对象。

2)Instrumentation监听

这里只介绍通过 callActivityOnStart方法进行 onStart耗时的统计,其它类似。

public class ApmInstrumentation extends Instrumentation {
    private static final String SUB_TAG = "traceactivity";

    private Instrumentation mOldInstrumentation = null;

    public ApmInstrumentation(Instrumentation oldInstrumentation) {
        if (oldInstrumentation instanceof Instrumentation) {
            mOldInstrumentation = oldInstrumentation;
        }
    }

    @Override
    public void callApplicationOnCreate(Application app) {}

    @Override
    public void callActivityOnCreate(Activity activity, Bundle icicle) {}

    @Override
    public void callActivityOnStart(Activity activity) {
        if (!isActivityTaskRunning()) {
            if (mOldInstrumentation != null) {
                mOldInstrumentation.callActivityOnStart(activity);
            } else {
                super.callActivityOnStart(activity);
            }
            return;
        }
        if (DEBUG) {
            LogX.d(TAG, SUB_TAG, "callActivityOnStart: ");
        }
        long startTime = System.currentTimeMillis();
        if (mOldInstrumentation != null) {
            mOldInstrumentation.callActivityOnStart(activity);
        } else {
            super.callActivityOnStart(activity);
        }
        ActivityCore.saveActivityInfo(activity, ActivityInfo.HOT_START, System.currentTimeMillis() - startTime, ActivityInfo.TYPE_START);
    }

    @Override
    public void callActivityOnResume(Activity activity) {}

    @Override
    public void callActivityOnStop(Activity activity) {}

    @Override
    public void callActivityOnPause(Activity activity) {}

    @Override
    public void callActivityOnDestroy(Activity activity) {}
}

可以看到,在 callActivityOnStart执行前记录一个 startTime,然后在执行完毕后调用 saveActivityInfo保存耗时的时间。

3.3 FPS 统计

FPS的统计是在 FpsTask任务中完成。原理是通过实现 Choreographer.FrameCallback协议的 doFrame接口完成对 FPS的耗时统计。

public class FpsTask extends BaseTask implements Choreographer.FrameCallback {
    private final String SUB_TAG = ApmTask.TASK_FPS;

    private long mLastFrameTimeNanos = 0; //最后一次时间
    private long mFrameTimeNanos = 0; //本次的当前时间
    private int mCurrentCount = 0; //当前采集条数
    private int mFpsCount = 0;
    private FpsInfo fpsInfo = new FpsInfo();
    private JSONObject paramsJson = new JSONObject();
    //定时任务
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if (!isCanWork()) {
                mCurrentCount = 0;
                return;
            }
            calculateFPS();
            mCurrentCount++;
            //实现分段采集
            if (mCurrentCount < ArgusApmConfigManager.getInstance().getArgusApmConfigData().onceMaxCount) {
                AsyncThreadTask.executeDelayed(runnable, TaskConfig.FPS_INTERVAL);
            } else {
                AsyncThreadTask.executeDelayed(runnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().pauseInterval > TaskConfig.FPS_INTERVAL ? ArgusApmConfigManager.getInstance().getArgusApmConfigData().pauseInterval : TaskConfig.FPS_INTERVAL);
                mCurrentCount = 0;
            }
        }
    };

    private void calculateFPS() {
        if (mLastFrameTimeNanos == 0) {
            mLastFrameTimeNanos = mFrameTimeNanos;
            return;
        }
        float costTime = (float) (mFrameTimeNanos - mLastFrameTimeNanos) / 1000000.0F;
        if (mFpsCount <= 0 && costTime <= 0.0F) {
            return;
        }
        int fpsResult = (int) (mFpsCount * 1000 / costTime);
        if (fpsResult < 0) {
            return;
        }
        if (fpsResult <= TaskConfig.DEFAULT_FPS_MIN_COUNT) {
            fpsInfo.setFps(fpsResult);
            try {
                paramsJson.put(FpsInfo.KEY_STACK, CommonUtils.getStack());
            } catch (JSONException e) {
                e.printStackTrace();
            }
            fpsInfo.setParams(paramsJson.toString());
            fpsInfo.setProcessName(ProcessUtils.getCurrentProcessName());
            save(fpsInfo);
        }
        if (AnalyzeManager.getInstance().isDebugMode()) {
            if (fpsResult > TaskConfig.DEFAULT_FPS_MIN_COUNT) {
                fpsInfo.setFps(fpsResult);
            }
            AnalyzeManager.getInstance().getParseTask(ApmTask.TASK_FPS).parse(fpsInfo);
        }
        mLastFrameTimeNanos = mFrameTimeNanos;
        mFpsCount = 0;
    }

    @Override
    protected IStorage getStorage() {
        return new FpsStorage();
    }

    @Override
    public void start() {
        super.start();
        AsyncThreadTask.executeDelayed(runnable, (int) (Math.round(Math.random() * TaskConfig.TASK_DELAY_RANDOM_INTERVAL)));
        Choreographer.getInstance().postFrameCallback(this);
    }

    @Override
    public void doFrame(long frameTimeNanos) {
        mFpsCount++;
        mFrameTimeNanos = frameTimeNanos;
        if (isCanWork()) {
            //注册下一帧回调
            Choreographer.getInstance().postFrameCallback(this);
        } else {
            mCurrentCount = 0;
        }
    }
}

可以看到,在 start任务时,会开启一个异步定时任务,同时对 Choreographer注册 postFrameCallback回调,用于监听每次帧变化,然后记录一下当前帧的时间 mFrameTimeNanos。然后在 calculateFPS()方法中计算耗时,并将计算结果进行保存。

3.4 内存统计

内存统计在 MemoryTask任务中完成。核心是通过 Debug类的 getMemoryInfo方法读取到当前的内存占用信息,然后进行保存。

public class MemoryTask extends BaseTask {
    private static final String SUB_TAG = "MemoryTask";

    //定时任务
    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if ((!isCanWork()) || (!checkTime())) {
                return;
            }
            MemoryInfo memoryInfo = getMemoryInfo();
            if (AnalyzeManager.getInstance().isDebugMode()) {
                AnalyzeManager.getInstance().getParseTask(ApmTask.TASK_MEM).parse(memoryInfo);
            }
            save(memoryInfo);
            updateLastTime();
            if (Env.DEBUG) {
                AsyncThreadTask.getInstance().executeDelayed(runnable, TaskConfig.TEST_INTERVAL);
            } else {
                AsyncThreadTask.getInstance().executeDelayed(runnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.getMemoryIntervalTime());
            }
        }
    };

    /**
     * 获取当前内存信息
     */
    private MemoryInfo getMemoryInfo() {
        // 注意:这里是耗时和耗CPU的操作,一定要谨慎调用
        Debug.MemoryInfo info = new Debug.MemoryInfo();
        Debug.getMemoryInfo(info);
        if (DEBUG) {
            LogX.d(TAG, SUB_TAG,
                    "当前进程:" + ProcessUtils.getCurrentProcessName()
                            + ",内存getTotalPss:" + info.getTotalPss()
                            + " nativeSize:" + info.nativePss
                            + " dalvikPss:" + info.dalvikPss
                            + " otherPss:" + info.otherPss

            );
        }
        return new MemoryInfo(ProcessUtils.getCurrentProcessName(), info.getTotalPss(), info.dalvikPss, info.nativePss, info.otherPss);
    }

    @Override
    public void start() {
        super.start();
        AsyncThreadTask.executeDelayed(runnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.getMemoryDelayTime() + (int) (Math.round(Math.random() * 1000)));
    }
}

3.5 卡顿监听

卡顿监听在 BlockTask任务中完成,通过 setMessageLogging接口的特性来实现卡顿的监听。

public class BlockTask extends BaseTask {
    private final String SUB_TAG = "BlockTask";
    private HandlerThread mBlockThread = new HandlerThread("blockThread");
    private Handler mHandler;

    private Runnable mBlockRunnable = new Runnable() {
        @Override
        public void run() {
            if (!isCanWork()) {
                return;
            }
            StringBuilder sb = new StringBuilder();
            StackTraceElement[] stackTrace = Looper.getMainLooper().getThread().getStackTrace();
            for (StackTraceElement s : stackTrace) {
                sb.append(s.toString() + "\n");
            }
            if (DEBUG) {
                LogX.d(TAG, SUB_TAG, sb.toString());
            }
            saveBlockInfo(sb.toString());
        }
    };

    @Override
    public void start() {
        super.start();
        if (!mBlockThread.isAlive()) { //防止多次调用
            mBlockThread.start();
            mHandler = new Handler(mBlockThread.getLooper());
            Looper.getMainLooper().setMessageLogging(new Printer() {

                private static final String START = ">>>>> Dispatching";
                private static final String END = "<<<<< Finished";

                @Override
                public void println(String x) {
                    if (x.startsWith(START)) {
                        startMonitor();
                    }
                    if (x.startsWith(END)) {
                        removeMonitor();
                    }
                }
            });
        }
    }

    public void startMonitor() {
        mHandler.postDelayed(mBlockRunnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.blockMinTime);
    }

    public void removeMonitor() {
        mHandler.removeCallbacks(mBlockRunnable);
    }

    /**
     * 保存卡顿相关信息
     */
    private void saveBlockInfo(final String stack) {
        AsyncThreadTask.execute(new Runnable() {
            @Override
            public void run() {
                BlockInfo info = new BlockInfo();
                info.blockStack = stack;
                info.blockTime = ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.blockMinTime;
                ITask task = Manager.getInstance().getTaskManager().getTask(ApmTask.TASK_BLOCK);
                if (task != null) {
                    task.save(info);
                } else {
                    if (DEBUG) {
                        LogX.d(TAG, "Client", "BlockInfo task == null");
                    }
                }
            }
        });
    }
}

通过判断日志的开始和结束标志,然后统计耗时时间。

3.5 ANR 统计

对于 ANR的统计是在 AnrLoopTask任务中实现。通过定期的去读取 /data/anr目录下的文件,采集对应的 ANR信息并存储。

public class AnrLoopTask extends AnrTask {
    public static final String SUB_TAG = "AnrLoopTask";

    public AnrLoopTask(Context c) {
        super(c);
    }

    @Override
    protected IStorage getStorage() {
        return null;
    }

    @Override
    public String getTaskName() {
        return ApmTask.TASK_ANR;
    }

    private Runnable runnable = new Runnable() {
        @Override
        public void run() {
            if (!isCanWork()) {
                return;
            }
            if (CommonUtils.isWiFiConnected(mContext)) {
                if (Env.DEBUG) {
                    LogX.d(Env.TAG, SUB_TAG, "anr start obtain");
                }

                readAnrFiles();
            }
            if (Env.DEBUG) {
                AsyncThreadTask.getInstance().executeDelayed(runnable, TaskConfig.TEST_INTERVAL);
            } else {
                AsyncThreadTask.getInstance().executeDelayed(runnable, ArgusApmConfigManager.getInstance().getArgusApmConfigData().funcControl.getAnrIntervalTime());
            }
        }
    };
    // 读取 /data/anr 文件
    private void readAnrFiles() {
        File anrDirF = new File(ANR_DIR);
        if (anrDirF.exists() && anrDirF.isDirectory()) {
            File[] anrFiles = anrDirF.listFiles();
            if (anrFiles != null && anrFiles.length > 0) {
                for (File f : anrFiles) {
                    String fileName = f.getName();
                    if (fileName.contains("trace") && (System.currentTimeMillis() - f.lastModified() < TaskConfig.ANR_VALID_TIME) && (f.length() <= TaskConfig.MAX_READ_FILE_SIZE) && parser != null) {
                        handle(f.getPath());
                    }
                }
            }
        }
    }

    @Override
    public void start() {
        super.start();
        AsyncThreadTask.executeDelayed(runnable, (int) (Math.round(Math.random() * 1000)));
    }
}

4. 总结

至此,我们对 Argus-APM项目有一个初步的认识,也算是接触 APM相关知识的一个入门,项目中涉及到的很多知识都值得我们学习。概括一下知识点:

  • 自定义 Gralde插件、编译耗时(BuildListenerTaskExecutionListener);
  • 自动添加依赖(DependencyResolutionListener),方便集成;
  • 通过 ClassVisitor扫描文件判断文件类型的思路;
  • ActivityInstrumentation hook方式;
  • Choreographer注册 postFrameCallback回调,用于监听帧变化;
  • Debug类的高级用处,获取内存信息;
  • Looper.getMainLooper().setMessageLogging用于卡顿的统计;
  • ANR文件的读取;

总之分析一个开源库,尽量从中学习一些作者的设计思路和技术点。