Kotlin-KCP的应用-第一篇 
  前言 
KCP的应用计划分两篇,本文是第一篇
本文主要记录从发现问题到使用KCP解决问题的折腾过程,下一篇记录KCP的应用
  背景 
Kotlin 号称百分百兼容 Java ,所以在 Kotlin 中一些修饰符,比如 internal ,在编译后放在纯 Java 的项目中使用(没有Kotlin环境),Java 仍然可以访问被 internal 修饰的类、方法、字段等
在使用 Kotlin 开发过程中需要对外提供 SDK 包,在 SDK 中有一些 API 不想被外部调用,并且已经添加了 internal 修饰,但是受限于上诉问题且第三方使用 SDK 的环境不可控(不能要求第三方必须使用Kotlin)
带着问题Google一番,查到以下几个解决方案:
使用 JvmName 注解设置一个不符合 Java 命名规则的标识符 
使用 ˋˋ 在 Kotlin 中把一个不合法的标识符强行合法化 
使用 JvmSynthetic 注解 
 
以上方案可以满足大部分需求,但是以上方案都不满足隐藏构造方法,可能会想什么情景下需要隐藏构造方法,例如:
1 2 3 4 5 6 7 class  Builder (internal  val  a: Int , internal  val  b: Int ) {              internal  constructor () : this (-1 , -1 ) } 
 
为此我还提了个Issue,期望官方把 JvmSynthetic 的作用域扩展到构造方法,不过官方好像没有打算实现😂
为解决隐藏构造方法,可以把构造方法私有化,对外暴露静态工厂方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 class  Builder  private  constructor  (internal  val  a: Int , internal  val  b: Int ) {              private  constructor () : this (-1 , -1 )          companion  object  {         @JvmStatic          fun  newBuilder (a: Int , b: Int )   = Builder(a, b)     } } 
 
解决方案说完了,大家散了吧,散了吧~
开玩笑,开玩笑😛,必然要折腾一番
  折腾 
  探索JvmSynthetic实现原理 
先看下 JvmSynthetic 注解的注释文档
1 2 3 4 5 6 7 8 9 /**  * 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 隐藏的,非公开的一种修饰符,可以修饰类、方法、字段等
得看看 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  为例,附上相关代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 private  fun  IrClass.getSynthAccessFlag (languageVersionSettings: LanguageVersionSettings )  : Int  {         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  } 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 			             (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 的搭建、使用,网上有很多相关文章,此处不再描述,下图是本仓库的组织架构
这里简单说明下:
  api-xxx 
api-xxx模块中只有一个注解类 Hide
1 2 3 4 @Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD}) @Retention(RetentionPolicy.CLASS) public  @interface  Hide {} 
 
1 2 3 4 5 6 7 8 9 @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 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 class  MaskPlugin  implements  Plugin <Project> {    @Override      void  apply(Project project) {                  project.logger.error("Welcome to guodongAndroid mask plugin." )                  LibraryExtension extension = project.extensions.findByType(LibraryExtension)         if  (extension == null ) {             project.logger.error("Only support [AndroidLibrary]." )             return          }         extension.registerTransform(new  MaskTransform(project))     } } 
 
创建 MaskTransform,继承 com.android.build.api.transform.Transform 抽象类,主要实现 transform 方法,以下为核心代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 class  MaskTransform  extends  Transform  {    @Override      void  transform(TransformInvocation transformInvocation) throws  TransformException, InterruptedException, IOException {         long  start = System.currentTimeMillis()         logE("$TAG - start" )         TransformOutputProvider outputProvider = transformInvocation.outputProvider                                    transformInvocation.inputs.each { transformInput ->             transformInput.directoryInputs.each { dirInput ->                 if  (dirInput.file.isDirectory()) {                     dirInput.file.eachFileRecurse { file ->                         if  (file.name.endsWith(".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)             }                          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 方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 class  MaskClassNode  extends  ClassNode  {    private  static  final  String TAG = MaskClassNode.class .simpleName          private  static  final  String HIDE_JAVA_DESCRIPTOR = "Lcom/guodong/android/mask/api/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() {                  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}" )             }         }                  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)         }     }          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      } } 
 
  build.gradle - project level 
1 2 3 4 5 6 buildscript {     ext.plugin_version = 'x.x.x'      dependencies {         classpath "com.guodong.android:mask-gradle-plugin:${plugin_version}"      } } 
 
  build.gradle - module level 
1 2 3 4 5 6 7 8 # lib-kotlin plugins {     id 'com.android.library'      id 'kotlin-android'      id 'kotlin-kapt'      id 'maven-publish'      id 'com.guodong.android.mask'  } 
 
  lib-kotlin 
1 2 3 4 5 6 interface  InterfaceTest  {         @Hide      fun  testInterface ()  } 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 class  KotlinTest (a: Int ) : InterfaceTest {         @Hide      constructor () : this (-2 )     companion  object  {         @JvmStatic          fun  newKotlinTest ()   = KotlinTest()     }     private  val  binding: LayoutKotlinTestBinding? = null           var  a = a         @Hide  get          @Hide  set      fun  getA1 ()  : Int  {         return  a     }     fun  test ()   {         a = 1000      }     override  fun  testInterface ()   {         println("Interface function test" )     } } 
 
  app 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 # MainActivity.java private  void  testKotlinLib ()  {         KotlinTest  test  =  KotlinTest.newKotlinTest();          Log.e(TAG, "testKotlinLib: before --> "  + test.getA1());     test.test();     Log.e(TAG, "testKotlinLib: after --> "  + test.getA1());               test.testInterface();          InterfaceTest  interfaceTest  =  test;          interfaceTest.testInterface(); } 
 
happy:happy:
  参考文档