Kotlin-KCP的應用-第一篇

前言

KCP的應用計劃分兩篇,本文是第一篇

本文主要記錄從發(fā)現(xiàn)問題到使用KCP解決問題的折騰過程,下一篇記錄KCP的應用

背景

Kotlin 號稱百分百兼容 Java ,所以在 Kotlin 中一些修飾符,比如 internal ,在編譯后放在純 Java 的項目中使用(沒有Kotlin環(huán)境),Java 仍然可以訪問被 internal 修飾的類、方法、字段等

在使用 Kotlin 開發(fā)過程中需要對外提供 SDK 包,在 SDK 中有一些 API 不想被外部調(diào)用,并且已經(jīng)添加了 internal 修飾,但是受限于上訴問題且第三方使用 SDK 的環(huán)境不可控(不能要求第三方必須使用Kotlin)

帶著問題Google一番,查到以下幾個解決方案:

  1. 使用 JvmName 注解設置一個不符合 Java 命名規(guī)則的標識符[1]
  2. 使用 ˋˋKotlin 中把一個不合法的標識符強行合法化[1]
  3. 使用 JvmSynthetic 注解[2]

以上方案可以滿足大部分需求,但是以上方案都不滿足隱藏構(gòu)造方法,可能會想什么情景下需要隱藏構(gòu)造方法,例如:

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

為此我還提了個Issue[3],期望官方把 JvmSynthetic 的作用域擴展到構(gòu)造方法,不過官方好像沒有打算實現(xiàn):joy:

為解決隱藏構(gòu)造方法,可以把構(gòu)造方法私有化,對外暴露靜態(tài)工廠方法:

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實現(xiàn)原理

先看下 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.
 */

好家伙,實現(xiàn)原理都說了:在 Java 字節(jié)碼中的注解目標上設置 ACC_SYNTHETIC 標識

此處涉及 Java 字節(jié)碼知識點,ACC_SYNTHETIC 標識可以簡單理解是 Java 隱藏的,非公開的一種修飾符,可以修飾類、方法、字段等[4]

得看看 Kotlin 是如何設置 ACC_SYNTHETIC 標識的,打開 Github Kotlin 倉庫,在倉庫內(nèi)搜索 JvmSynthetic 關(guān)鍵字 Search · JvmSynthetic (github.com)

在搜索結(jié)果中分析發(fā)現(xiàn) JVM_SYNTHETIC_ANNOTATION_FQ_NAME 關(guān)聯(lián)性較大,繼續(xù)在倉庫內(nèi)搜索 JVM_SYNTHETIC_ANNOTATION_FQ_NAME 關(guān)鍵字 Search · JVM_SYNTHETIC_ANNOTATION_FQ_NAME (github.com)

在搜索結(jié)果中發(fā)現(xiàn)幾個類名與代碼生成相關(guān),這里以 ClassCodegen.kt 為例,附上相關(guān)代碼

// 獲取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 是字節(jié)碼操作庫 ASM 中的類

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

:ok:,知道了 JvmSynthetic 注解的實現(xiàn)原理,是不是可以仿照 JvmSynthetic 給構(gòu)造方法也添加 ACC_SYNTHETIC 標識呢:question:

首先想到的就是利用 AGP Transform 進行字節(jié)碼修改

AGP Transform

AGP Transform 的搭建、使用,網(wǎng)上有很多相關(guān)文章,此處不再描述,下圖是本倉庫的組織架構(gòu)

這里簡單說明下:

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相關(guān),下篇再講

lib-xxx

lib-xxx模塊中包含對注解api-xxx的測試,打包成SDK,供app模塊使用

plugin

plugin模塊包含AGP Transform

實現(xiàn)plugin模塊

創(chuàng)建MaskPlugin

創(chuàng)建 MaskPlugin 類,實現(xiàn) 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))
    }
}

創(chuàng)建MaskTransform

創(chuàng)建 MaskTransform,繼承 com.android.build.api.transform.Transform 抽象類,主要實現(xiàn) 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
        
        // 沒有適配增量編譯

        // 只關(guān)心本項目生成的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)
            }

            // 不關(guān)心第三方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)
    }
}

創(chuàng)建MaskClassNode

創(chuàng)建 MaskClassNode,繼承 org.objectweb.asm.tree.ClassNode,主要實現(xiàn) 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() {
    // 創(chuàng)建對象時不能訪問無參構(gòu)造方法,可以訪問有參構(gòu)造方法或訪問靜態(tài)工廠方法
    KotlinTest test = KotlinTest.newKotlinTest();
    // 調(diào)用時不能訪問`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) ?

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容