共计 8572 个字符,预计需要花费 22 分钟才能阅读完成。
前言
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 的 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 文件
,晓得了 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-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) ↩