草庐IT

Kotlin-KCP的应用-第一篇

guodongAndroid 2023-03-28 原文

前言

KCP的应用计划分两篇,本文是第一篇

本文主要记录从发现问题到使用KCP解决问题的折腾过程,下一篇记录KCP的应用

背景

Kotlin 号称百分百兼容 Java ,所以在 Kotlin 中一些修饰符,比如 internal ,在编译后放在纯 Java 的项目中使用(没有Kotlin环境),Java 仍然可以访问被 internal 修饰的类、方法、字段等

在使用 Kotlin 开发过程中需要对外提供 SDK 包,在 SDK 中有一些 API 不想被外部调用,并且已经添加了 internal 修饰,但是受限于上诉问题且第三方使用 SDK 的环境不可控(不能要求第三方必须使用Kotlin)

带着问题Google一番,查到以下几个解决方案:

  1. 使用 JvmName 注解设置一个不符合 Java 命名规则的标识符[1]
  2. 使用 ˋˋKotlin 中把一个不合法的标识符强行合法化[1]
  3. 使用 JvmSynthetic 注解[2]

以上方案可以满足大部分需求,但是以上方案都不满足隐藏构造方法,可能会想什么情景下需要隐藏构造方法,例如:

class Builder(internal val a: Int, internal val b: Int) {
    
    /**
     * non-public constructor for java
     */
    internal constructor() : this(-1, -1)
}

为此我还提了个Issue[3],期望官方把 JvmSynthetic 的作用域扩展到构造方法,不过官方好像没有打算实现:joy:

为解决隐藏构造方法,可以把构造方法私有化,对外暴露静态工厂方法:

class Builder private constructor (internal val a: Int, internal val b: Int) {
    
    /**
     * non-public constructor for java
     */
    private constructor() : this(-1, -1)
    
    companion object {

        @JvmStatic
        fun newBuilder(a: Int, b: Int) = Builder(a, b)
    }
}

解决方案说完了,大家散了吧,散了吧~

开玩笑,开玩笑:stuck_out_tongue:,必然要折腾一番

折腾

探索JvmSynthetic实现原理

先看下 JvmSynthetic 注解的注释文档

/**
 * Sets `ACC_SYNTHETIC` flag on the annotated target in the Java bytecode.
 *
 * Synthetic targets become inaccessible for Java sources at compile time while still being accessible for Kotlin sources.
 * Marking target as synthetic is a binary compatible change, already compiled Java code will be able to access such target.
 *
 * This annotation is intended for *rare cases* when API designer needs to hide Kotlin-specific target from Java API
 * while keeping it a part of Kotlin API so the resulting API is idiomatic for both languages.
 */

好家伙,实现原理都说了:在 Java 字节码中的注解目标上设置 ACC_SYNTHETIC 标识

此处涉及 Java 字节码知识点,ACC_SYNTHETIC 标识可以简单理解是 Java 隐藏的,非公开的一种修饰符,可以修饰类、方法、字段等[4]

得看看 Kotlin 是如何设置 ACC_SYNTHETIC 标识的,打开 Github Kotlin 仓库,在仓库内搜索 JvmSynthetic 关键字 Search · JvmSynthetic (github.com)

在搜索结果中分析发现 JVM_SYNTHETIC_ANNOTATION_FQ_NAME 关联性较大,继续在仓库内搜索 JVM_SYNTHETIC_ANNOTATION_FQ_NAME 关键字 Search · JVM_SYNTHETIC_ANNOTATION_FQ_NAME (github.com)

在搜索结果中发现几个类名与代码生成相关,这里以 ClassCodegen.kt 为例,附上相关代码

// 获取Class的SynthAccessFlag
private fun IrClass.getSynthAccessFlag(languageVersionSettings: LanguageVersionSettings): Int {
    // 如果有`JvmSynthetic`注解,返回`ACC_SYNTHETIC`标识
    if (hasAnnotation(JVM_SYNTHETIC_ANNOTATION_FQ_NAME))
        return Opcodes.ACC_SYNTHETIC
    if (origin == IrDeclarationOrigin.GENERATED_SAM_IMPLEMENTATION &&
        languageVersionSettings.supportsFeature(LanguageFeature.SamWrapperClassesAreSynthetic)
    )
        return Opcodes.ACC_SYNTHETIC
    return 0
}

