关于kotlin:KotlinKCP的应用第一篇

42次阅读

共计 8572 个字符,预计需要花费 22 分钟才能阅读完成。

前言

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)
}

为此我还提了个 Issue3,冀望官网把 JvmSynthetic 的作用域扩大到构造方法,不过官网如同没有打算实现

为解决暗藏构造方法,能够把构造方法私有化,对外裸露动态工厂办法:

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)
    }
}

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

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

折腾

摸索 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 文件

,晓得了 JvmSynthetic 注解的实现原理,是不是能够仿照 JvmSynthetic 给构造方法也增加 ACC_SYNTHETIC 标识呢

首先想到的就是利用 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) ↩

正文完
 0