滴滴開源庫Booster:架構運做及源碼分析

概述

Booster庫是什麼這裏就很少說了,本文嘗試經過源碼分析,表述對Booster工做流程及相關細節的理解。html

通過我的理解及梳理得出如下的說明圖,展現了Booster庫在Gradle的兩個重要生命週期中,發生的行爲:java

image.png


驟眼一看項目源碼會發現有不少模塊,感受不能一會兒掌控。不要緊,經過手敲模塊名稱,按名稱前綴規律、構建依賴共同點等特徵分類,我得出瞭如下的劃分:node

簡述android

相關模塊api

各模塊基礎依賴、kotlin的基礎擴展緩存

buildSrc、booster-kotlinx性能優化

對Project、AGP中的BaseVariant、VariantScope、TransformInvocation等類添加方法擴展,bash

booster-gradle-api、booster-gradle-v3_0架構

booster-gradle-v3_二、booster-gradle-v3_3app

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的這套架構展現了上述知識點的」組合拳拳法「,特別是在架構設計上展現職責解耦、擴展靈活,值得模仿學習。

在下一篇文章中,我打算接着分析各個功能模塊的技術點和原理。

相關文章
相關標籤/搜索