// 计算字段的AccessFlag
private fun IrField.computeFieldFlags(context: JvmBackendContext, languageVersionSettings: LanguageVersionSettings): Int =
    origin.flags or visibility.flags or
            (if (isDeprecatedCallable(context) ||
                correspondingPropertySymbol?.owner?.isDeprecatedCallable(context) == true
            ) Opcodes.ACC_DEPRECATED else 0) or
            (if (isFinal) Opcodes.ACC_FINAL else 0) or
            (if (isStatic) Opcodes.ACC_STATIC else 0) or
            (if (hasAnnotation(VOLATILE_ANNOTATION_FQ_NAME)) Opcodes.ACC_VOLATILE else 0) or
            (if (hasAnnotation(TRANSIENT_ANNOTATION_FQ_NAME)) Opcodes.ACC_TRANSIENT else 0) or
            // 如果有`JvmSynthetic`注解,返回`ACC_SYNTHETIC`标识
            (if (hasAnnotation(JVM_SYNTHETIC_ANNOTATION_FQ_NAME) ||
                isPrivateCompanionFieldInInterface(languageVersionSettings)
            ) Opcodes.ACC_SYNTHETIC else 0)

上述源码中 Opcodes 是字节码操作库 ASM 中的类

猜想 Kotlin 编译器也是使用 ASM 编译生成/修改Class文件

:ok:,知道了 JvmSynthetic 注解的实现原理,是不是可以仿照 JvmSynthetic 给构造方法也添加 ACC_SYNTHETIC 标识呢:question:

首先想到的就是利用 AGP Transform 进行字节码修改

AGP Transform

AGP Transform 的搭建、使用,网上有很多相关文章,此处不再描述,下图是本仓库的组织架构

这里简单说明下:

api-xxx

api-xxx模块中只有一个注解类 Hide

@Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD})
@Retention(RetentionPolicy.CLASS)
public @interface Hide {
}
@Target(
    AnnotationTarget.FIELD,
    AnnotationTarget.CONSTRUCTOR,
    AnnotationTarget.FUNCTION,
    AnnotationTarget.PROPERTY_GETTER,
    AnnotationTarget.PROPERTY_SETTER,
)
@Retention(AnnotationRetention.BINARY)
annotation class Hide

kcp

kcp相关,下篇再讲

lib-xxx

lib-xxx模块中包含对注解api-xxx的测试,打包成SDK,供app模块使用

plugin

plugin模块包含AGP Transform

实现plugin模块

创建MaskPlugin

创建 MaskPlugin 类,实现 org.gradle.api.Plugin 接口

class MaskPlugin implements Plugin<Project> {

    @Override
    void apply(Project project) {
        // 输出日志,查看Plugin是否生效
        project.logger.error("Welcome to guodongAndroid mask plugin.")

        // 目前增加了限制仅能用于`AndroidLibrary`
        LibraryExtension extension = project.extensions.findByType(LibraryExtension)
        if (extension == null) {
            project.logger.error("Only support [AndroidLibrary].")
            return
        }

        extension.registerTransform(new MaskTransform(project))
    }
}

创建MaskTransform

创建 MaskTransform,继承 com.android.build.api.transform.Transform 抽象类,主要实现 transform 方法,以下为核心代码

class MaskTransform extends Transform {
    @Override
    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException {
        long start = System.currentTimeMillis()
        logE("$TAG - start")

        TransformOutputProvider outputProvider = transformInvocation.outputProvider
        
        // 没有适配增量编译

        // 只关心本项目生成的Class文件
        transformInvocation.inputs.each { transformInput ->
            transformInput.directoryInputs.each { dirInput ->
                if (dirInput.file.isDirectory()) {
                    dirInput.file.eachFileRecurse { file ->
                        if (file.name.endsWith(".class")) {
                            // 使用ASM修改Class文件
                            ClassReader cr = new ClassReader(file.bytes)
                            ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_MAXS)
                            ClassVisitor cv = new CheckClassAdapter(cw)
                            cv = new MaskClassNode(Opcodes.ASM9, cv, mProject)
                            int parsingOptions = 0
                            cr.accept(cv, parsingOptions)
                            byte[] bytes = cw.toByteArray()

                            FileOutputStream fos = new FileOutputStream(file)
                            fos.write(bytes)
                            fos.flush()
                            fos.close()
                        }
                    }
                }

                File dest = outputProvider.getContentLocation(dirInput.name, dirInput.contentTypes, dirInput.scopes, Format.DIRECTORY)
                FileUtils.copyDirectory(dirInput.file, dest)
            }

            // 不关心第三方Jar中的Class文件
            transformInput.jarInputs.each { jarInput ->
                String jarName = jarInput.name
                String md5Name = DigestUtils.md5Hex(jarInput.file.getAbsolutePath())
                if (jarName.endsWith(".jar")) {
                    jarName = jarName.substring(0, jarName.length() - 4)
                }
                File dest = outputProvider.getContentLocation(jarName + md5Name, jarInput.contentTypes, jarInput.scopes, Format.JAR)
                FileUtils.copyFile(jarInput.file, dest)
            }
        }

