詳解Android Gradle生成位元組碼流程

newtonker發表於2019-12-23

本文首發於知乎專欄:詳解Android Gradle生成位元組碼流程

一、背景

當前絕大部分的Android工程都是使用Gradle框架搭配Android Gradle Plugin(以下簡稱AGP)和Kotlin Gradle Plugin(以下簡稱KGP)進行編譯構建的。雖然市面上有很多入門介紹,但是分析其中實現細節的文章並不多。這篇文章主要介紹了AGP和KGP生成位元組碼的核心流程,通過這些介紹,讀者將瞭解到Java類和Kotlin類是如何被編譯為位元組碼的,並學習到一些加快編譯速度的最佳實踐。

二、準備工作

為了加深理解位元組碼的生成過程,讀者需要如下一些背景知識:

2.1 Gradle基礎

學習Gradle基礎,可以參考深入理解Android之Gradle【Android 修煉手冊】Gradle 篇 -- Gradle 的基本使用這兩篇文章,重點掌握以下幾點:

  • Task是Gradle構建中最核心的概念,Android工程的構建過程也是被分成了無數個Task按照一定的順序執行,最後輸出apk產物;
  • Gradle構建生命週期分三個階段:初始化階段,配置階段和執行階段,每個階段都有不同的作用;
  • Gradle構建過程中有三個非常重要的類:Gradle,Project,Setting,每個類都有不同的作用。

2.2 AGP和KGP構建流程

AGP是谷歌團隊為了支援Android工程構建所開發的外掛。AGP在Gradle的基礎上,新增了一些與Android工程構建相關的Task。AGP的基本構建流程可以參考【Android 修煉手冊】Android Gradle Plugin 外掛主要流程。如果我們在工程中也使用了Kotlin語言來開發,則需要依賴KGP外掛來編譯Kotlin檔案。

只有找到外掛的入口類,才能分析外掛的原始碼。Android專案中每個子工程的build.gradle指令碼檔案通過apply引用的外掛id,實際上也是該外掛入口類宣告的檔名。比如:“com.android.application”和“kotlin-android”外掛的入口分別為AppPlugin和KotlinAndroidPluginWrapper,入口宣告如下:

AppPlugin入口
KotlinAndroidPluginWrapper入口

2.3 工程和除錯

StudyGradleDemo是一個Demo工程,可以用於除錯AGP和KGP編譯過程,也可以用於閱讀和分析AGP、KGP原始碼,讀者可按需自行下載。

  • AGP: 依賴包gradle-3.5.0-source.jar,入口類:AppPlugin
  • KGP: 依賴包kotlin-gradle-plugin-1.3.50-source.jar,入口類:KotlinAndroidPluginWrapper

Gradle除錯方法可以參照官方教程Debugging build logic。總結來說:

  • 建立Remote除錯任務;
  • 命令列輸入開始除錯命令,注意紅框中的任務也可以改成其他debug的任務:
    Gradle除錯
  • 點選Android Studio中的debug按鈕開啟除錯;

2.4 其他說明

  • AGP和KGP當下依然在不定期的升級,不同的版本,類中方法的實現可能有所不同,但是核心的實現應該都一樣。本文基於的原始碼版本如下:
    • gradle-wrapper.properites/gradle-5.6.1-all.zip
    • build.gradle.kts/org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.50
    • build.gradle.kts/com.android.tools.build:gradle:3.5.0

三、Java類檔案位元組碼編譯流程

3.1 任務名

compile(Flavor)JavaWithJavac

3.2 實現類

AndroidJavaCompile

3.3 整體實現圖

java類檔案編譯流程

