滴滴开源库Booster:架构运作及源码分析

3,592 阅读10分钟

概述

Booster库是什么这里就不多说了,本文尝试通过源码分析,表述对Booster工作流程及相关细节的理解。

经过个人理解及梳理得出以下的说明图,展示了Booster库在Gradle的两个重要生命周期中,发生的行为:

image.png


骤眼一看项目源码会发现有很多模块,感觉不能一下子掌控。没关系,通过手敲模块名称,按名称前缀规律、构建依赖共同点等特征分类,我得出了以下的划分:

简述

相关模块

各模块基础依赖、kotlin的基础扩展

buildSrc、booster-kotlinx

对Project、AGP中的BaseVariant、VariantScope、TransformInvocation等类添加方法扩展,

booster-gradle-api、booster-gradle-v3_0

booster-gradle-v3_2、booster-gradle-v3_3

Booster的作用入口:自定义插件 + 注册自定义Transfrom。封装ASM层解析class文件成ClassNode并分发到下层具体功能实现。自定义ClassTransform、

VariantProcessor接口等提供扩展能力

booster-gradle-plugin、booster-transform-asm

booster-transform-javassist、booster-transform-spi

booster-transform-util、booster-task-spi

提供编译期对Android API插桩的能力

booster-android-api、booster-android-instrument

具体Booster功能模块

其他模块各自实现:性能检测、性能优化、系统问题、应用瘦身等


正文

1.buildSrc

apply from: ‘../gradle/booster.gradle'

booster-gradle-xxx、booster-transform-xxx等,非booster-android-instrument相关的模块都使用了该统一配置,应用kotlin的相关依赖配置。

并且引入定义在buildSrc目录的自定义Gradle插件’BuildPropsPlugin’。

'buildSrc'是Gradle约定的默认编译目录,效果等同于在settings.gradle中include,该目录是非必要的。

其作用是对项目中Gradle DSL无法简单实现的构建逻辑,进行补充使用。

【BuildPropsPlugin.kt】
让模块project的java、groovy、kotlin相关compile Task,依赖其自定义Task:generateBuildProps,在编译前创建一个Build接口类,包含当前module的GROUP、ARTIFACT、VERSION、REVISION等常量值,在运行过程中方便report信息。


2.booster-gradle-plugin

2.1 BoosterPlugin.kt

负责两件事情:

  1. 根据android app 和 library模块,分别注册BoosterAppTransformBoosterLibTransform,都是BoosterTransform的子类。
  2. 利用Java的ServiceLoader,运行时(Gradle本身是在JVM运行)动态查找VariantProcessor的实现对象列表,遍历并调用processor()方法。

2.2 BoosterTransform.kt

对于抽象类com.android.build.api.transform.Transform,先来一段说明:

  • Transform是在构建构件的过程中执行的过程
  • 多个Transform会连接起来按顺序执行,前者产物Content为后者的入参 Input
  • 同时每个Transform可以设置要接收的content类型ContentType和范围Scope,以及产出类型'ContentType',让整个链更高效
  • inputTransformInput类型集合接收内容,有两个属性 jarInputsdirectoryInputs,两者都有ContentTypeScope的信息
  • 利用TransformOutputProvider对象,可以创建本Transform独立的文件存储空间

BoosterTransform的执行过程是支持Gradle的增量编译,在transform()方法的实现中,把回调的TransformInvocation对象代理给BoosterTransformInvocation

流程逻辑:

增量:
  -> onPreTransform() 
    -> doIncrementalTransform() 
    -> onPostTransform()
非增量:
  -> 清理构建目录、输出目录 
    -> onPreTransfrom() 
    -> doFullTransform() 
    -> onPostTransform()

主要的处理差异在于doIncrementalTransform()doFullTransform() 方法,细节实现都要往BoosterTransformInvocation.kt去看。

2.3 BoosterTransformInvocation.kt

代理默认的TransformInvocation对象,并进行扩展,另外还实现了booster-transform-spi模块的相关接口:

  • TransformContext: 封装了Booster在Transform过程中的上下文数据:类路径、文件目录、应用信息、调试信息等
  • TransformListener: 定义transform的前后监听回调方法onPreTransform()onPostTransform()
  • ArtifactManager: 对Artifact的管理,具体为构建过程的各种文件,get()方法使用VariantScope的各种方法一一对应实现。