        long cost = System.currentTimeMillis() - start
        logE(String.format(Locale.CHINA, "$TAG - end, cost: %dms", cost))
    }

    private void logE(String msg) {
        mProject.logger.error(msg)
    }
}

创建MaskClassNode

创建 MaskClassNode,继承 org.objectweb.asm.tree.ClassNode,主要实现 visitEnd 方法

class MaskClassNode extends ClassNode {

    private static final String TAG = MaskClassNode.class.simpleName

    // api-java中`Hide`注解的描述符
    private static final String HIDE_JAVA_DESCRIPTOR = "Lcom/guodong/android/mask/api/Hide;"
    
    // api-kt中`Hide`注解的描述符
    private static final String HIDE_KOTLIN_DESCRIPTOR = "Lcom/guodong/android/mask/api/kt/Hide;"

    private static final Set<String> HIDE_DESCRIPTOR_SET = new HashSet<>()

    static {
        HIDE_DESCRIPTOR_SET.add(HIDE_JAVA_DESCRIPTOR)
        HIDE_DESCRIPTOR_SET.add(HIDE_KOTLIN_DESCRIPTOR)
    }

    private final Project project

    MaskClassNode(int api, ClassVisitor cv, Project project) {
        super(api)
        this.project = project
        this.cv = cv
    }

    @Override
    void visitEnd() {

        // 处理Field
        for (fn in fields) {
            boolean has = hasHideAnnotation(fn.invisibleAnnotations)
            if (has) {
                project.logger.error("$TAG, before --> typeName = $name, fieldName = ${fn.name}, access = ${fn.access}")
                // 修改字段的访问标识
                fn.access += Opcodes.ACC_SYNTHETIC
                project.logger.error("$TAG, after --> typeName = $name, fieldName = ${fn.name}, access = ${fn.access}")
            }
        }

        // 处理Method
        for (mn in methods) {
            boolean has = hasHideAnnotation(mn.invisibleAnnotations)
            if (has) {
                project.logger.error("$TAG, before --> typeName = $name, methodName = ${mn.name}, access = ${mn.access}")
                // 修改方法的访问标识
                mn.access += Opcodes.ACC_SYNTHETIC
                project.logger.error("$TAG, after --> typeName = $name, methodName = ${mn.name}, access = ${mn.access}")
            }
        }

        super.visitEnd()

        if (cv != null) {
            accept(cv)
        }
    }

    /**
     * 是否有`Hide`注解
     */
    private static boolean hasHideAnnotation(List<AnnotationNode> annotationNodes) {
        if (annotationNodes == null) return false
        for (node in annotationNodes) {
            if (HIDE_DESCRIPTOR_SET.contains(node.desc)) {
                return true
            }
        }
        return false
    }
}

使用Transform

build.gradle - project level

buildscript {
    ext.plugin_version = 'x.x.x'
    dependencies {
        classpath "com.guodong.android:mask-gradle-plugin:${plugin_version}"
    }
}

build.gradle - module level

# lib-kotlin
plugins {
    id 'com.android.library'
    id 'kotlin-android'
    id 'kotlin-kapt'
    id 'maven-publish'
    id 'com.guodong.android.mask'
}

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.java

private 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:

参考文档


  1. 正确地使用 Kotlin 的 internal

  2. Support more targets for @JvmSynthetic : KT-24981 (jetbrains.com)

  3. Support 'constructor' target for JvmSynthetic annotation : KT-50609 (jetbrains.com)

  4. Chapter 4. The class File Format (oracle.com)

