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.ArgusAPMPlugin
的 gradle 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
中有一个点可以学习,判断当前项目是否是 App
或 Library
项目。
1)判断当前项目是否是 App
或 Library
项目
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
添加 TaskExecutionListener
和 BuildListener
监听来实现。
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)
}
}
}
这里分别对 HttpClient
和 URLConnection
两种网络请求方式进行插码。其中根据不同的阶段进行插码。此处以 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-main
、argus-apm-aop
、rgus-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
方法中对输入的 JarInputs
和 DirectoryInputs
进行扫描,完成对文件的遍历和任务添加。开启增量编译时,则执行 whenDirInputsChanged
和 whenJarInputsChanged
方法,内部只对发生改变的文件进行处理。如果没开启增量编译,则先全部删除已有的输出文件,然后执行 cutDirInputs
和 cutJarInputs
方法进行分割。这里以 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
类的 filterAJClassFromJar
和 filterClassFromJar
方法执行切割。
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()
方法中,根据是否开启增量编译创建不同的任务:createIncrementalTask
和 createTask
。
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
进行任务的执行,这里分别注册了 Fps
、Net
、Anr
、ProcessInfo
等任务,这些任务共同完成对于性能的统计。
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();
}
}
任务执行的逻辑分为两大部分:对于 Activity
的 Hook
监听和其它统计任务的执行。
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
插件、编译耗时(BuildListener
和TaskExecutionListener
); - 自动添加依赖(
DependencyResolutionListener
),方便集成; - 通过
ClassVisitor
扫描文件判断文件类型的思路; Activity
的Instrumentation 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%免费】↓↓↓