Kotlin-KCP的应用-修改SDK版本号

背景 在 SDK 开发中,一般会暴露获取 SDK 版本号的接口,获取的版本号一般为 String 类型,比如:

// sdk接口 interface Sdk { fun getVersion(): String }// sdk调用方 sdk.getVersion()

上述方式可以通过在 gradle.properties 中配置版本号,然后在 build.gradle 中读取版本号生成至 BuildConfig.java 中,例如:
// gradle.properties VERSION=1.0.0.0// builde.gradle android { defaultConfig { buildConfigField("String", "SDK_VERSION", "\"$VERSION\"") } }// SdkImple.kt class SdkImpl : Sdk { override fun getVersion(): String { // 返回 BuildConfig 中的 SDK_VERSION return BuildConfig.SDK_VERSION } }

上述方式在 SDK 发版时只需修改 gradle.properties 中的版本号即可
但是上述方式有一个弊端:SDK 提供的版本号为 String 类型,第三方根据版本号进行适配开发时不太方便,第三方需要自己实现版本号大小的判断,笔者希望 SDK 自身可以暴露判断版本号大小的接口
方案 基于上述需求,SDK 暴露的获取版本号接口就不能返回 String 类型了,Sdk 接口修改如下:
interface Sdk { // 返回一个 Version 对象 fun getVersion(): Version }class SdkImpl : Sdk { override fun getVersion(): Version { // 返回 Version 中的 CURRENT return Version.CURRENT } }

下面是 Version 对象的定义1,版本号规则不尽相同,以下是示例:
class Version internal constructor( private val major: Int, // 主版本 1 private val minor: Int, // 次版本 0 private val patch: Int, // 补丁版本 0 private val extra: Int, // 保留版本 0 private val suffix: String?, // 后缀版本, 比如:alpha01、beta01 ) : Comparable {private val version = versionOf(major, minor, patch, extra)// 版本校验 private fun versionOf(major: Int, minor: Int, patch: Int, extra: Int): Int { require( major in 0..MAX_COMPONENT_VALUE && minor in 0..MAX_COMPONENT_VALUE && patch in 0..MAX_COMPONENT_VALUE && extra in 0..MAX_COMPONENT_VALUE ) { "Version components are out of range: $major.$minor.$patch.$extra" } return major.shl(24) + minor.shl(16) + patch.shl(8) + extra }override fun toString(): String = if (suffix.isNullOrEmpty()) "$major.$minor.$patch.$extra" else "$major.$minor.$patch.$extra-$suffix"override fun equals(other: Any?): Boolean { if (this === other) return true val otherVersion = (other as? Version) ?: return false return this.version == otherVersion.version }override fun hashCode(): Int = version// 版本比较1 override fun compareTo(other: Version): Int = version - other.version// 版本比较2 fun isAtLeast(major: Int, minor: Int): Boolean = this.major > major || (this.major == major && this.minor >= minor)// 版本比较2 fun isAtLeast(major: Int, minor: Int, patch: Int): Boolean = this.major > major || (this.major == major && (this.minor > minor || this.minor == minor && this.patch >= patch))// 版本比较2 fun isAtLeast(major: Int, minor: Int, patch: Int, extra: Int): Boolean = this.major > major || (this.major == major && (this.minor > minor || this.minor == minor && (this.patch > patch || this.patch == patch && this.extra >= extra)))companion object { internal const val MAX_COMPONENT_VALUE = https://www.it610.com/article/255// 当前版本 @JvmField val CURRENT: Version = VersionCurrentValue.get() } }private object VersionCurrentValue { @JvmStatic fun get(): Version = Version(0, 0, 0, 0, null) // value is written here automatically during build }

第三方进行版本适配开发时,可以如下操作,就比较方便了:
val version = sdk.getVersion() println("version = $version")if (version.isAtLeast(1, 2)) { // 当前版本大于等于 1.2.0.0 // do something } else { // 当前版本小于 1.2.0.0 // do something }

上述方案是不是比较友好了?:happy:,不知道读者有没有发现,在哪里修改版本号呢?
细心的读者可能已经发现,Version.CURRENT 是调用的 VersionCurrentValue#get() 方法,VersionCurrentValue#get() 方法会创建 Version 对象的实例,只需要修改 VersionCurrentValue#get() 方法传入版本号即可。等下,每次发版时都要修改 VersionCurrentValue#get() 方法?
隐隐感觉到一丝不妥,要是哪次发版时忘记修改 VersionCurrentValue#get() 方法,这不惨了
“人非圣贤孰能无过” 呢,还是让程序帮我们生成版本号吧,同时兼容方案一:只修改 gradle.properties 即可
使用 KCP 在编译阶段修改 VersionCurrentValue#get() 方法
实现 在上篇 Kotlin-KCP的应用-第二篇 中笔者记录了搭建 KCP 环境的基本步骤,这里不再赘述,有兴趣的读者可以先看下上篇文章
Kotlin-KCP的应用-修改SDK版本号
文章图片

上图是本项目的组织架构,简单介绍下:
  • sample:包含 Version 及测试类
  • version-plugin-gradle:kcp 中的 gradle plugin 部分
  • version-plugin-kotlin:kcp 中的 kotlin compiler plugin 部分
sample 模块不做介绍,下面主要实现其他两个模块
build.gradle.kts - project level
在项目级别的 build.gradle.kts 脚本中配置插件依赖
buildscript { // 配置 Kotlin 插件唯一ID extra["kotlin_plugin_id"] = "com.guodong.android.version.kcp" }plugins { kotlin("jvm") version "1.5.31" apply false// 配置 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 }allprojects { // 配置 Kotlin 插件版本 version = "0.0.1" }

version-plugin-gradle
首先配置下 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.version.kcp.plugin.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", "\"version-kcp-kotlin-plugin\"")// 设置 Kotlin 插件 Version buildConfigField("String", "KOTLIN_PLUGIN_VERSION", "\"${project.version}\"") }gradlePlugin { plugins { create("Version") { id = rootProject.extra["kotlin_plugin_id"] as String // `apply plugin: "com.guodong.android.version.kcp"` displayName = "Version Kcp" description = "Version Kcp" implementationClass = "com.guodong.android.version.kcp.gradle.VersionGradlePlugin" // 插件入口类 } } }tasks.withType { kotlinOptions.jvmTarget = "1.8" }

VersionGradlePlugin 创建 VersionGradlePlugin 实现 KotlinCompilerPluginSupportPlugin 接口
class VersionGradlePlugin : KotlinCompilerPluginSupportPlugin {override fun apply(target: Project): Unit = with(target) { logger.error("Welcome to guodongAndroid-version kcp gradle plugin.")// 此处配置 Gradle 插件扩展 extensions.create("version", VersionExtension::class.java) }// 是否适用, 默认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 { val project = kotlinCompilation.target.project val extension = project.extensions.getByType(VersionExtension::class.java) return project.provider { listOf( SubpluginOption(key = "version", value = https://www.it610.com/article/extension.version) ) } } }

因为版本号需要在外部配置传入 Gradle Plugin,这里需要创建 VersionExtension:
open class VersionExtension {var version: String = "0.0.0.0"override fun toString(): String { return "VersionExtension(version=$version)" } }

至此 Gradle 插件编写完成
version-plugin-kotlin
【Kotlin-KCP的应用-修改SDK版本号】接下来编写 Kotlin 编译器插件,首先配置下 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.version.kcp.plugin.kotlin")// 设置 Kotlin 插件唯一 ID buildConfigField("String", "KOTLIN_PLUGIN_ID", "\"${rootProject.extra["kotlin_plugin_id"]}\"") }tasks.withType { kotlinOptions.jvmTarget = "1.8" }

VersionCommandLineProcessor 实现 CommandLineProcessor
@AutoService(CommandLineProcessor::class) class VersionCommandLineProcessor : CommandLineProcessor {companion object { // OptionName 对应 VersionGradlePlugin#applyToCompilation() 传入的 Key private const val OPTION_VERSION = "version"// ConfigurationKey val ARG_VERSION = CompilerConfigurationKey(OPTION_VERSION) }// 配置 Kotlin 插件唯一 ID override val pluginId: String = BuildConfig.KOTLIN_PLUGIN_ID// 读取 `SubpluginOptions` 参数,并写入 `CliOption` override val pluginOptions: Collection = listOf( CliOption( optionName = OPTION_VERSION, valueDescription = "string", description = "version string", required = true, ) )// 处理 `CliOption` 写入 `CompilerConfiguration` override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) { when (option.optionName) { OPTION_VERSION -> configuration.put(ARG_VERSION, value) else -> throw IllegalArgumentException("Unexpected config option ${option.optionName}") } } }

VersionComponentRegistrar 实现 ComponentRegistrar
@AutoService(ComponentRegistrar::class) class VersionComponentRegistrar( private val defaultVersion: String, ) : ComponentRegistrar {companion object { internal const val DEFAULT_VERSION = "0.0.0.0" }@Suppress("unused") // Used by service loader constructor() : this(DEFAULT_VERSION)override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { // 获取日志收集器 val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY, MessageCollector.NONE)// 获取传入的版本号 val version = configuration.get(VersionCommandLineProcessor.ARG_VERSION, defaultVersion)// 输出日志,查看是否执行 // CompilerMessageSeverity.INFO - 没有看到日志输出 // CompilerMessageSeverity.ERROR - 编译过程停止执行 messageCollector.report(CompilerMessageSeverity.STRONG_WARNING, "Welcome to guodongAndroid-version kcp kotlin plugin")// 此处在 `ClassBuilderInterceptorExtension` 中注册扩展 ClassBuilderInterceptorExtension.registerExtension( project, VersionClassGenerationInterceptor( messageCollector = messageCollector, // 传入版本号 version = version ) ) } }

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

VersionClassBuilder
class VersionClassBuilder( private val messageCollector: MessageCollector, private val version: String, private val delegate: ClassBuilder, ) : DelegatingClassBuilder() {companion object { private const val VERSION_NAME = "com/guodong/android/VersionCurrentValue" private const val MIN_COMPONENT_VALUE = https://www.it610.com/article/0 private const val MAX_COMPONENT_VALUE = 255 }override fun getDelegate(): ClassBuilder { return delegate }override fun newMethod( origin: JvmDeclarationOrigin, access: Int, name: String, desc: String, signature: String?, exceptions: Array? ): MethodVisitor {val original = super.newMethod(origin, access, name, desc, signature, exceptions)val thisName = delegate.thisName// 校验VersionCurrentValue的完全限定名 if (thisName != VERSION_NAME) { return original }// 校验是否在`build.gradle`中设置了版本号 if (version == VersionComponentRegistrar.DEFAULT_VERSION) { messageCollector.report( CompilerMessageSeverity.ERROR, "Missing version, need to set version in build.gradle, like this:\n" + "version {\n" + "\tversion = \"1.0.0.0\"\n" + "}" ) }// 结构版本号 val (major, minor, patch, extra, suffix) = parseVersion()// 返回ASM MethodVisitor return VersionMethodVisitor(Opcodes.ASM9, original, major, minor, patch, extra, suffix) }// 解析版本号为`Multiple` private fun parseVersion(): Multiple { if (version.isEmpty()) { throw IllegalArgumentException("Version must not be empty.") }val major: Int val minor: Int val patch: Int val extra: Int val suffix: String?if (version.contains("-")) { val split = version.split("-") if (split.size != 2) { throw IllegalArgumentException("Version components must be only contains one `-`.") }val versions = split[0].split(".") val length = versions.size if (length != 4) { throw IllegalArgumentException("Version components must be four digits, it is [ $version ] now.") }try { major = versions[0].toInt() minor = versions[1].toInt() patch = versions[2].toInt() extra = versions[3].toInt() suffix = split[1] } catch (e: NumberFormatException) { val errMsg = "Version components must consist of numbers." val exception = IllegalArgumentException(errMsg) exception.addSuppressed(e) throw exception } } else { val versions = version.split(".") val length = versions.size if (length != 4) { throw IllegalArgumentException("Version components must be four digits, it is [ $version ] now.") }try { major = versions[0].toInt() minor = versions[1].toInt() patch = versions[2].toInt() extra = versions[3].toInt() suffix = null } catch (e: NumberFormatException) { val errMsg = "Version components must consist of numbers." val exception = IllegalArgumentException(errMsg) exception.addSuppressed(e) throw exception } }if (suffix.isNullOrEmpty()) { messageCollector.report( CompilerMessageSeverity.WARNING, String.format(Locale.CHINA, "version = %d.%d.%d.%d", major, minor, patch, extra) ) } else { messageCollector.report( CompilerMessageSeverity.WARNING, String.format(Locale.CHINA, "version = %d.%d.%d.%d-%s", major, minor, patch, extra, suffix) ) }if (checkVersion(major) || checkVersion(minor) || checkVersion(patch) || checkVersion(extra)) { val msg = String.format( Locale.CHINA, "Version components are out of range: %d.%d.%d.%d.", major, minor, patch, extra ) throw IllegalArgumentException(msg) }return Multiple(major, minor, patch, extra, suffix) }private fun checkVersion(version: Int): Boolean { return version < MIN_COMPONENT_VALUE || version > MAX_COMPONENT_VALUE } }

Multiple
data class Multiple( val first: A, val second: B, val third: C, val fourth: D, val fifth: E? ) : Serializable {override fun toString(): String = "($first, $second, $third, $fourth, $fifth)" }

VersionMethodVisitor
class VersionMethodVisitor( api: Int, mv: MethodVisitor, private val major: Int, private val minor: Int, private val patch: Int, private val extra: Int, private val suffix: String? ) : MethodPatternAdapter(api, mv) {companion object { // 状态 private const val SEEN_ICONST_0 = 1 private const val SEEN_ICONST_0_ICONST_0 = 2 private const val SEEN_ICONST_0_ICONST_0_ICONST_0 = 3 private const val SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0 = 4 private const val SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL = 5// Version完全限定名 private const val OWNER = "com/guodong/android/Version" private const val METHOD_NAME = "" private const val METHOD_DESCRIPTOR = "(IIIILjava/lang/String; )V" }/** * val version = Version(0, 0, 0, 0, null) * ICONST_0 * ICONST_0 * ICONST_0 * ICONST_0 * ACONST_NULL */ override fun visitInsn(opcode: Int) { // 状态机 when (state) { SEEN_NOTHING -> { if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0 return } } SEEN_ICONST_0 -> { if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0_ICONST_0 return } } SEEN_ICONST_0_ICONST_0 -> { if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0_ICONST_0_ICONST_0 return } } SEEN_ICONST_0_ICONST_0_ICONST_0 -> { if (opcode == Opcodes.ICONST_0) { state = SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0 return } } SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0 -> { if (opcode == Opcodes.ACONST_NULL) { state = SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL return } } SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL -> { if (opcode == Opcodes.ACONST_NULL) { mv.visitInsn(opcode) return } } }super.visitInsn(opcode) }override fun visitMethodInsn( opcode: Int, owner: String, name: String, descriptor: String, isInterface: Boolean ) {val flag = opcode == Opcodes.INVOKESPECIAL && OWNER == owner && METHOD_NAME == name && METHOD_DESCRIPTOR == descriptorwhen (state) { SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL -> { if (flag) { weaveCode(major) weaveCode(minor) weaveCode(patch) weaveCode(extra) weaveSuffix() state = SEEN_NOTHING } } }super.visitMethodInsn(opcode, owner, name, descriptor, isInterface) }// 补发 override fun visitInsn() { when (state) { SEEN_ICONST_0 -> { mv.visitInsn(Opcodes.ICONST_0) } SEEN_ICONST_0_ICONST_0 -> { mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) } SEEN_ICONST_0_ICONST_0_ICONST_0 -> { mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) } SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0 -> { mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) } SEEN_ICONST_0_ICONST_0_ICONST_0_ICONST_0_ACONST_NULL -> { mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ICONST_0) mv.visitInsn(Opcodes.ACONST_NULL) } } state = SEEN_NOTHING }// 织入版本号 private fun weaveCode(code: Int) { when { code <= 5 -> { val opcode = when (code) { 0 -> Opcodes.ICONST_0 1 -> Opcodes.ICONST_1 2 -> Opcodes.ICONST_2 3 -> Opcodes.ICONST_3 4 -> Opcodes.ICONST_4 5 -> Opcodes.ICONST_5 else -> Opcodes.ICONST_0 } mv.visitInsn(opcode) } code <= 127 -> { mv.visitIntInsn(Opcodes.BIPUSH, code) } else -> { mv.visitIntInsn(Opcodes.SIPUSH, code) } } }// 织入后缀 private fun weaveSuffix() { if (suffix.isNullOrEmpty()) { mv.visitInsn(Opcodes.ACONST_NULL) } else { mv.visitLdcInsn(suffix) } } }

应用 sample - build.gradle.kts
plugins { kotlin("jvm") id("com.guodong.android.version.kcp") }version { version = "1.0.0.1" }

Test
fun main() { println("version = ${Version.CURRENT}") }// output version = 1.0.0.1

happy~
参考
  1. 参考 KotlinVersion.kt ?

    推荐阅读