詳解Android Gradle生成字節碼流程

本文首發於知乎專欄:詳解Android Gradle生成字節碼流程html

1、背景

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

2、準備工做

爲了加深理解字節碼的生成過程,讀者須要以下一些背景知識:android

2.1 Gradle基礎

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

  • 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文件。github

只有找到插件的入口類,才能分析插件的源碼。Android項目中每一個子工程的build.gradle腳本文件經過apply引用的插件id,實際上也是該插件入口類聲明的文件名。好比:「com.android.application」和「kotlin-android」插件的入口分別爲AppPlugin和KotlinAndroidPluginWrapper,入口聲明以下: 緩存

AppPlugin入口
KotlinAndroidPluginWrapper入口

2.3 工程和調試

StudyGradleDemo是一個Demo工程,能夠用於調試AGP和KGP編譯過程,也能夠用於閱讀和分析AGP、KGP源碼,讀者可按需自行下載。session

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

Gradle調試方法能夠參照官方教程Debugging build logic。總結來講:app

  • 建立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

3、Java類文件字節碼編譯流程

3.1 任務名

compile(Flavor)JavaWithJavac框架

3.2 實現類

AndroidJavaCompileide

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文件編譯裝飾關係

4、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);
	}
}
複製代碼

5、最佳實踐

經過上述分析,相信讀者已經對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的執行時間。

6、總結

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

7、參考資料

相關文章
相關標籤/搜索