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

kotlin开发sdk混淆配置踩坑记录 使用Kotlin开发确实爽,但是后期上线准备工作有太多坑要踩,这篇文章主要记录使用Kotlin开发完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<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~

参考

           }
            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

```groovy
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 ↩︎

安卓Kotlin接入高德定位和地图SDK 高德的定位sdk可以获取设备当前的详细信息,如经纬度,具体地址(省->街道)等,本文主要使用的是定位sdk和地图sdk中的poi搜索功能(以当前位置半径多少米内的关键词搜索) 阅读详情

相关推荐

Keil MDK编译错误#541解决方案与ARM_Compiler包解析

在嵌入式开发中,编译器工具链的依赖管理是确保项目顺利构建的关键环节。以Keil MDK为例,其软件包依赖机制通过组件化设计实现功能解耦,其中ARM_Compiler包专门处理系统IO重定向等编译器扩展功能。这种架构既提升了模块复用性,也带来了版本兼容性挑战。当出现'Error #541: component missing'报错时,通常是由于中间件组件(如文件系统)在新版本中显式依赖了ARM_Compiler包。开发者需要通过Pack Installer安装该包,并在Run-Time Environment

weixin_32129187的博客 363

AndroidStudio报错:java.lang.RuntimeException:

在build.gradle添加依赖。

weixin_51467172的博客 1446

IC验证 AHB-RAM 项目(二)——接口与事务代码的编写

本文介绍了准备工作及接口、事务相关代码编写要点。在接口部分,重点说明了lvc_ahb_if.sv、rkv_ahbram_if.sv和rkv_ahbram_tb.sv三个关键文件的功能,其中rkv_ahbram_if.sv通过动态任务实现复位信号控制。事务部分则涉及lvc_ahb_types.sv中的枚举类型定义和lvc_ahb_transaction.sv中的随机化变量设置,用于构建AHB传输激励。整个验证环境采用模块化设计,通过接口实现TB与DUT的通信,为后续验证工作奠定基础。

weixin_45926435的博客 405

安卓依赖冲突问题

小白经历一番挫折解决安卓依赖冲突重复

我是我的印象的一部分 而我的全部印象才是我 1592

激光三角测量法:斜射与直射式技术的原理与应用解析

本文深入解析激光三角测量法中斜射式与直射式技术的原理、应用及对比。斜射式适用于高精度小范围测量,尤其擅长处理反光表面;直射式则适合大范围位移检测,结构更简单。文章详细介绍了两种技术的光路分析、数学建模、应用场景及选型建议,并探讨了系统校准与误差分析方法,为工业检测领域提供实用参考。

weixin_29280219的博客 413

深入解析UDS 0x19服务:DTC状态掩码与故障诊断实战

本文深入解析了UDS诊断协议中的核心0x19服务(ReadDTCInformation),重点拆解了DTC状态掩码(DTCStatusMask)的八个状态位及其在故障生命周期中的联动逻辑。通过实战案例,详细说明了从故障发生、确认到清除的全过程,并介绍了如何利用该服务进行深度诊断,包括读取扩展数据(如冻结帧)和避免常见开发误区,是理解汽车ECU故障管理机制的关键。

weixin_29248313的博客 323

【Android Studio插件升级以后kotlin jdk版本冲突】

今天升级一些插件版本后无法编译,发现kotlin jdk 1.8.20和kotlin jdk 1.6.21重复冲突,网上找了挺多方法都没生效,千回百转才解决,发出来希望可以帮助到同样报错的同学。build.gradle文件中修改kotlin-stdlib-jdk加载方式。

LLCCQAQ的博客 9048

基于ADRC算法的多旋翼飞行器抗扰动控制实践

本文详细介绍了基于ADRC算法的多旋翼飞行器抗扰动控制实践,通过线性扩张状态观测器(LESO)实时估算风力等扰动,实现提前补偿,显著提升飞行稳定性。文章结合具体案例和工程实现技巧,展示了ADRC在突风条件下的优异表现,为无人机控制提供了高效解决方案。

weixin_29284885的博客 191

Kotlin-KCP的应用-第一篇

前言 KCP的应用计划分两篇,本文是第一篇 本文主要记录从发现问题到使用KCP解决问题的折腾过程,下一篇记录KCP的应用 背景 Kotlin 号称百分百兼容 Java ,所以在 Kotlin 中一些修饰符,比如 internal ,在编译后放在纯 Java 的项目中使用(没有Kotlin环境),Java 仍然可以访问被 internal 修饰的类、方法、字段等 在使用 Kotlin 开发过程中需要对外提供 SDK 包,在 SDK 中有一些 API 不想被外部调用,并且已经添加了 internal 修饰,但是

孫小逗 1377

AVEVA PDMS 二次开发之PML实战:从宏到自定义窗体的效率提升

本文详细介绍了AVEVA PDMS二次开发中PML语言的应用,从基础宏编写到高级窗体开发,全面提升工程设计效率。通过实战案例展示如何利用PML实现批量操作、流程自动化和交互优化,帮助工程师快速掌握这一强大工具。

weixin_42549451的博客 338

Autosar诊断实战系列05-Dcm之0x27加解密服务配置开发详解

UDS 0x27服务实战开发介绍,对解锁尝试次数及delay时间进一步解释。

initiallizer的博客 1991

Kotlin 移动端跨平台是种怎样的体验

前言 近些年,移动端无论是在技术还是在市场方面都已经逐步成熟,效率成为各家争夺存量 、开拓疆土的利器,因而跨平台技术的热度也如日中天。其中Flutter算得上是跨平台技术的网红,易上手、UI像素级同步、性能优异,引起一大波粉丝(各大厂)为它打call,各大厂商都为Flutter社区带来了很大贡献,迅速扩大了它的技术生态。实不相瞒,个人也是Flutter的小粉丝,既然Flutter这么好,那我还在...

a357664853的专栏 1251

手把手教你学simulink实例--基于Simulink的储能系统性能评估与仿真

储能系统是现代能源体系中的关键组成部分,广泛应用于可再生能源并网、电网调峰调频、电力质量改善以及电动汽车充电等领域。为了提高储能系统的运行效率和可靠性,对其性能进行准确评估至关重要。基于MATLAB/Simulink平台,可以构建储能系统的动态模型,并通过仿真分析其在不同工况下的性能表现。本项目旨在研究储能系统的建模方法、控制策略及其性能评估指标,并通过Simulink仿真验证系统的动态特性和稳态性能。

MHD0815的博客 194

androidx.annotation.Nullable找不到,@Nullable报红

相关引用与之冲突导致,我的代码里还有kotlin相关依赖和插件,kotlin里也有这个androidx的annotation引用。以上代码频繁出现kotlin相关包出现多余的依赖,于是分析可能是kotlin相关依赖或插件有问题。以上代码全部屏蔽后,Sync Now 一下代码,再去看项目中的错误已经消失。虽然代码中有报错,但我还是运行了一下,结果就报了以下错误。如题,在android开发环境中,以下引用找不到。

gaoqingliang521的专栏 3803

VB摄像头监控

该VB模块可对摄像头捕获的画面进行录像等,可在创建的窗口进行控制

假人有限元模型

假人模型,车辆碰撞假人有限元模型,适合做乘员安全的研究者利用

Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules。Android studio纯java代码报错

我使用java代码 构建项目,初始代码运行就会报错。我使用的是Android Studio Giraffe(Adroid-studio-2022.3.1.18-windows)。我在网上找的解决办法是删除重复的类,但这操作起来真的太麻烦了。最后在堆栈上找到了简单的方法,添加上这段代码就好了。

Night_Journey的博客 3万+

android 蓝牙通讯demo

android 下蓝牙通讯demo,实现了发现蓝牙,使能蓝牙,通过蓝牙发送消息。

Jetpack Compose入门——Duplicate class kotlin.time.jdk8.DurationConversionsJDK8Kt

在build.gradle中添加。

长颈鹿的专栏 1251

NTscan扫描工具

ntscan是一个扫描器,可以扫描没有密码和弱密码的共享,先到配置文件中填写猜测的用户名和密码,扫描过程中右侧出现的是扫描日志。

Duplicate class kotlin.XXX 问题处理

Duplicate class kotlin

louise_tsc的博客 659

Android Studio解决kotlin-stdlib-jdk7和kotlin-stdlib-jdk8库版本冲突(Java示范)

本文介绍了如何处理AndroidStudio中出现的kotlin-stdlib-jdk7和kotlin-stdlib-jdk8库版本冲突的问题,这个问题在Kotlin模式和Java模式下都较为常见,且是一个让人极为头疼的问题。这里给出的一种用户反馈良好,成功率极高的解法,用来处理这类问题,并在作者的电脑上成功实践。

m0_74353785的博客 1808

【安卓环境搭建报错的解决】

电脑中新安装的 Android Studio Giraffe | 2022.3.1 Patch ,运行 studio 系统工程,提示如下错误。在build.gradle.kts 中增加。

kerwin的专栏 1912

解决Duplicate class xxx found in modules编译报错的问题

其实错误信息已比较明确,错误的原因是在com.mygroup.mylib:baselib1:1.0.1和com.mygroup.mylib:baselib2:1.0.0包里同时定义了com.mygroup.mylib.MyBean类,导致重复定义了,所以编译失败。解决方案也比较明确,就是去掉其中一个包的com.mygroup.mylib.MyBean类的定义。

冰雪世界 1万+

记一个 Duplicate class kotlin-stblib vs kotlin-stdlib-jdk7/8 编译问题引发的案例

某天将项目 kotlin 版本升级到了 1.8.0 ,然后编译报错了,然后开始寻求解决方案...

过客2019 1819

android studio非业务代码报错 Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules。

Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules

雨汨的博客 4931

Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules kotlin-stdlib-1.8.10

Duplicate class kotlin.collections.jdk8.CollectionsJDK8Kt found in modules kotlin-stdlib-1.8.10 (org.jetbrains.kotlin:kotlin-stdlib:1.8.10)

hhhyyyjjj的专栏 7816

AndroidSDK开发6我用kotlin协程写了一个简单sdk

由于前面几篇文章讲解了sdk初始化、sdk设计原则、将moule或者项目打成aar、不用第三方库开发一个sdk等,本文讲解使用如何使用kolin开发一个简单sdk

u012556114的博客 2107

Android Studio启动第一个项目的报错——笔记

然后在刚刚建好的项目里找到gradle-wrapper.properties,修改distributionUrl,并把自己从官网下载的包放到和gradle-wrapper.properties同级目录下点一下这个。

桂的博客 605
上一篇: Java并发编程-Android的UI框架为什么是单线程的
下一篇: Java并发编程-Java内存模型(JMM)
guodongAndroid
博客等级 码龄10年 16粉丝 19原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值