前言
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è)解決方案:
- 使用
JvmName注解設(shè)置一個(gè)不符合Java命名規(guī)則的標(biāo)識(shí)符[1] - 使用
ˋˋ在Kotlin中把一個(gè)不合法的標(biāo)識(shí)符強(qiáng)行合法化[1] - 使用
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: