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

KotlinThe compilation process, in a nutshell, is to convertKotlinThe 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:

  1. KAPTIs based on the annotation processor, which needs to integrateKotlinCode toStubThe annotations are then parsed to generate code, often converted toStubTakes longer than generating code, whileKCPIt’s direct parsingKotlinSymbol, therefore on compilation speedKCPthanKAPTMuch better
  2. KAPTCan only generate code, cannot modify code, andKCPYou can not only generate code, you can also modify code, you can view it askapt+transormmechanism

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

KCPThe basic concept of

And as it says,KCPThe development cost is high, mainly including the following:

  • Plugin:GradlePlugins for readingGradleConfiguration pass toKCP(Kotlin Plugin)
  • Subplugin: in order toKCPProviding customizationKPmavenConfiguration information such as the library address
  • CommandLineProcessor: responsible for thePluginThe passed parameters are converted and verified
  • ComponentRegistrar: Responsible for putting all kinds of user – definedExtensionRegistered 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 code
  • DiagnosticSuppressor, used to suppress syntax errors,Jetpack ComposeHave to use
  • StorageComponentContainerContributor, used to implementIOC

@ComposeThe 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:

  1. $changedisIntType, one of 32 bits
  2. Each parameter has five types, so a parameter needs three bits to represent it
  3. So a$changedIt can indicate whether 10 parameters have changed. If more parameters are exceeded, one more parameter needs to be added$changedparameter

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?