preface
For those of you who know about Compose, you only need to add an @compose annotation to turn the Compose function into the Compose function, and the Compose function can only be run inside the Compose function. This might seem a bit like a coroutine. Is @compose adding some parameters to the function like a coroutine?
Let’s take a look at what @compose does and how it does it.
Front knowledge
The @compose annotation is a simple way to think about the @compose annotation processor. However, the @compose resolution is not implemented through the annotation processor, because the annotation processor can only generate code and cannot modify it. KCP (Kotlin Compiler Plugin) : The kotlin compiler plugin is cross-platform and can be likened to the Kapt + Transform mechanism for Android developers to generate and modify code
The @compose annotation is resolved using KCP
What is theKCP
Kotlin
The compilation process, in a nutshell, is to convertKotlin
The process of compiling source code into bytecode is as follows:
The Kotlin compile-time plug-in provides Hook time during compilation, allowing us to parse symbols, modify bytecode generation results, and so on. Kotlin libraries use KCP for many syntactic candy, such as Kotlin-Android-extension, @parcelize, etc. The @compose annotation is also parsed by KCP
Compared with KAPT, KCP has the following advantages:
KAPT
Is based on the annotation processor, which needs to integrateKotlin
Code toStub
The annotations are then parsed to generate code, often converted toStub
Takes longer than generating code, whileKCP
It’s direct parsingKotlin
Symbol, therefore on compilation speedKCP
thanKAPT
Much betterKAPT
Can only generate code, cannot modify code, andKCP
You can not only generate code, you can also modify code, you can view it askapt
+transorm
mechanism
However, the disadvantage of KCP is that the development cost of KCP is too high, which involves the use of Gradle Plugin and Kotlin Plugin, etc., and the API involves some compiler knowledge, which is difficult for common developers to master. So if you just need to deal with the annotation generation code, you don’t need to modify the code, it is usually enough to use KSP, which is a wrapper around KCP. If you are interested in using KSP, see Goodbye KAPT! Speed up Kotlin compilation using KSP
KCP
The basic concept of
And as it says,KCP
The development cost is high, mainly including the following:
Plugin
:Gradle
Plugins for readingGradle
Configuration pass toKCP
(Kotlin Plugin
)Subplugin
: in order toKCP
Providing customizationKP
的maven
Configuration information such as the library addressCommandLineProcessor
: responsible for thePlugin
The passed parameters are converted and verifiedComponentRegistrar
: Responsible for putting all kinds of user – definedExtension
Registered toKP
, and call it when appropriate
ComponentRegistrar is the core entry point. All the KCP customization functions need to be registered via the Extension interface via this class.
Here are some common Extension interfaces that you can use as required:
IrGenerationExtension
, used to add/delete/change/look up codeDiagnosticSuppressor
, used to suppress syntax errors,Jetpack Compose
Have to useStorageComponentContainerContributor
, used to implementIOC
@Compose
The role of annotations
The @compose annotation is composed for Jetpack Compose. The @compose annotation is composed for Jetpack Compose
registeredIrGenerationExtension
The ComponentRegistrar is the core entry point to register various user-defined extensions into KP and call them at the appropriate time. And IrGenerationExtension can be used to modify the code composer plug-in entry for ComposePlugin, including a ComposeComponentRegistrar, Registration for IrGenerationExtension is done here
class ComposeComponentRegistrar : ComponentRegistrar {
override fun registerProjectComponents(
project: MockProject,
configuration: CompilerConfiguration
) {
registerProjectExtensions(
project as Project,
configuration
)
}
fun registerProjectExtensions(
project: Project,
configuration: CompilerConfiguration
) {
IrGenerationExtension.registerExtension(
project,
ComposeIrGenerationExtension(
/ /...))}}Copy the code
As shown above, the IrGenerationExtension is registered, and then the IrGenerationExtension calls the relevant methods of ComposerParamTransformer to complete the parameter filling. Much of the subsequent processing is done in ComposerParamTransformer
add$Composer
Said that on the subsequent add parameters in the function of work is mainly done in ComposerParamTransformer, specific call IrFunction. WithComposerParamIfNeeded
private fun IrFunction.withComposerParamIfNeeded(a): IrFunction {
// If it is not the 'Compose' function, it returns directly and is not processed later
if (!this.hasComposableAnnotation()) {
return this
}
// If the function is a 'Lambda' as an argument and is not a 'Compose' function, return it directly
if (isNonComposableInlinedLambda()) return this
// Do not handle expect functions
if (isExpect) return this
// Cache the result of the conversion
return transformedFunctions[this] ?: copyWithComposerParam()
}
Copy the code
As shown above, the main idea is to determine if the function has an @compose annotation. If not, the function returns no more processing, and if it does, it continues processing and caches the result. Then call copyWithComposerParam
private fun IrFunction.copyWithComposerParam(a): IrSimpleFunction {
/ /...
return copy().also { fn ->
// $composer
val composerParam = fn.addValueParameter {
name = KtxNameConventions.COMPOSER_PARAMETER
type = composerType.makeNullable()
origin = IrDeclarationOrigin.DEFINED
isAssignable = true
}
/ /...}}Copy the code
As shown above, a $Composer is inserted into all Compose functions, which effectively makes Composer available to any subtree, providing all the information needed to implement the Composable tree and keep it updated.
add$changed
We know that Compose has an intelligent recombination mechanism that allows the recombination to be skipped when the input is exactly the same, and that the compiler will inject the $CHANGED parameter in addition to $Composer. This parameter is used to provide whether the input parameters for the current Composable are the same as after the component occurred, allowing the reorganization to be skipped if they are.
private fun IrFunction.copyWithComposerParam(a): IrSimpleFunction {
/ /...
return copy().also { fn ->
// $changed[n]
val changed = KtxNameConventions.CHANGED_PARAMETER.identifier
//changedparamCount, calculates the amount of $changed
for (i in 0 until changedParamCount(realParams, fn.thisParamCount)) {
fn.addValueParameter(
if (i == 0) changed else "$changed$i",
context.irBuiltIns.intType
)
}
/ /...}}Copy the code
As shown above, $changed[n] was added. Where did the n come from? This is because there are five states for each parameter, and an enumeration is defined in compose as follows:
enum class ParamState(val bits: Int) {
Uncertain(0b000),
Same(0b001),
Different(0b010),
Static(0b011),
Unknown(0b100),
Mask(0b111);
}
Copy the code
As shown above, $changed uses a bit operation to indicate whether the parameter has changed:
$changed
isInt
Type, one of 32 bits- Each parameter has five types, so a parameter needs three bits to represent it
- So a
$changed
It can indicate whether 10 parameters have changed. If more parameters are exceeded, one more parameter needs to be added$changed
parameter
The compiler injected $changed to look like this:
@Composable
fun A(x: Int, $composer: Composer<*>, $changed: Int) {
var $dirty = $changed
if ($changed and 0b0110= = =0) {
$dirty = $dirty or if ($composer.changed(x)) 0b0010 else 0b0100
}
if (%dirty and 0b1011! = =0b1010| |! $composer.skipping) { f(x) }else {
$composer.skipToGroupEnd()
}
}
Copy the code
add$default
The default arguments supported by Kotlin do not apply to arguments of composable functions, because composable functions need to execute default expressions for their arguments within the function’s scope (the generated group). For this purpose, Compose provides an alternative implementation of the default parameter resolution mechanism. Add $defaulut to the Compose method
private fun IrFunction.copyWithComposerParam(a): IrSimpleFunction {
/ /...
// $default[n]
if (oldFn.requiresDefaultParameter()) {
val defaults = KtxNameConventions.DEFAULT_PARAMETER.identifier
for (i in 0 until defaultParamCount(realParams)) {
fn.addValueParameter(
if (i == 0) defaults else "$defaults$i",
context.irBuiltIns.intType,
IrDeclarationOrigin.MASK_FOR_DEFAULT_FUNCTION
)
}
}
/ /...
}
Copy the code
$default = $changed; $default = $changed; $default = $changed; If not, add another $changed and the compiler will inject $default as follows:
@Composable
fun A(x: Int, $default: Int) {
val x = if ($default and 0b1! =0) 0 else x
f(x)
}
Copy the code
conclusion
This article provides a brief introduction to what KCP is and how it handles the @compose annotation. It shows how powerful and complex KCP can be. If you only need to parse the annotation generation code, you can use KSP instead of KAPT. Try using KCP and see how the Compose design hides all the complexity behind the Compose framework. It’s neat and elegant to use a simple @compose annotation to turn a normal function into a Compose function. Interested students can also directly view the source ~
The resources
Say goodbye to KAPT! Speeding up Kotlin Compilation with KSP Kotlin compiler plug-in: What are we looking forward to?