关键成员属性:

// 使用SPI机制,读取Transformer接口的实现列表
private val transformers: List<Transformer!> 
  = ServiceLoader.load(Transformer::class.java, 
                       javaClass.classLoader).toList()

对于关键doFullTransform()doIncrementalTransform()方法,主要了解两点:

  • 如何实现增量构建的兼容
  • 具体逻辑流程

构建过程中Transform的回调,通过inputs:List<TransformInput>属性获取资源。

public interface TransformInput {
    /** Returns a collection of {@link JarInput}. */
    @NonNull
    Collection<JarInput> getJarInputs();
    /** Returns a collection of {@link DirectoryInput}. */
    @NonNull
    Collection<DirectoryInput> getDirectoryInputs();
}

JarInputDirectoryInput都是QualifiedContent的子接口,提供方法获取资源的状态:

/**
 * The file changed status for incremental execution.
 */
public enum Status {
    /** 相对上次构建,没有变化.*/
    NOTCHANGED,
    /** 相对上次构建,属于新增文件.*/
    ADDED,
    /** 相对上次构建,有被修改. */
    CHANGED,
    /** 相对上次构建,被删除. */
    REMOVED;
}

显而易见的「增量构建」是发生在两次构建之间的过程,这四种状态涵盖了两次构建中每个文件、文件夹的变化状况。

when (status) {
  REMOVED -> 本次编译不需要该文件/文件夹,删除
    ADDED, CHANGED -> 该文件/文件夹,需要被编译处理
    NOT_CHANGED -> 不需要处理,因为已经有上次构建的.class文件
}

无论是JarInput还是DirectoryInput,都是这样兼容增量构建。

下面来看看input的内容都经历了什么。

FileInputStreamByteArray类,都利用Kotlin添加扩展方法transform

  • File.transform(output:File, transform:(ByteArray)->ByteArray)