如上圖所示:當編譯Java類檔案時,AndroidJavaCompile和JavaCompile首先做一些預處理操作,如校驗註解型別,判斷編譯配置是否允許增量編譯等。如果配置為增量編譯,則使用SelectiveCompiler對輸入做全量/增量的判斷(注意並不是所有的修改都會進行增量編譯,有些修改可能會觸發全量編譯),這些判斷是在JavaRecompilationSpecProvider的processClasspathChanges和processOtherChanges方法中完成。如果判斷結果為全量編譯,則直接走接下來的編譯流程;如果判斷結果為增量編譯,還會進一步確定修改的影響範圍,並把所有受到影響的類都作為編譯的輸入,再走接下來的編譯流程。最後的編譯流程是使用JdkJavaCompiler執行編譯任務,用javac將類檔案編譯為位元組碼。

3.4 呼叫鏈路

這裡給出了Java類檔案生成位元組碼的核心呼叫鏈路(實現類和具體方法),讀者可參考該呼叫鏈路自行翻閱原始碼。

/* ------ 編譯java檔案準備階段 ------ */
-> AndroidJavaCompile.compile
-> JavaCompile.compile
/* ------ 兩種編譯方式可選,本例選擇跟蹤:增量編譯 ------ */
-> JavaCompile.performCompilation
-> CompileJavaBuildOperationReportingCompiler.execute
-> IncrementalResultStoringCompiler.execute
-> SelectiveCompiler.execute
/* ------ 搜尋增量編譯範圍 ------ */
-> JavaRecompilationSpecProvider.provideRecompilationSpec
-> JavaRecompilationSpecProvider.processOtherChanges
-> InputChangeAction.execute
-> SourceFileChangeProcessor.processChange
-> PreviousCompilation.getDependents
-> ClassSetAnalysis.getRelevantDependents
/* ------ 編譯任務執行 ------ */
-> CleaningJavaCompilerSupport.execute
-> AnnotationProcessorDiscoveringCompiler.execute
-> NormalizingJavaCompiler.execute
-> JdkJavaCompiler.execute
-> JavacTaskImpl.call
-> JavacTaskImpl.doCall
/* ------ javac執行階段 ------ */
-> Main.compile
-> JavaCompiler.compile
-> JavaCompiler.compile2
複製程式碼

3.5 主要程式碼分析

compile(Flavor)JavaWithJavac任務的入口類是AndroidJavaCompile。執行時該類首先做了註解的校驗工作,然後再將類檔案編譯位元組碼。本節將從註解處理,編譯方式,位元組碼生成,JdkJavaCompiler的擴充設計四個方面進行介紹,其他環節請讀者自行查閱原始碼。

3.5.1 註解處理

為了高效開發,我們往往會自定義一些註解來生成模板程式碼。在編譯過程中,處理註解有兩種方式:一種是直接在compile(Flavor)JavaWithJavac的Task中處理,一種是建立獨立的Task處理。獨立的Task又分為ProcessAnnotationsTask和KaptTask兩種。

  • 建立ProcessAnnotationsTask處理註解要求滿足如下三點:
    • 設定了增量編譯(無論是使用者主動設定還是DSL預設設定);
    • build.gradle中沒有使用kapt依賴註解處理器;
    • 使能了BooleanOption.ENABLE_SEPARATE_ANNOTATION_PROCESSING標誌位;
  • 如果build.gradle中使用kapt依賴註解處理器(常見於純Kotlin工程或者Kotlin、Java混合工程),則:
    • 不會建立ProcessAnnotationsTask;
    • 建立KaptTask且該Task只處理註解,不處理編譯;
    • AndroidJavaCompile和KotlinCompile只編譯,不處理註解;
  • 如果build.gradle中沒有使用kapt依賴註解處理器(常見於純Java工程),則:
    • 如果建立了ProcessAnnotationsTask,那麼ProcessAnnotationsTask將負責處理註解,AndroidJavaCompile只負責進行編譯,不處理註解。
    • 如果沒有建立ProcessAnnotationsTask,那麼AndroidJavaCompile將會處理註解和編譯;

AndroidJavaCompile中處理註解的原始碼如下,當var3不為空時,在編譯位元組碼前會先處理註解。

