前言
接Kotlin-KCP的利用-第一篇,本文是第二篇,以下是本文的指标:
- 记录如何简略搭建 KCP 开发环境
- 应用 KCP 解决第一篇中的问题
何为KCP?为何不应用KSP?
KSP
KSP
即 Kotlin Symbol Processing(Kotlin符号处理器)
,KSP 目前只能生成代码,不能批改字节码,第一篇中的问题须要批改字节码,因而 KSP 不能满足需要
KCP
KCP
即 Kotlin Compiler Plugin(Kotlin编译器插件)
,在 kotlinc
过程中提供 hook 机会,在此期间能够生成代码、批改字节码等
规范的 KCP 架构如下1:
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 架构中的 Plugin
和 Subplugin
首先配置下 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 插件编写实现,是不是很简略
plugin-kotlin
接下来编写 Kotlin 编译器插件,此插件对应 KCP 架构中的 CommandLineProcessor
、 ComponentRegistrar
和 Extension
首先配置下 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
办法,复写 newField
和 newMethod
办法
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-kotlinplugins { 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.javaprivate 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:
参考文献
- Writing Your First Kotlin Compiler Plugin (jetbrains.com) ↩