头图

背景

在 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<Version> {

    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 环境的基本步骤,这里不再赘述,有兴趣的读者可以先看下上篇文章

image-20220522224118965

上图是本项目的组织架构,简单介绍下:

  • 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<KotlinCompile> {
    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<List<SubpluginOption>> {
        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<KotlinCompile> {
    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<String>(OPTION_VERSION)
    }

    // 配置 Kotlin 插件唯一 ID
    override val pluginId: String = BuildConfig.KOTLIN_PLUGIN_ID

    // 读取 `SubpluginOptions` 参数,并写入 `CliOption`
    override val pluginOptions: Collection<AbstractCliOption> = 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<out String>?
    ): 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<Int, Int, Int, Int, String?> {
        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<out A, out B, out C, out D, out E>(
    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 = "<init>"
        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~

参考


  1. 参考 KotlinVersion.kt

guodongAndroid
1 声望1 粉丝

始于Android,不止于Android