// com.sun.tool.javac.main.JavaCompiler.java
public void compile(List<JavaFileObject> var1, List<String> var2, Iterable<? extends Processor> var3) {
    ...
    this.initProcessAnnotations(var3);
    this.delegateCompiler = this.processAnnotations(this.enterTrees(this.stopIfError(CompileState.PARSE, this.parseFiles(var1))), var2);
    ...
}
複製程式碼
3.5.2 編譯方式

一般而言,我們首次開啟工程或者執行了clean project操作之後,編譯器會把工程中的全部檔案編譯一次,把編譯過程中的一些中間產物進行快取,即為全量編譯。如果後面又觸發了一次編譯,編譯器首先會把變化內容和之前快取的內容做對比,找出所有需要重新編譯的檔案,然後只對這些檔案進行重新編譯,其他的仍然複用之前的快取,即為增量編譯。通常來講,增量編譯的速度肯定快於全量編譯,平時開發過程中,我們用到更多的應該也是增量編譯。

將Java類檔案編譯為位元組碼支援全量編譯和增量編譯兩種方式。當編譯配置支援增量編譯時,AGP會在JavaRecompilationSpecProvider類的processClasspathChanges方法和processOtherChanges方法中拿當前輸入的修改內容和之前快取的編譯內容做對比。下面給出了processOtherChanges方法的原始碼,可以看出AGP主要從原始檔、註解處理器,資源等方面進行了對比。

// JavaRecompilationSpecProvider.java
private void processOtherChanges(CurrentCompilation current, PreviousCompilation previous, RecompilationSpec spec) {
	SourceFileChangeProcessor javaChangeProcessor = new SourceFileChangeProcessor(previous);
	AnnotationProcessorChangeProcessor annotationProcessorChangeProcessor = new AnnotationProcessorChangeProcessor(current, previous);
	ResourceChangeProcessor resourceChangeProcessor = new ResourceChangeProcessor(current.getAnnotationProcessorPath());
	InputChangeAction action = new InputChangeAction(spec, javaChangeProcessor, annotationProcessorChangeProcessor, resourceChangeProcessor, this.sourceFileClassNameConverter);
	this.inputs.outOfDate(action);
	this.inputs.removed(action);
}
複製程式碼

如果輸入的修改內容滿足了全量編譯的條件,則會觸發全量編譯;否則會執行增量編譯。全量/增量判斷的示意圖如下:

java增量編譯校驗
上圖中的判斷條件是通過除錯原始碼提煉出來的,從這些判斷條件可以看出,開發過程中一些不經意的書寫習慣可能會觸發全量編譯,所以我們應該有意識地改變這些書寫習慣。另外Gradle官網也對一些判斷條件作了解釋,詳情參閱Incremental Java compilation

除了上述情況外,編譯過程還有一個非常重要的概念:類的依賴鏈。舉個例子:定義了一個類A,然後類B引用了類A,然後類C有使用類B的一個方法,然後類D又引用了類C,這樣A-B-C-D就構成一條類的依賴鏈。假如類A被修改了,AGP會用遞迴的方式找出所有這個類A相關的類依賴鏈,本例中即為A-B-C-D。在得到整個類依賴鏈之後,AGP會把這個依賴鏈作為輸入進行編譯,如此一來,看似只是修改了一個類,實際被編譯的可能是多個類檔案。如果依賴鏈複雜,只修改一個類卻編譯上千的類也不是不可能,這樣就出現了compile(Flavor)JavaWithJavac非常耗時的情況。AGP中遞迴搜尋類的依賴鏈原始碼如下:

// ClassSetAnalysis.java
private void recurseDependentClasses(Set<String> visitedClasses, Set<String> resultClasses, Set<GeneratedResource> resultResources, Iterable<String> dependentClasses) {
	Iterator var5 = dependentClasses.iterator();
	while(var5.hasNext()) {
		String d = (String)var5.next();
		if (visitedClasses.add(d)) {
			if (!this.isNestedClass(d)) {
				resultClasses.add(d);
			}
			DependentsSet currentDependents = this.getDependents(d);
			if (!currentDependents.isDependencyToAll()) {
				resultResources.addAll(currentDependents.getDependentResources());
				this.recurseDependentClasses(visitedClasses, resultClasses, resultResources, currentDependents.getDependentClasses());
			}
		}
	}
}
複製程式碼

