前言
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)
}
为此我还提了个Issue[3],期望官方把 JvmSynthetic
的作用域扩展到构造方法,不过官方好像没有打算实现:joy:
为解决隐藏构造方法,可以把构造方法私有化,对外暴露静态工厂方法:
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
实现原理
先看下 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的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文件
:ok:,知道了 JvmSynthetic
注解的实现原理,是不是可以仿照 JvmSynthetic
给构造方法也添加 ACC_SYNTHETIC
标识呢:question:
首先想到的就是利用 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 {
@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 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 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:
参考文档
-
正确地使用 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) ↩