ArgusAPM 源码解析与学习

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的过程这里就不赘述了,下面看一下整体的框架。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vt1jfbtf-1655453421261)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dccd8afaa5c441bbabce8ece48f20992~tplv-k3u1fbpfcp-zoom-in-crop-mark:1956:0:0:0.image)]

通过上面的时序图可以看到,对于字节码的核心处理逻辑是在 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文件的读取;

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

总结

要想成为架构师,那就不要局限在编码,业务,要会选型、扩展,提升编程思维。此外,良好的职业规划也很重要,学习的习惯很重要,但是最重要的还是要能持之以恒,任何不能坚持落实的计划都是空谈。

如果你没有方向,这里给大家分享一套由阿里高级架构师编写的《Android八大模块进阶笔记》,帮大家将杂乱、零散、碎片化的知识进行体系化的整理,让大家系统而高效地掌握Android开发的各个知识点。
在这里插入图片描述
相对于我们平时看的碎片化内容,这份笔记的知识点更系统化,更容易理解和记忆,是严格按照知识体系编排的。

一、架构师筑基必备技能

1、深入理解Java泛型
2、注解深入浅出
3、并发编程
4、数据传输与序列化
5、Java虚拟机原理
6、高效IO
……

在这里插入图片描述

二、Android百大框架源码解析

1.Retrofit 2.0源码解析
2.Okhttp3源码解析
3.ButterKnife源码解析
4.MPAndroidChart 源码解析
5.Glide源码解析
6.Leakcanary 源码解析
7.Universal-lmage-Loader源码解析
8.EventBus 3.0源码解析
9.zxing源码分析
10.Picasso源码解析
11.LottieAndroid使用详解及源码解析
12.Fresco 源码分析——图片加载流程

在这里插入图片描述

三、Android性能优化实战解析

  • 腾讯Bugly:对字符串匹配算法的一点理解
  • 爱奇艺:安卓APP崩溃捕获方案——xCrash
  • 字节跳动:深入理解Gradle框架之一:Plugin, Extension, buildSrc
  • 百度APP技术:Android H5首屏优化实践
  • 支付宝客户端架构解析:Android 客户端启动速度优化之「垃圾回收」
  • 携程:从智行 Android 项目看组件化架构实践
  • 网易新闻构建优化:如何让你的构建速度“势如闪电”?

在这里插入图片描述

四、高级kotlin强化实战

1、Kotlin入门教程
2、Kotlin 实战避坑指南
3、项目实战《Kotlin Jetpack 实战》

  • 从一个膜拜大神的 Demo 开始

  • Kotlin 写 Gradle 脚本是一种什么体验?

  • Kotlin 编程的三重境界

  • Kotlin 高阶函数

  • Kotlin 泛型

  • Kotlin 扩展

  • Kotlin 委托

  • 协程“不为人知”的调试技巧

  • 图解协程:suspend

在这里插入图片描述

五、Android高级UI开源框架进阶解密

1.SmartRefreshLayout的使用
2.Android之PullToRefresh控件源码解析
3.Android-PullToRefresh下拉刷新库基本用法
4.LoadSir-高效易用的加载反馈页管理框架
5.Android通用LoadingView加载框架详解
6.MPAndroidChart实现LineChart(折线图)
7.hellocharts-android使用指南
8.SmartTable使用指南
9.开源项目android-uitableview介绍
10.ExcelPanel 使用指南
11.Android开源项目SlidingMenu深切解析
12.MaterialDrawer使用指南
在这里插入图片描述

六、NDK模块开发

1、NDK 模块开发
2、JNI 模块
3、Native 开发工具
4、Linux 编程
5、底层图片处理
6、音视频开发
7、机器学习

在这里插入图片描述

七、Flutter技术进阶

1、Flutter跨平台开发概述
2、Windows中Flutter开发环境搭建
3、编写你的第一个Flutter APP
4、Flutter开发环境搭建和调试
5、Dart语法篇之基础语法(一)
6、Dart语法篇之集合的使用与源码解析(二)
7、Dart语法篇之集合操作符函数与源码分析(三)

在这里插入图片描述

八、微信小程序开发

1、小程序概述及入门
2、小程序UI开发
3、API操作
4、购物商场项目实战……

在这里插入图片描述

全套视频资料:

一、面试合集
在这里插入图片描述
二、源码解析合集

在这里插入图片描述
三、开源框架合集

在这里插入图片描述
欢迎大家一键三连支持,若需要文中资料,直接点击文末CSDN官方认证微信卡片免费领取【保证100%免费】↓↓↓
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值