fun File.transform(output: File, transformer: (ByteArray) -> ByteArray = { it -> it }) {
    when {
        isDirectory -> {
            val base = this.toURI()
            this.search().forEach {
                it.transform(File(output, base.relativize(it.toURI()).path), transformer)
            }
        }
        isFile -> {
            when (output.extension.toLowerCase()) {
                "jar" -> {
                    JarOutputStream(output.touch().outputStream()).use { dest ->
                        JarFile(this).use { jar ->
                            jar.entries().asSequence().forEach { entry ->
                                dest.putNextEntry(JarEntry(entry.name))
                                if (!entry.isDirectory) {
                                    when (entry.name.substringAfterLast('.', "")) {
                                        "class" -> jar.getInputStream(entry).use { src ->
                                            logger.info("Transforming ${this.absolutePath}!/${entry.name}")
                                            src.transform(transformer).redirect(dest)
                                        }
                                        else -> jar.getInputStream(entry).use { src ->
                                            src.copyTo(dest)
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
                "class" -> inputStream().use {
                    logger.info("Transforming ${this.absolutePath}")
                    it.transform(transformer).redirect(output)
                }
                else -> this.copyTo(output, true)
            }
        }
        else -> TODO("Unexpected file: ${this.absolutePath}")
    }
}

对于上述代码,我们可以进行以下的语义理解:

/* 语义理解:把当前File对象通过transform方法处理输出到output文件 */
Check Point 1 : (output, file) {
    -> 检查file是文件目录Directory: 把子File一个个拿出排好队,
           到「Check Point 2」 接受检查
      -> 检查file是文件File: 到「Check Point 2」 接受检查
}
Check Point 2 : (output, file) {
      -> 是'.jar'文件:装饰一下变成JarFile,把entry一个个掏出,
           后缀是'class',拿去「Check Point 3」检查
      -> 是'.class'文件:拿去「Check Point 3」检查
}
Check Point 3 : (output, classFile) {
    -> classFile 转 InputStream,调用InputStream.transform()
}
  • InputStream.transform( transformer:(ByteArray)->ByteArray): ByteArray
// booster-transform-util模块的transform.kt中分别定义transform 和 readBytes方法扩展

fun InputStream.transform( transformer:(ByteArray)->ByteArray): ByteArray {
  transformer(readBytes())
}

private fun InputStream.readBytes(estimatedSize: Int = DEFAULT_BUFFER_SIZE): ByteArray {
    val buffer = ByteArrayOutputStream(Math.max(estimatedSize, this.available()))
    copyTo(buffer)
    return buffer.toByteArray()
}


  • ByteArray.transform(invocation: BoosterTransformInvocation): ByteArray
    private fun ByteArray.transform(invocation: BoosterTransformInvocation): ByteArray {
        return transformers.fold(this) { bytes, transformer ->
            transformer.transform(invocation, bytes) // 成功接受transformer的洗礼
        }
    }   

小结:让每个符合BoosterTransform规定的ContentTypeScope .class文件,都能接受ServiceLoader加载到的Transformtransform()方法的“洗礼”。

问题来了:下层 Transformer拿到ByteArray后都发生了什么?我们接着往下看。


3.booster-transform-asm

该模块只有这些kt文件

AbstractInsnNode.kt
AsmTransformer
ClassNode.kt
ClassTransformer.kt
InsnList.kt
TypeInsnNode.kt

除了AsmTransformClassTransform,其他kt文件都是对ASM API的类进行方法扩展。

3.1 AsmTransform.kt

在这一层的职责,是把ByteArray -> ClassNode,暴露给下一层进行方法修改操作,然后转换回ByteArray返回上层。

使用auto-service库的@AutoService注解,声明AsmTransform按照SPI的规范生成相关的META-INF文件。

在build.gradle中声明依赖:

kapt "com.google.auto.service:auto-service:1.0-rc4"

compile 'com.google.auto.service:auto-service:1.0-rc4'

保证了前面的BoosterTransformInvocation能通过ServiceLoader加载数据。


override fun transform(context: TransformContext, bytecode: ByteArray): ByteArray {
        return ClassWriter(ClassWriter.COMPUTE_MAXS).also { writer ->
            transformers.fold(ClassNode().also { klass ->
                ClassReader(bytecode).accept(klass, 0)
            }) { klass, transformer ->
                transformer.transform(context, klass)
            }.accept(writer)
        }.toByteArray()
    }


这段代码混合使用了kotlin的语法及高阶函数fold(),让上层BoosterTransformInvocation传入的bytecode:ByteArray,经过ASM Core API的ClassWriterClassReader + Tree API 的 ClassNode,对ByteArray进行读取解析,然后通过累计遍历transformers,再次洗礼。

下面尝试换把AsmTransformer.transform()方法的代码,换一个老套的写法,更容易理解:

val klass = ClassNode() // ClassNode是ASM tree API的类
// accept()方法,读取bytecode的相关.class信息赋值到klass对象的相应字段
ClassReader(bytecode).accept(klass, 0) // ClassReader是ASM core API的类
transformers.foreach { transformer ->
    // 每次transform后,class的内容变化都缓存在klass中
  // 注意此时的klass对象的内容就是bytecode的内容,只是格式不同而已
  transformer.transform(context, klass) 
}
val writer = ClassWriter(ClassWriter.COMPUTE_MAXS)// ClassWriter是ASM core API的类 
klass.accept(writer) // 接收一个writer
// writer把被洗礼后的klass,即ClassNode对象,
// 写成ByteArray返回到上层BoosterTransformInvocation
return writer.toByteArray()

关于ASM相关的知识说明:

  • ASM的模型基本理念、术语
  • ASM的对class文件操作是基于JVM标准中,对class文件内容的规范来进行的,所以要理解class的相关规范,JVM执行模型及方法调用过程的工作原理、字节码指令的格式和运作、opcode格式等
  • ASM Core API 和 Tree API的区别
  • 了解API 对class和method的读写操作基本够用

3.2 ClassTransform.kt

ClassTransform是其他booster-transform-xxx模块,需要进行字节码插桩时候,需要具体实现的接口。

/**
 * Represents class transformer
 *
 * @author johnsonlee
 */
interface ClassTransformer : TransformListener {
    /**
     * Transform the specified class node
     *
     * @param context The transform context
     * @param klass The class node to be transformed
     * @return The transformed class node
     */
    fun transform(context: TransformContext, klass: ClassNode) = klass
}

实现者同时要实现TransformListener,可以在transform执行前后,游刃有余地处理资源准备和释放。

当然啦,实现者也要使用@AutoService注解,不然AsmTransform是找不到。


4.booster-task-spi

相比较transform-spi,task-spi就显得简单一些了。

package com.didiglobal.booster.task.spi

import com.android.build.gradle.api.BaseVariant

interface VariantProcessor {

    fun process(variant: BaseVariant)
}

其实现主要使用场景:

  • 通过BaseVariant,添加自定义task,并作为编译task的依赖,保证自动执行
  • 为project添加booster-android-instrument-xxx相关的模块依赖

具体的代码,在下一篇文章中再说明。


5.booster-android-instrument

在说instrument之前,先说booster-android-api模块。

image.png

看其源码你就会发现,它并没有提供什么实质的API。再看看:

image.png

噢,都是依赖类型compileOnly,所以这个模块的作用,其实是为了booster-instrument-xxx模块中,编写HOOK代码的时候,能有一定的Android API代码编译环境,没什么特别。

回到booster-android-instrument,可以发现都是仅适用java,并没有使用kotlin。

CaughtCallback.java // 代理Handler, 捕获handleMessage()的RuntimeException
CaughtRunnable.java // 代理Runnable, 捕获run()的RuntimeException
Constants.java // android.util.Log的TAG,全局统一使用
Reflection.java // 抽象类,提供常用的反射静态方法

因为booster-android-instrument-xxx模块,是提供具体HOOK方法的实现,HOOK需用利用反射对某些对象进行代理置换,所以这个模块负责提供工具方法。


6.booster-android-gradle-api

image.png

主要对AGP API类进行方法扩展,以及定义一些transform中用到的类。

6.1 ResolvedArtifactResults.kt

类型上是ResolvedArtifactResult的集合。

ResolvedArtifactResult类图

ResolvedArtifactResults.kt中,关键属性的results初始化过程:

results = 
    listOf(AndroidArtifacts.ArtifactType.AAR, AndroidArtifacts.ArtifactType.JAR) //指定构件类型列表为AAR和JAR
    .asSequence() // -> Sequence<AndroidArtifacts.ArtifactType>
    .map { // map转换 
        variant.variantData.scope
            .getArtifactCollection(
                AndroidArtifacts.ConsumedConfigType.RUNTIME_CLASSPATH, 
                AndroidArtifacts.ArtifactScope.ALL, 
                it) 
        
    } // -> Sequence<ArtifactCollection> 构件集合 
    .map { 
        it.artifacts // ArtifactCollection.getArtifacts()
    }  // -> Sequence<ResolvedArtifactResult> 
    .flatten() // 平铺
    .filter { it.id.componentIdentifier !is ProjectComponentIdentifier } // 过滤ResolvedArtifactResult中,构件标识类型 != ProjectComponentIdentifier
    .toList() // -> List<ResolvedArtifactResult>
    .distinctBy { it.id.componentIdentifier.displayName } // 按构件名称去重
    .sortedBy { it.id.componentIdentifier.displayName } // 按构件名称排序
    .toList() // 得到结果

结论:ResolvedArtifactResults解析、提取Android模块项目中,依赖构件文件类型为AAR和JAR,且构件标识为ProjectComponentIdentifier类型的构件,并且按名称去重、排序,得到一个列表results,并围绕这个列表,提供最大名称长度、最大构建文件长度等变量。

6.2 ComponentHandler.kt

class ComponentHandler : DefaultHandler() {

    val applications = mutableSetOf<String>()
    val activities = mutableSetOf<String>()
    val services = mutableSetOf<String>()
    val providers = mutableSetOf<String>()
    val receivers = mutableSetOf<String>()

    override fun startElement(uri: String, localName: String, qName: String, attributes: Attributes) {
        val name: String = attributes.getValue(ATTR_NAME) ?: return
        when (qName) {
            "application" -> applications.add(name)
            "activity" -> activities.add(name)
            "service" -> services.add(name)
            "provider" -> providers.add(name)
            "receiver" -> receivers.add(name)
        }
    }
}

专门解析AndroidManifest.xml,提取并缓存Application、及所有的四大组件信息列表。


总结

Gradle生命周期、ASM API、Java SPI机制、Kotlin语法、AGP,基本上涵盖了我在阅读源码时候需要运用的知识层面。

之于我个人,booster的这套架构展示了上述知识点的”组合拳拳法“,特别是在架构设计上展示职责解耦、扩展灵活,值得模仿学习。

在下一篇文章中,我打算接着分析各个功能模块的技术点和原理。