AGP為什麼不只編譯當前修改的類,而是要編譯整個類依賴鏈呢?筆者認為這其實涉及到自動化編譯中一個非常重要的問題:在通用場景下,自動化編譯的自動化邊界如何確定?比如本例中:AGP如何知道被修改的檔案是否會影響其下游?這個問題很難回答,通常需要結合具體的場景來分析。AGP作為一個通用的編譯工具,首要考慮的應該是準確性,在保證準確性的基礎上再考慮速度問題。所以AGP增量編譯的方案編譯了整個類的依賴鏈。在開發過程中,我們可以從實際場景出發,在速度和準確性方面做出一定的取捨,如:release包要發到線上必須要正確性,而debug階段為了加快編譯速度,儘快看到效果,不追求絕對正確性,這樣就可以針對性的做出優化了。

3.5.3 位元組碼生成

在增量編譯確定了最終的輸入類檔案後,接下來的任務就是將類檔案編譯為位元組碼,即javac執行過程。AGP的javac過程最終是通過呼叫JDK 的Java Compiler API來實現的。javac將Java類編譯成位元組碼檔案需要經過語法解析、詞法解析、語義解析、位元組碼生成四個步驟。如下圖:

javc位元組碼生成
javac過程是深入理解JVM的一部分,我們在此就不做深入介紹了,讀者可以自行查閱。javac最終通過Gen類將語義解析後的語法樹轉換成位元組碼,並將位元組碼寫入*.class檔案。

3.5.4 JdkJavaCompiler的擴充設計

javac最終執行前需要提前做一些準備工作,如編譯引數的校驗,收集註解處理器;執行後也需要做一些處理工作,如對返回結果的封裝,日誌記錄等;AGP使用了裝飾模式來實現這一流程,下面是其中一層裝飾的原始碼:

// DefaultJavaCompilerFactory.java
public Compiler<JavaCompileSpec> create(Class<? extends CompileSpec> type) {
    Compiler<JavaCompileSpec> result = this.createTargetCompiler(type, false);
    return new AnnotationProcessorDiscoveringCompiler(new NormalizingJavaCompiler(result), this.processorDetector);
}

// AnnotationProcessorDiscoveringCompiler.java
public class AnnotationProcessorDiscoveringCompiler<T extends JavaCompileSpec> implements Compiler<T> {
    private final Compiler<T> delegate;
    private final AnnotationProcessorDetector annotationProcessorDetector;

    public AnnotationProcessorDiscoveringCompiler(Compiler<T> delegate, AnnotationProcessorDetector annotationProcessorDetector) {
        this.delegate = delegate;
        this.annotationProcessorDetector = annotationProcessorDetector;
    }

    public WorkResult execute(T spec) {
        Set<AnnotationProcessorDeclaration> annotationProcessors = this.getEffectiveAnnotationProcessors(spec);
        spec.setEffectiveAnnotationProcessors(annotationProcessors);
        return this.delegate.execute(spec);
    }
    ...
}
複製程式碼

我們先分析DefaultJavaCompilerFactory類中的create方法,這個方法首先通過createTargetCompiler()方法建立了一個目標Compiler(debug可以發現是JdkJavaCompiler),然後將該目標Compiler作為構造引數建立了NormalizingJavaCompiler,最後將NormalizingJavaCompiler例項作為構造引數建立了AnnotationProcessorDiscoveringCompiler,並將該例項返回。這些Compiler類都繼承了Compiler介面,最終負責執行的是介面中的execute方法。從AnnotationProcessorDiscoveringCompiler的execute方法中,我們可以看到先執行了getEffectiveAnnotationProcessors方法去搜尋有效的註解處理器,最後呼叫了delegate的execute方法,也就是繼續執行NormalizingJavaCompiler的execute方法,以此類推,最後再執行JdkJavaCompiler的execute方法。

