前言

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的SynthAccessFlagprivate 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}// 计算字段的AccessFlagprivate 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-kotlinplugins {    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.javaprivate 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) ↩