背景
在 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 = 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 环境的基本步骤,这里不再赘述,有兴趣的读者可以先看下上篇文章
上图是本项目的组织架构,简单介绍下:
- 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 = 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 编译器插件,首先配置下 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 = 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 == descriptor
when (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~
参考
- 参考
KotlinVersion.kt
↩