由此可見,AGP在生成位元組碼的過程中,建立了多層裝飾來將核心的位元組碼生成功能和其他一些裝飾功能區分開,這樣設計可以簡化核心Compiler類,也有了更好的擴充性,這種設計思路是我們需要學習的一點。整個位元組碼生成過程中Compiler裝飾關係如下圖所示:

java檔案編譯裝飾關係

四、Kotlin類檔案位元組碼編譯流程

4.1 任務名

compile(Flavor)Kotlin

4.2 實現類

KotlinCompile, CompileServiceImpl

4.3 整體實現圖

kotlin整體實現圖

如上圖所示:編譯Kotlin類檔案時,先由KotlinCompile做一些準備工作,如建立臨時輸出檔案等。然後啟動編譯服務CompileService,並在該服務的實現類CompileServiceImpl中完成全量編譯和增量編譯的判斷工作,最後由K2JVMCompiler執行編譯,用kotlinc將Kotlin類檔案編譯為位元組碼。

4.4 呼叫鏈路

這裡給出了Kotlin類檔案生成位元組碼的核心呼叫鏈路(實現類和具體方法),讀者可參考該呼叫鏈路自行翻閱原始碼。

/* ------ 編譯kotlin檔案準備階段,配置環境及引數 ------ */
-> KotlinCompile.callCompilerAsync
-> GradleCompilerRunner.runJvmCompilerAsync
-> GradleCompilerRunner.runCompilerAsync
-> GradleKotlinCompilerWork.run
-> GradleKotlinCompilerWork.compileWithDaemonOrFallbackImpl
/* ------ 三種編譯策略可選,本例選擇跟蹤:daemon策略 ------ */
-> GradleKotlinCompilerWork.compileWithDaemon
/* ------ 兩種編譯方式可選,本例選擇跟蹤:增量編譯 ------ */
-> GradleKotlinCompilerWork.incrementalCompilationWithDaemon
/* ------ 啟動編譯服務 ------ */
-> CompileServiceImpl.compile
-> CompileServiceImplBase.compileImpl
-> CompileServiceImplBase.doCompile
/* ------ 執行增量編譯 ------ */
-> CompileServiceImplBase.execIncrementalCompiler
-> IncrementalCompilerRunner.compile
-> IncrementalCompilerRunner.compileIncrementally
-> IncrementalJvmCompilerRunner.runCompiler
/* ------ kotlinc執行階段 ------ */
-> CLITool.exec
-> CLICompiler.execImpl
-> K2JVMCompiler.doExecute
-> KotlinToJVMBytecodeCompiler.compileModules
複製程式碼

4.5 主要程式碼分析

在AbstractAndroidProjectHandler類中有這樣一段程式碼:

// AbstractAndroidProjectHandler.kt
internal fun configureJavaTask(kotlinTask: KotlinCompile, javaTask: AbstractCompile, logger: Logger) {
	...
	javaTask.dependsOn(kotlinTask)
	...
}
複製程式碼

我們可以看到Kotlin檔案位元組碼編譯是在Java檔案位元組碼編譯之前完成的。為什麼要把Kotlin編譯放到Java編譯之前呢?官方並沒有給出解釋,所以這裡的理解就仁者見仁智者見智了,一種比較合理的解釋是:一般來講,語言的發展都是向前相容的,即後來的語言會相容之前語言的特性。我們開發過程中很多情況下都是Kotlin和Java程式碼相互之間混合呼叫的,所以理論上來講,如果Kotlin工程依賴了Java的Library工程應該是可以相容並編譯成功的,反過來如果Java工程依賴了Kotlin的Library工程可能就會出現不相容的情況,所以應該先編譯Kotlin的檔案。

compile(Flavor)Kotlin任務的入口類是KotlinCompile,執行時該類首先做一些編譯準備工作,如引數校驗工作,然後再將類檔案編譯位元組碼。本節將重點介紹編譯策略,編譯方式,位元組碼生成三個部分的實現,其他部分請讀者自行查閱原始碼。