有关Kotlin-KCP的应用-第一篇的更多相关文章

  1. ruby - 将差异补丁应用于字符串/文件 - 2

    对于具有离线功能的智能手机应用程序,我正在为Xml文件创建单向文本同步。我希望我的服务器将增量/差异(例如GNU差异补丁)发送到目标设备。这是计划:Time=0Server:hasversion_1ofXmlfile(~800kiB)Client:hasversion_1ofXmlfile(~800kiB)Time=1Server:hasversion_1andversion_2ofXmlfile(each~800kiB)computesdeltaoftheseversions(=patch)(~10kiB)sendspatchtoClient(~10kiBtransferred)Cl

  2. ruby-on-rails - Rails 应用程序之间的通信 - 2

    我构建了两个需要相互通信和发送文件的Rails应用程序。例如,一个Rails应用程序会发送请求以查看其他应用程序数据库中的表。然后另一个应用程序将呈现该表的json并将其发回。我还希望一个应用程序将存储在其公共(public)目录中的文本文件发送到另一个应用程序的公共(public)目录。我从来没有做过这样的事情,所以我什至不知道从哪里开始。任何帮助,将不胜感激。谢谢! 最佳答案 无论Rails是什么,几乎所有Web应用程序都有您的要求,大多数现代Web应用程序都需要相互通信。但是有一个小小的理解需要你坚持下去,网站不应直接访问彼此

  3. ruby - 无法运行 Rails 2.x 应用程序 - 2

    我尝试运行2.x应用程序。我使用rvm并为此应用程序设置其他版本的ruby​​:$rvmuseree-1.8.7-head我尝试运行服务器,然后出现很多错误:$script/serverNOTE:Gem.source_indexisdeprecated,useSpecification.Itwillberemovedonorafter2011-11-01.Gem.source_indexcalledfrom/Users/serg/rails_projects_terminal/work_proj/spohelp/config/../vendor/rails/railties/lib/r

  4. ruby-on-rails - Rails 应用程序中的 Rails : How are you using application_controller. rb 是新手吗? - 2

    刚入门rails,开始慢慢理解。有人可以解释或给我一些关于在application_controller中编码的好处或时间和原因的想法吗?有哪些用例。您如何为Rails应用程序使用应用程序Controller?我不想在那里放太多代码,因为据我了解,每个请求都会调用此Controller。这是真的? 最佳答案 ApplicationController实际上是您应用程序中的每个其他Controller都将从中继承的类(尽管这不是强制性的)。我同意不要用太多代码弄乱它并保持干净整洁的态度,尽管在某些情况下ApplicationContr

  5. ruby-on-rails - 如何在我的 Rails 应用程序 View 中打印 ruby​​ 变量的内容? - 2

    我是一个Rails初学者,但我想从我的RailsView(html.haml文件)中查看Ruby变量的内容。我试图在ruby​​中打印出变量(认为它会在终端中出现),但没有得到任何结果。有什么建议吗?我知道Rails调试器,但更喜欢使用inspect来打印我的变量。 最佳答案 您可以在View中使用puts方法将信息输出到服务器控制台。您应该能够在View中的任何位置使用Haml执行以下操作:-puts@my_variable.inspect 关于ruby-on-rails-如何在我的R

  6. ruby-on-rails - 如何在 Gem 中获取 Rails 应用程序的根目录 - 2

    是否可以在应用程序中包含的gem代码中知道应用程序的Rails文件系统根目录?这是gem来源的示例:moduleMyGemdefself.included(base)putsRails.root#returnnilendendActionController::Base.send:include,MyGem谢谢,抱歉我的英语不好 最佳答案 我发现解决类似问题的解决方案是使用railtie初始化程序包含我的模块。所以,在你的/lib/mygem/railtie.rbmoduleMyGemclassRailtie使用此代码,您的模块将在

  7. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

  8. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  9. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  10. ruby-on-rails - 从应用程序中自定义文件夹内的命名空间自动加载 - 2

    我们目前正在为ROR3.2开发自定义cms引擎。在这个过程中,我们希望成为我们的rails应用程序中的一等公民的几个类类型起源,这意味着它们应该驻留在应用程序的app文件夹下,它是插件。目前我们有以下类型:数据源数据类型查看我在app文件夹下创建了多个目录来保存这些:应用/数据源应用/数据类型应用/View更多类型将随之而来,我有点担心应用程序文件夹被这么多目录污染。因此,我想将它们移动到一个子目录/模块中,该子目录/模块包含cms定义的所有类型。所有类都应位于MyCms命名空间内,目录布局应如下所示:应用程序/my_cms/data_source应用程序/my_cms/data_ty

随机推荐