Kotlin-KCP的應(yīng)用-第一篇

前言

KCP的應(yīng)用計(jì)劃分兩篇,本文是第一篇

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

背景

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

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

帶著問(wèn)題Google一番,查到以下幾個(gè)解決方案:

  1. 使用 JvmName 注解設(shè)置一個(gè)不符合 Java 命名規(guī)則的標(biāo)識(shí)符[1]
  2. 使用 ˋˋKotlin 中把一個(gè)不合法的標(biāo)識(shí)符強(qiáng)行合法化[1]
  3. 使用 JvmSynthetic 注解[2]

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

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

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

為解決隱藏構(gòu)造方法,可以把構(gòu)造方法私有化,對(duì)外暴露靜態(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)
    }
}

解決方案說(shuō)完了,大家散了吧,散了吧~

開玩笑,開玩笑:stuck_out_tongue:,必然要折騰一番

折騰

探索JvmSynthetic實(shí)現(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.
 */

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

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

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

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

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

// 獲取Class的SynthAccessFlag
private fun IrClass.getSynthAccessFlag(languageVersionSettings: LanguageVersionSettings): Int {
    // 如果有`JvmSynthetic`注解,返回`ACC_SYNTHETIC`標(biāo)識(shí)
    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
}

// 計(jì)算字段的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`標(biāo)識(shí)
            (if (hasAnnotation(JVM_SYNTHETIC_ANNOTATION_FQ_NAME) ||
                isPrivateCompanionFieldInInterface(languageVersionSettings)
            ) Opcodes.ACC_SYNTHETIC else 0)

上述源碼中 Opcodes 是字節(jié)碼操作庫(kù) ASM 中的類

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

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

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

AGP Transform

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

這里簡(jiǎn)單說(shuō)明下:

api-xxx

api-xxx模塊中只有一個(gè)注解類 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模塊中包含對(duì)注解api-xxx的測(cè)試,打包成SDK,供app模塊使用

plugin

plugin模塊包含AGP Transform

實(shí)現(xiàn)plugin模塊

創(chuàng)建MaskPlugin

創(chuàng)建 MaskPlugin 類,實(shí)現(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 抽象類,主要實(shí)現(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)心本項(xiàng)目生成的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,主要實(shí)現(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}")
                // 修改字段的訪問(wèn)標(biāo)識(shí)
                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}")
                // 修改方法的訪問(wèn)標(biāo)識(shí)
                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)建對(duì)象時(shí)不能訪問(wèn)無(wú)參構(gòu)造方法,可以訪問(wèn)有參構(gòu)造方法或訪問(wèn)靜態(tài)工廠方法
    KotlinTest test = KotlinTest.newKotlinTest();
    // 調(diào)用時(shí)不能訪問(wèn)`test.getA()`方法,僅能訪問(wèn)`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)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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