4.5.1 編譯策略

從GradleKotlinCompilerWork類的compileWithDaemonOrFallbackImpl方法中,我們可以看到在Kotlin檔案編譯過程中,根據編譯引數設定的不同,有三種可選的編譯策略:daemon, in-process, out-of-process。三種編譯策略的差異主要體現在編譯任務的執行方式上:

  • daemon策略:在daemon程式中啟動編譯服務,後續將Kotlin檔案編譯為位元組碼都由該服務完成,支援增量編譯,預設採用此策略;
  • in-process策略:直接在當前執行緒中將Kotlin編譯為位元組碼,該策略不支援增量編譯,一般除錯編譯過程可以嘗試此策略;
  • out-of-process策略:新起一個程式來將Kotlin編譯為位元組碼,程式起失敗則編譯失敗,該策略不支援增量編譯。

按筆者理解:daemon策略應該是編譯最快的策略,out-of-process策略應該是編譯最慢的策略,in-process策略應該介於這兩個策略之間。因為通常來講,在Gradle開啟編譯流程前就已經啟動了daemon程式,daemon策略下可以直接啟動編譯服務並執行編譯過程,這樣原程式也可以去並行執行其他任務,並且還支援增量編譯;而out-of-process策略需要啟動一個全新的程式,並且不支援增量編譯,所以編譯耗時應該最久;有時為了方便除錯,可以考慮使用in-process策略。

那應該怎麼配置編譯策略呢?有兩種配置方式:

  • 在全域性的gradle.property(注意:全域性的gradle目錄一般是/User/.gradle/gradle.property,gradle.property不存在時需新建,而非當前工程的gradle.property)下使用如下配置:

    kotlin.compiler.execution.strategy=???(可選項:daemon/in-process/out-of-process)
    org.gradle.daemon=???(可選項:true/false)
    複製程式碼
  • 在除錯命令後增加除錯引數,指定編譯策略。示例如下:

    > ./gradlew <task> -Dorg.gradle.debug=true -Dkotlin.compiler.execution.strategy=in-process -Dorg.gradle.daemon=false
    複製程式碼
4.5.2 編譯方式

和AGP一樣,KGP同樣支援增量編譯和全量編譯兩種方式。編譯過程是否採用增量編譯主要取決於KotlinCompile類的incremental屬性,該屬性初始化時被設定為true,並且後續的編譯過程並沒有修改該屬性,所以KGP預設支援增量編譯。增量編譯的核心判斷原始碼如下:

// KotlinCompile.kt
init {
	incremental = true
}

// GradleKotlinCompilerWork.kt
private fun compileWithDaemon(messageCollector: MessageCollector): ExitCode? {
	...
	val res = if (isIncremental) {
		incrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
	} else {
		nonIncrementalCompilationWithDaemon(daemon, sessionId, targetPlatform, bufferingMessageCollector)
	}
	...
}
複製程式碼

同AGP一樣,KGP會在IncrementalJvmCompilerRunner類的calculateSourcesToCompile方法中進行全量/增量編譯的判斷,滿足全量編譯的條件則會觸發全量編譯,否則會執行增量編譯。全量/增量判斷的示意圖如下:

kotlin增量編譯校驗
執行增量編譯前,KGP也會通過遞迴的方式搜尋出類的編譯鏈,搜尋結果將作為增量編譯的輸入。在增量編譯完成後,KGP會將增量編譯的中間產物和原有快取的中間產物合併,並更新快取。KGP最終是通過IncrementalCompilerRunner類的compileIncrementally方法來執行增量編譯的。上述過程的原始碼如下:

// IncrementalCompilerRunner.kt
private fun compileIncrementally(args: Args, caches: CacheManager, allKotlinSources: List<File>, compilationMode: CompilationMode, messageCollector: MessageCollector): ExitCode {
	...
	val complementaryFiles = caches.platformCache.getComplementaryFilesRecursive(dirtySources)
	...
	exitCode = runCompiler(sourcesToCompile.toSet(), args, caches, services, messageCollectorAdapter)
	...
	caches.platformCache.updateComplementaryFiles(dirtySources, expectActualTracker)
	...
}
複製程式碼
4.5.3 位元組碼生成

