前言
KCP的利用打算分两篇,本文是第一篇
本文次要记录从发现问题到应用KCP解决问题的折腾过程,下一篇记录KCP的利用
背景
Kotlin
号称百分百兼容 Java
,所以在 Kotlin
中一些修饰符,比方 internal
,在编译后放在纯 Java
的我的项目中应用(没有Kotlin
环境),Java
依然能够拜访被 internal
润饰的类、办法、字段等
在应用 Kotlin
开发过程中须要对外提供 SDK
包,在 SDK
中有一些 API
不想被内部调用,并且曾经增加了 internal
润饰,然而受限于上诉问题且第三方应用 SDK
的环境不可控(不能要求第三方必须应用Kotlin
)
带着问题Google一番,查到以下几个解决方案:
- 应用
JvmName
注解设置一个不合乎Java
命名规定的标识符1 - 应用
在
Kotlin
中把一个不非法的标识符强行合法化1 - 应用
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:
参考文档
- 正确地应用 Kotlin 的 internal ↩
- Support more targets for @JvmSynthetic : KT-24981 (jetbrains.com) ↩
- Support 'constructor' target for JvmSynthetic annotation : KT-50609 (jetbrains.com) ↩
- Chapter 4. The class File Format (oracle.com) ↩