增量處理
增量處理是一種處理技術,它會盡可能避免對原始碼進行重複處理。 增量處理的主要目標是縮短典型變更-編譯-測試週期的週轉時間。 有關一般資訊,請參閱維基百科關於增量計算的文章。
為了判斷哪些原始碼是 髒污的 (需要重新處理的),KSP 需要處理器的幫助來識別 哪些輸入原始碼對應到哪些生成的輸出。為了解決這個通常繁瑣且容易出錯的過程, KSP 設計成只要求最少量的 根源碼,處理器將其用作導航程式碼結構的起始點。 換句話說,如果 KSNode
是從以下任何一個方法獲得的,處理器就需要將輸出與對應 KSNode
的原始碼關聯起來:
Resolver.getAllFiles
Resolver.getSymbolsWithAnnotation
Resolver.getClassDeclarationByName
Resolver.getDeclarationsFromPackage
增量處理目前預設啟用。要停用它,請設定 Gradle 屬性 ksp.incremental=false
。 要啟用根據依賴項和輸出傾印髒污集合的日誌,請使用 ksp.incremental.log=true
。 你可以在 build
輸出目錄中找到這些副檔名為 .log
的日誌檔。
在 JVM 上,類路徑變更以及 Kotlin 和 Java 原始碼變更預設會被追蹤。 若要僅追蹤 Kotlin 和 Java 原始碼變更,請透過設定 ksp.incremental.intermodule=false
Gradle 屬性來停用類路徑追蹤。
聚合式 (Aggregating) 與 隔離式 (Isolating)
與 Gradle 註解處理中的概念類似, KSP 同時支援 聚合式 (aggregating) 和 隔離式 (isolating) 模式。請注意,與 Gradle 註解處理不同,KSP 是將 每個輸出歸類為聚合式或隔離式,而不是整個處理器。
聚合式輸出可能會受到任何輸入變更的影響,除非是移除不影響其他檔案的檔案。 這表示任何輸入變更都會導致所有聚合式輸出的重新建構, 這反過來意味著重新處理所有對應的已註冊、新增和修改的原始碼檔案。
舉例來說,一個收集所有帶有特定註解的符號的輸出被視為聚合式輸出。
隔離式輸出僅依賴於其指定的原始碼。對其他原始碼的變更不會影響隔離式輸出。 請注意,與 Gradle 註解處理不同,你可以為給定輸出定義多個原始碼檔案。
舉例來說,一個專門用於其所實作介面的生成類別被視為隔離式。
總之,如果輸出可能依賴於新增或任何變更的原始碼,則被視為聚合式。 否則,輸出是隔離式。
以下是熟悉 Java 註解處理的讀者摘要:
- 在隔離式 Java 註解處理器中,所有輸出在 KSP 中都是隔離式。
- 在聚合式 Java 註解處理器中,某些輸出可以是隔離式,某些可以是聚合式。
實作方式
依賴項是透過輸入和輸出檔案的關聯來計算的,而不是透過註解。 這是一種多對多關係。
由於輸入-輸出關聯而導致的髒污傳播規則是:
- 如果輸入檔案發生變更,它將始終被重新處理。
- 如果輸入檔案發生變更,且它與輸出相關聯,則所有與相同輸出相關聯的其他輸入檔案也將被重新處理。 這是遞移的,即失效會重複發生,直到沒有新的髒污檔案為止。
- 所有與一個或多個聚合式輸出相關聯的輸入檔案都將被重新處理。 換句話說,如果輸入檔案未與任何聚合式輸出相關聯,它就不會被重新處理 (除非它符合上述第 1 或第 2 條)。
原因如下:
- 如果輸入發生變更,可以引入新資訊,因此處理器需要再次使用該輸入運行。
- 一個輸出由一組輸入構成。處理器可能需要所有輸入才能重新生成輸出。
aggregating=true
表示輸出可能潛在地依賴於新資訊,這可以來自新檔案,或已變更的現有檔案。aggregating=false
表示處理器確信資訊僅來自某些輸入檔案,而絕不來自其他或新檔案。
範例 1
一個處理器在讀取 A.kt
中的類別 A
和 B.kt
中的類別 B
後生成 outputForA
,其中 A
繼承自 B
。 處理器透過 Resolver.getSymbolsWithAnnotation
取得 A
,然後透過 A
的 KSClassDeclaration.superTypes
取得 B
。 因為包含 B
是由於 A
,所以 outputForA
的 dependencies
中不需要指定 B.kt
。 在此情況下,你仍然可以指定 B.kt
,但這是不必要的。
// A.kt
@Interesting
class A : B()
// B.kt
open class B
// Example1Processor.kt
class Example1Processor : SymbolProcessor {
override fun process(resolver: Resolver) {
val declA = resolver.getSymbolsWithAnnotation("Interesting").first() as KSClassDeclaration
val declB = declA.superTypes.first().resolve().declaration
// B.kt 不是必需的,因為 KSP 可以將其推導為依賴項
val dependencies = Dependencies(aggregating = true, declA.containingFile!!)
// outputForA.kt
val outputName = "outputFor${declA.simpleName.asString()}"
// outputForA 依賴於 A.kt 和 B.kt
val output = codeGenerator.createNewFile(dependencies, "com.example", outputName, "kt")
output.write("// $declA : $declB
".toByteArray())
output.close()
}
// ...
}
範例 2
假設一個處理器在讀取 sourceA
後生成 outputA
,並在讀取 sourceB
後生成 outputB
。
sourceA
變更時:
- 如果
outputB
是聚合式,則sourceA
和sourceB
都會被重新處理。 - 如果
outputB
是隔離式,則只有sourceA
會被重新處理。
sourceC
新增時:
- 如果
outputB
是聚合式,則sourceC
和sourceB
都會被重新處理。 - 如果
outputB
是隔離式,則只有sourceC
會被重新處理。
sourceA
移除時,不需要重新處理任何內容。
sourceB
移除時,不需要重新處理任何內容。
檔案髒污的判斷方式
髒污檔案是由使用者直接 變更,或間接被其他髒污檔案 影響。KSP 以兩個步驟傳播髒污:
- 透過 解析追蹤 傳播: 解析型別引用 (隱式或顯式) 是從一個檔案導航到另一個檔案的唯一方式。 當處理器解析型別引用時,一個已變更或受影響的檔案如果包含可能影響解析結果的變更,將會影響包含該引用的檔案。
- 透過 輸入-輸出對應 傳播: 如果原始碼檔案發生變更或受影響,則所有與該檔案有共同輸出的其他原始碼檔案都會受影響。
請注意,這兩者都是遞移的,且第二者形成等價類別。
回報錯誤
要回報錯誤,請設定 Gradle 屬性 ksp.incremental=true
和 ksp.incremental.log=true
,並執行一次乾淨的建構。 這次建構會產生兩個日誌檔:
build/kspCaches/<source set>/logs/kspDirtySet.log
build/kspCaches/<source set>/logs/kspSourceToOutputs.log
接著你可以運行連續的增量建構,這會產生兩個額外的日誌檔:
build/kspCaches/<source set>/logs/kspDirtySetByDeps.log
build/kspCaches/<source set>/logs/kspDirtySetByOutputs.log
這些日誌包含原始碼和輸出的檔案名稱,以及建構的時間戳記。