確定了最終輸入後,接下來便是生成位元組碼,即kotlinc執行過程。執行kotlinc的入口是K2JVMCompiler的doExecute方法。這個方法首先會配置編譯的引數,並做一些編譯準備工作(比如建立臨時資料夾和臨時輸出檔案),準備工作結束後呼叫KotlinToJVMBytecodeCompiler的repeatAnalysisIfNeeded做詞法分析、語法分析和語義分析,最後呼叫DefaultCodegenFactory的generateMultifileClass方法來生成位元組碼。Kotlin類檔案生成位元組碼的流程圖如下:

kotlin位元組碼生成
如上圖所示:kotlic在詞法分析、語法分析、語義分析這些流程上和javac基本一致,但是目的碼生成階段與javac有較大的區別。這裡的區別主要有兩點:一是雙方生成位元組碼的方式不一樣,javac通過自帶的Gen類生成位元組碼,kotlinc通過ASM生成位元組碼;二是kotlinc在這個階段通過各種Codegen做了很多自身語法糖的解析工作。比如屬性自動生成Getter/Setter程式碼、reified修飾的方法中解析過程等。由此可見:我們在誇kotlin語言簡潔的時候,實際上編譯器在編譯過程中幫我們做了很多的轉換工作。Kotlin語法糖解析原始碼示例:

// PropertyCodegen.java
private void gen(@NotNull KtProperty declaration, @NotNull PropertyDescriptor descriptor, @Nullable KtPropertyAccessor getter, @Nullable KtPropertyAccessor setter) {
	...
	if (isAccessorNeeded(declaration, descriptor, getter, isDefaultGetterAndSetter)) {
	    generateGetter(descriptor, getter);
	}
	if (isAccessorNeeded(declaration, descriptor, setter, isDefaultGetterAndSetter)) {
	    generateSetter(descriptor, setter);
	}
}
複製程式碼

五、最佳實踐

通過上述分析,相信讀者已經對Android工程中Java類檔案和Kotlin類檔案生成位元組碼的過程瞭然於胸了。下面我們來總結一些最佳實踐來避免本應增量編譯卻觸發全量編譯的情況發生,從而加快編譯的速度。

5.1 修復增量編譯失效

增量編譯失效,意味著本次修改將會進行全量編譯,那麼編譯時間必然會增加,所以我們應該從以下幾個方面來改善我們的程式碼:

  • BuildConfig中的itemValue如果存在動態變化的值,建議區分場景,如release包變,開發除錯包不變;

  • 將註解處理器修改為支援增量的註解處理器,修改方法請參考官網Incremental annotation processing

  • 如果類中有定義一些公有靜態常量需要被外部引用,嘗試改為靜態方法去獲取,而不是直接引用,例如:

    public class Constants {
    	private static String TAG = "Constans";
    	
    	// 暴露靜態方法給外部引用
    	public static String getTag() {
    		return TAG;
    	}
    }
    複製程式碼

5.2 類編譯鏈過長

  • 為了避免類的依賴鏈過長,我們應該儘可能拆分解耦業務,如推進元件化,並將模組之間的依賴關係改為二進位制依賴而非原始碼依賴。只有這樣,才有可能減少類依賴鏈的長度,進而減少Task的執行時間。

六、總結

至此,Java類和Kotlin類生成位元組碼的流程就介紹完了,最後我們來總結一下:編譯Java類時,AGP通過AndroidJavaCompile先做一些預處理操作,然後進行全量/增量編譯的判斷,最終通過javac生成位元組碼。編譯Kotlin類時,KGP通過KotlinCompile先做一些準備工作,然後進行全量/增量編譯的判斷,最終通過kotlinc生成位元組碼。最後,為了加快編譯速度,本文給出了最佳實踐。

七、參考資料

相關文章