美文网首页AndroidKotlin
Kotlin-KCP的应用-第二篇

Kotlin-KCP的应用-第二篇

作者: guodongAndroid | 来源:发表于2022-05-12 15:37 被阅读0次

    前言

    Kotlin-KCP的应用-第一篇,本文是第二篇,以下是本文的目标:

    1. 记录如何简单搭建 KCP 开发环境
    2. 使用 KCP 解决第一篇中的问题

    何为KCP?为何不使用KSP?

    KSP

    KSPKotlin Symbol Processing(Kotlin符号处理器),KSP 目前只能生成代码,不能修改字节码,第一篇中的问题需要修改字节码,因此 KSP 不能满足需求

    KCP

    KCPKotlin Compiler Plugin(Kotlin编译器插件),在 kotlinc 过程中提供 hook 时机,在此期间可以生成代码、修改字节码等

    标准的 KCP 架构如下[1]

    plugin-architecture

    Plugin

    • Gradle 插件,与 Kotlin 无关,在 build.gradle 脚本中提供一个入口
    • 通过 Gradle 扩展设置配置信息

    Subplugin

    • 介于 Gradle 和 Kotlin 直接的 APIs 接口
    • 读取 Gradle 扩展配置信息并写入 SubpluginOptions
    • 定义编译器插件的唯一ID
    • 定义 Kotlin 插件的 Maven 坐标信息,便于编译器下载它

    CommandLinProcessor

    • 设置 Kotlin 插件唯一 ID
    • 读取 kotlinc -Xplugin 参数
    • 读取 SubpluginOptions 配置信息,并写入 CompilerConfigurationKeys

    ComponentRegistrar

    • 读取 CompilerConfigurationKeys
    • 注册 Extension 到各编译流程

    Extension

    • 生成代码
    • 修改字节码
    • 多种类型的扩展,比如
      • ExpressionCodegenExtension
      • ClassBuilderInterceptorExtension
      • StorageComponentContainerContributor
      • IrGenerationExtension

    实现KCP

    目标

    根据 KCP 的架构,下面一一进行实现

    仓库组织架构

    上图是本仓库架构,旨在通过 KCP 在 Java 字节码中 @Hide 注解目标上设置 ACC_SYNTHETIC 标识,使其在 Java 中不能正常调用,达到隐藏 API 的效果

    build.gradle - project level

    在项目级别的 build.gradle 脚本中配置插件依赖

    buildscript {
        // 配置 Kotlin 插件唯一ID
        ext.kotlin_plugin_id = "com.guodong.android.mask.kcp"
        
        // 配置 Kotlin 插件版本
        ext.plugin_version = 'x.x.x'
    }
    
    plugins {
        // 配置 Gradle 发布插件,可以不再写 META-INF
        id("com.gradle.plugin-publish") version "0.16.0" apply false
        
        // 配置生成 BuildConfig 插件
        id("com.github.gmazzo.buildconfig") version "3.0.3" apply false
        id 'org.jetbrains.kotlin.jvm' version '1.6.10' apply false
    }
    

    plugin-gradle

    接下来编写 Gradle 插件,此插件对应 KCP 架构中的 PluginSubplugin

    首先配置下 build.gradle.kts 脚本

    build.gradle.kts - module level

    plugins {
        id("java-gradle-plugin")
        kotlin("jvm")
        id("com.github.gmazzo.buildconfig")
    }
    
    dependencies {
        implementation(kotlin("gradle-plugin-api"))
    }
    
    buildConfig {
        // 配置 BuildConfig 的包名
        packageName("com.guodong.android.mask.kcp.gradle")
        
        // 设置 Kotlin 插件唯一 ID
        buildConfigField("String", "KOTLIN_PLUGIN_ID", "\"${rootProject.extra["kotlin_plugin_id"]}\"")
        
        // 设置 Kotlin 插件 GroupId
        buildConfigField("String", "KOTLIN_PLUGIN_GROUP", "\"com.guodong.android\"")
        
        // 设置 Kotlin 插件 ArtifactId
        buildConfigField("String", "KOTLIN_PLUGIN_NAME", "\"mask-kcp-kotlin-plugin\"")
        
        // 设置 Kotlin 插件 Version
        buildConfigField("String", "KOTLIN_PLUGIN_VERSION", "\"${rootProject.extra["PLUGIN_VERSION"]}\"")
    }
    
    gradlePlugin {
        plugins {
            create("Mask") {
                id = rootProject.extra["kotlin_plugin_id"] as String // `apply plugin: "com.guodong.android.mask.kcp"`
                displayName = "Mask Kcp"
                description = "Mask Kcp"
                implementationClass = "com.guodong.android.mask.kcp.gradle.MaskGradlePlugin" // 插件入口类
            }
        }
    }
    
    tasks.withType<KotlinCompile> {
        kotlinOptions.jvmTarget = "1.8"
    }
    

    MaskGradlePlugin

    创建 MaskGradlePlugin 实现 KotlinCompilerPluginSupportPlugin 接口

    class MaskGradlePlugin : KotlinCompilerPluginSupportPlugin {
    
        override fun apply(target: Project) = with(target) {
            logger.error("Welcome to guodongAndroid mask kcp gradle plugin.")
            
            // 此处可以配置 Gradle 插件扩展
            // 本插件没有配置项, 无需配置扩展
        }
    
        // 是否适用, 默认True
        override fun isApplicable(kotlinCompilation: KotlinCompilation<*>): Boolean = true
    
        // 获取 Kotlin 插件唯一ID
        override fun getCompilerPluginId(): String = BuildConfig.KOTLIN_PLUGIN_ID
    
        // 获取 Kotlin 插件 Maven 坐标信息
        override fun getPluginArtifact(): SubpluginArtifact = SubpluginArtifact(
            groupId = BuildConfig.KOTLIN_PLUGIN_GROUP,
            artifactId = BuildConfig.KOTLIN_PLUGIN_NAME,
            version = BuildConfig.KOTLIN_PLUGIN_VERSION,
        )
    
        // 读取 Gradle 插件扩展信息并写入 SubpluginOption
        // 本插件没有扩展信息,所以返回空集合
        override fun applyToCompilation(kotlinCompilation: KotlinCompilation<*>): Provider<List<SubpluginOption>> {
            val project = kotlinCompilation.target.project
            return project.provider { emptyList() }
        }
    }
    

    至此 Gradle 插件编写完成,是不是很简单:smile:

    plugin-kotlin

    接下来编写 Kotlin 编译器插件,此插件对应 KCP 架构中的 CommandLineProcessorComponentRegistrarExtension

    首先配置下 build.gradle.kts 脚本

    build.gradle.kts - module level

    plugins {
        kotlin("jvm")
        kotlin("kapt")
        id("com.github.gmazzo.buildconfig")
    }
    
    dependencies {
        // 依赖 Kotlin 编译器库
        compileOnly("org.jetbrains.kotlin:kotlin-compiler-embeddable")
    
        // 依赖 Google auto service
        kapt("com.google.auto.service:auto-service:1.0")
        compileOnly("com.google.auto.service:auto-service-annotations:1.0")
    }
    
    buildConfig {
        // 配置 BuildConfig 的包名
        packageName("com.guodong.android.mask.kcp.kotlin")
        
        // 设置 Kotlin 插件唯一 ID
        buildConfigField("String", "KOTLIN_PLUGIN_ID", "\"${rootProject.extra["kotlin_plugin_id"]}\"")
    }
    
    tasks.withType<KotlinCompile> {
        kotlinOptions.jvmTarget = "1.8"
    }
    

    根据 KCP 架构,下面实现 CommandLineProcessor

    MaskCommandLineProcessor

    创建 MaskCommandLineProcessor 实现 CommandLineProcessor 接口

    @AutoService(CommandLineProcessor::class)
    class MaskCommandLineProcessor : CommandLineProcessor {
    
        // 配置 Kotlin 插件唯一 ID
        override val pluginId: String = BuildConfig.KOTLIN_PLUGIN_ID
    
        // 读取 `SubpluginOptions` 参数,并写入 `CliOption`
        // 本插件没有配置信息,返回空集合
        override val pluginOptions: Collection<AbstractCliOption> = emptyList()
        
        // 处理 `CliOption` 写入 `CompilerConfiguration`
        // 本插件没有配置信息,此处没有实现
        override fun processOption(
            option: AbstractCliOption,
            value: String,
            configuration: CompilerConfiguration
        ) {
            super.processOption(option, value, configuration)
        }
    }
    

    CommandLineProcessor 编写完成

    接下来实现 ComponentRegistrar

    MaskComponentRegistrar

    创建 MaskComponentRegistrar 实现 ComponentRegistrar 接口

    @AutoService(ComponentRegistrar::class)
    class MaskComponentRegistrar : ComponentRegistrar {
    
        override fun registerProjectComponents(
            project: MockProject,
            configuration: CompilerConfiguration
        ) {
            // 获取日志收集器
            val messageCollector =
                configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)
    
            // 输出日志,查看是否执行
            // CompilerMessageSeverity.INFO - 没有看到日志输出
            // CompilerMessageSeverity.ERROR - 编译过程停止执行
            messageCollector.report(
                CompilerMessageSeverity.WARNING,
                "Welcome to guodongAndroid mask kcp kotlin plugin"
            )
    
            // 此处在 `ClassBuilderInterceptorExtension` 中注册扩展
            ClassBuilderInterceptorExtension.registerExtension(
                project,
                MaskClassGenerationInterceptor(messageCollector)
            )
        }
    }
    

    最后实现 Extension

    MaskClassGenerationInterceptor

    创建 MaskClassGenerationInterceptor 实现 ClassBuilderInterceptorExtension 接口

    class MaskClassGenerationInterceptor(
        private val messageCollector: MessageCollector,
    ) : ClassBuilderInterceptorExtension {
    
        // 拦截 ClassBuilderFactory
        override fun interceptClassBuilderFactory(
            interceptedFactory: ClassBuilderFactory,
            bindingContext: BindingContext,
            diagnostics: DiagnosticSink
            // 自定义 ClassBuilderFactory 委托给 源ClassBuilderFactory
        ): ClassBuilderFactory = object : ClassBuilderFactory by interceptedFactory {
            
            // 复写 newClassBuilder,自定义 ClassBuilder
            override fun newClassBuilder(origin: JvmDeclarationOrigin): ClassBuilder {
                // 传入源ClassBuilder
                return MaskClassBuilder(messageCollector, interceptedFactory.newClassBuilder(origin))
            }
        }
    }
    

    MaskClassBuilder

    创建 MaskClassBuilder 继承 DelegatingClassBuilder,实现 getDelegate 方法,复写 newFieldnewMethod 方法

    getDelegate
    class MaskClassBuilder(
        private val messageCollector: MessageCollector,
        private val delegate: ClassBuilder
    ) : DelegatingClassBuilder() {
    
        // @Hide注解的完全限定名(fully qualified name)
        private val annotations: List<FqName> = listOf(
            FqName("com.guodong.android.mask.api.kt.Hide"),
            FqName("com.guodong.android.mask.api.Hide")
        )
    
        // 返回代理
        override fun getDelegate(): ClassBuilder = delegate
    }
    
    newField

    处理 @Hide 注解目标为字段

    class MaskClassBuilder(
        private val messageCollector: MessageCollector,
        private val delegate: ClassBuilder
    ) : DelegatingClassBuilder() {
    
        // @Hide注解的完全限定名(fully qualified name)
        private val annotations: List<FqName> = listOf(
            FqName("com.guodong.android.mask.api.kt.Hide"),
            FqName("com.guodong.android.mask.api.Hide")
        )
    
        override fun newField(
            origin: JvmDeclarationOrigin,
            access: Int,
            name: String,
            desc: String,
            signature: String?,
            value: Any?
        ): FieldVisitor {
            // 判断描述符是否是字段
            val field = origin.descriptor as? FieldDescriptor
                ?: return super.newField(origin, access, name, desc, signature, value)
    
            // 判断字段上是否有`@Hide`注解
            if (annotations.none { field.annotations.hasAnnotation(it) }) {
                return super.newField(origin, access, name, desc, signature, value)
            }
    
            // 输出字段源访问标志
            messageCollector.report(
                CompilerMessageSeverity.WARNING,
                "Mask Class = ${delegate.thisName}, fieldName = $name, originalAccess = $access"
            )
    
            // 增加`ACC_SYNTHETIC`标识
            val maskAccess = access + Opcodes.ACC_SYNTHETIC
    
            // 输出字段Mask后访问标志
            messageCollector.report(
                CompilerMessageSeverity.WARNING,
                "Mask Class = ${delegate.thisName}, fieldName = $name, maskAccess = $maskAccess"
            )
    
            // 传入Mask后访问标志
            return super.newField(origin, maskAccess, name, desc, signature, value)
        }
    
    }
    
    newMethod

    处理 @Hide 注解目标为方法/函数,处理逻辑与字段类似

    override fun newMethod(
        origin: JvmDeclarationOrigin,
        access: Int,
        name: String,
        desc: String,
        signature: String?,
        exceptions: Array<out String>?
    ): MethodVisitor {
        // 判断是否是方法/函数描述符
        val function = origin.descriptor as? FunctionDescriptor
            ?: return super.newMethod(origin, access, name, desc, signature, exceptions)
    
        // 判断方法/函数上是否有`@Hide`注解
        if (annotations.none { function.annotations.hasAnnotation(it) }) {
            return super.newMethod(origin, access, name, desc, signature, exceptions)
        }
    
        // 输出方法/函数源访问标志
        messageCollector.report(
            CompilerMessageSeverity.WARNING,
            "Mask Class = ${delegate.thisName}, methodName = $name, originalAccess = $access"
        )
    
        // 增加`ACC_SYNTHETIC`标识
        val maskAccess = access + Opcodes.ACC_SYNTHETIC
    
        // 输出方法/函数Mask后访问标志
        messageCollector.report(
            CompilerMessageSeverity.WARNING,
            "Mask Class = ${delegate.thisName}, methodName = $name, maskAccess = $maskAccess"
        )
    
        // 传入Mask后访问标志
        return super.newMethod(origin, maskAccess, name, desc, signature, exceptions)
    }
    

    至此 Kotlin 编译器插件完成:happy:

    应用KCP

    build.gradle - project level

    buildscript {
        ext.plugin_version = 'x.x.x'
        dependencies {
            classpath "com.guodong.android:mask-kcp-gradle-plugin:${plugin_version}"
        }
    }
    

    lib-kotlin/build.gradle - module level

    # lib-kotlin
    plugins {
        id 'com.android.library'
        id 'kotlin-android'
        id 'kotlin-kapt'
        id 'maven-publish'
        // id 'com.guodong.android.mask' // use kcp
        id 'com.guodong.android.mask.kcp'
    }
    

    lib-kotlin

    interface InterfaceTest {
    
        // 使用api-kt中的注解
        @Hide
        fun testInterface()
    }
    
    class KotlinTest(a: Int) : InterfaceTest {
    
        // 使用api-kt中的注解
        @Hide
        constructor() : this(-2)
    
        companion object {
    
            @JvmStatic
            fun newKotlinTest() = KotlinTest()
        }
    
        private val binding: LayoutKotlinTestBinding? = null
    
        // 使用api-kt中的注解
        var a = a
            @Hide get
            @Hide set
    
        fun getA1(): Int {
            return a
        }
    
        fun test() {
            a = 1000
        }
    
        override fun testInterface() {
            println("Interface function test")
        }
    }
    

    app

    # MainActivity.java
    
    private void testKotlinLib() {
        // 创建对象时不能访问无参构造方法,可以访问有参构造方法或访问静态工厂方法
        KotlinTest test = KotlinTest.newKotlinTest();
        // 调用时不能访问`test.getA()`方法,仅能访问`getA1()方法
        Log.e(TAG, "testKotlinLib: before --> " + test.getA1());
        test.test();
        Log.e(TAG, "testKotlinLib: after --> " + test.getA1());
        
        test.testInterface();
        
        InterfaceTest interfaceTest = test;
        // Error - cannot resolve method 'testInterface' in 'InterfaceTest'
        interfaceTest.testInterface();
    }
    

    happy:happy:

    参考文献


    1. [Writing Your First Kotlin Compiler Plugin (jetbrains.com)](https://resources.jetbrains.com/storage/products/kotlinconf2018/slides/5_Writing Your First Kotlin Compiler Plugin.pdf)

    相关文章

      网友评论

        本文标题:Kotlin-KCP的应用-第二篇

        本文链接:https://www.haomeiwen.com/subject/eoosurtx.html