1
头图

前言

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)

image-20220508120615028

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

image-20220508121250275

image-20220508121115651

image-20220508121137580

在搜索结果中发现几个类名与代码生成相关,这里以 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 的搭建、使用,网上有很多相关文章,此处不再描述,下图是本仓库的组织架构

image-20220508131245185

这里简单说明下:

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:

参考文档


guodongAndroid
1 声望1 粉丝

始于Android,不止于Android