持续创作,加快生长!这是我参与「日新方案 10 月更文挑战」的第 1 天,点击查看活动详情

简介

Sword:一个能够给 Kotlin 函数增加署理的第三方库,基于 KCP 完成。

前言

续接 上篇,在上篇文章中笔者记载了搭建 Sword 的根底开发环境以及技能选型为:注解 + KCP + ASM。本文首要记载使用 ASM 的完成进程。

首要看下上篇文章最终没有记载的 ClassBuilder

ClassBuilder

private val annotations: List<FqName> = listOf(
  FqName("com.guodong.android.sword.api.kt.Proxy"),
)
​
override fun newMethod(
  origin: JvmDeclarationOrigin,
  access: Int,
  name: String,
  desc: String,
  signature: String?,
  exceptions: Array<out String>?
): MethodVisitor {
  val newMethod = super.newMethod(origin, access, name, desc, signature, exceptions)
​
  val function = origin.descriptor as? FunctionDescriptor ?: return newMethod
​
  if (function.isOperator ||
    function.isInfix ||
    function.isInline ||
    function.isSuspend ||
    function.isTailrec
    ) {
    return newMethod
   }
​
  if (annotations.none { function.annotations.hasAnnotation(it) }) {
    return newMethod
   }
​
  val className = delegate.thisName
​
  messageCollector.report(
    CompilerMessageSeverity.WARNING,
    "Sword className = $className, methodName = $name"
   )
​
  val realClassName = className.substring(className.lastIndexOf("/") + 1)
​
  return SwordAdapter(
    Opcodes.ASM9,
    newMethod,
    realClassName,
    access,
    name,
    desc
   )
}

ClassBuilder 中首要覆写 newMethod 函数拦截 Java 办法的生成:

  1. 首要判别是否是函数描述符,不然直接回来,
  2. 若是操作符重载、中缀、内联、挂起以及尾递归函数,不予处理,直接回来,
  3. 函数若是不存在 Proxy 注解,不予处理,直接回来,
  4. 获取真实的类名,交予 SwordAdapter 处理。

能够看出在 ClassBuilder 中首要是完成了一些校验逻辑,第 2 步中的过滤逻辑可增加装备参数提供给集成方在外部灵敏装备。

接下来咱们看下 SwordAdapter 是怎么处理的吧。

SwordAdapter

SwordAdapter 的逻辑较为杂乱,笔者先描述下自己的完成思路,然后再按照思路一点点剖析。

  1. 首要经过 ASM 判别当时函数是否存在 Proxy 注解,若存在则解析出注解中的数据暂存起来,不然不予转化,

  2. 若存在Proxy 注解并解析出注解中的数据,则依据注解中的 enable 字段判别是否启用署理,若启用则进行转化,不然不予转化,

  3. 若进行转化,再判别注解中的 handler 字段是否为空字符串,若是空字符串则进行简略的转化,不然进行署理转化,

  4. 简略转化:依据函数回来类型判别

    1. 无回来值类型回来 void
    2. 根本数据类型回来:-1char 类型回来 48
    3. 引证类型回来 null
  5. 署理转化:替换handler字段中的全限定名,调用InvocationHandler#invoke函数。

下面的流程图看起来或许更清楚一些:

Sword - 为 Kotlin 函数增加代理功能(二)

解析注解

首要定义一个 Proxy 注解数据实体类:

internal data class SwordParam(
  /**
   * 是否有[Proxy]注解
   */
  var hasProxyAnnotation: Boolean = false,
​
  /**
   * 是否启用, 默许True
   */
  var enable: Boolean = true,
​
  /**
   * [InvocationHandler]完成类的全限定名, 完成类必须有无参结构办法
   *
   * e.g. com.example.ProxyTestInvocationHandler
   */
  var handler: String = ""
) {
  companion object {
    // 与[Proxy]注解的参数名一一对应
    internal const val PARAM_ENABLE = "enable"
    internal const val PARAM_HANDLER = "handler"
   }
}

此实体类存储 Proxy 注解中解析出来的数据,下面便是解析 Proxy 注解了:

// 定义一些常量
companion object {
  private const val PROXY_KT_DESC = "Lcom/guodong/android/sword/api/kt/Proxy;"private const val KT_INVOCATION_HANDLER_OWNER =
  "com/guodong/android/sword/api/kt/InvocationHandler"
  private const val INVOKE_METHOD = "invoke"
  private const val INVOCATION_HANDLER_INVOKE_DESC =
  "(Ljava/lang/String;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/Object;"private val proxyDesc = listOf(PROXY_KT_DESC)
}
​
// 声明注解数据实体变量
private val param = SwordParam()
​
// 覆写`visitAnnotation`
override fun visitAnnotation(descriptor: String, visible: Boolean): AnnotationVisitor {
  var av = super.visitAnnotation(descriptor, visible)
​
  // 判别是否存在`Proxy`注解
  if (proxyDesc.contains(descriptor)) {
    param.hasProxyAnnotation = true
    if (av != null) {
      // 解析`Proxy`注解
      av = AnnotationAdapter(api, av, param)
     }
   }
​
  return av
}

解析注解数据首要覆写 visitAnnotation 函数,在此函数中首要判别是否存在 Proxy 注解,若存在则进行解析,不然不予处理。

解析逻辑就在下面代码的 AnnotationAdapter 中了:

internal class AnnotationAdapter(
  api: Int,
  annotationVisitor: AnnotationVisitor?,
  private val param: SwordParam
) : AnnotationVisitor(api, annotationVisitor) {
​
  override fun visit(name: String, value: Any) {
    when (name) {
      SwordParam.PARAM_ENABLE -> param.enable = (value as Boolean)
      SwordParam.PARAM_HANDLER -> param.handler = (value as String)
      else -> {}
     }
   }
}

如上所示,解析逻辑也比较简略,在 visit 函数中:

  1. 第一个参数 name 表明注解中参数的称号,第二参数 value表明注解中参数的值,
  2. 经过比对 name 参数的称号来解析注解中的数据并存储在实体中。

至此解析 Proxy 注解完成,咱们已经拿到注解中的数据,下面咱们就能够开始转化了。

转化分支

对函数署理功用的转化,笔者完成了两种转化分支:

  1. 简略转化:或者称为默许转化,就像 switchdefault 分支一样,
  2. 署理转化:真正的署理功用完成。

转化逻辑在 visitCode 函数中处理,咱们先看看转化分支的挑选:

override fun visitCode() {
  // 判别是否有`Proxy`注解且是否启用署理
  if (param.hasProxyAnnotation && param.enable) {
​
    // 织入一个`booelan`值:True
    super.visitInsn(Opcodes.ICONST_1)
    val label = Label()
    
    // 织入`if`判别句子
    super.visitJumpInsn(Opcodes.IFEQ, label)
​
    // 获取`methodType`
    val methodType = Type.getMethodType(
      methodDescriptor
     )
    
    // 获取`returnType`,函数的回来值类型
    val returnType = methodType.returnType
​
    val handler = param.handler// 判别`handler`是否是空字符串
    if (handler.isNotEmpty()) {
      // 署理转化
      weaveHandler(methodType, returnType, handler)
     } else {
      // 简略转化
      weaveDefaultValue(returnType)
     }
​
    super.visitLabel(label)
   }
  super.visitCode()
}

visitCode 函数的前部分是一些判别处理:

  1. 如果有Proxy注解且启用了署理,则经过 ASM 先织入 if (true) 条件判别句子,
  2. 接下来获取函数的 methodTypereturnType,分别表明在 ASM 眼中的函数类型和回来值类型,
  3. 最终判别 handler 是否是空字符串来决定执行哪种转化分支。

简略转化

简略转化的完成是依据函数回来类型判别:

  1. 无回来值类型回来 void
  2. 根本数据类型回来:-1char 类型回来 48
  3. 引证类型回来 null

下面是简略转化的完成代码片段:

private fun weaveDefaultValue(returnType: Type) {
  val sort = returnType.sort
  when {
    sort == Type.VOID -> {
      super.visitInsn(Opcodes.RETURN)
     }
    sort == Type.CHAR -> {
      super.visitIntInsn(Opcodes.BIPUSH, 48)
      super.visitInsn(returnType.getOpcode(Opcodes.IRETURN))
     }
    sort >= Type.BOOLEAN && sort <= Type.INT -> {
      super.visitInsn(Opcodes.ICONST_M1)
      super.visitInsn(returnType.getOpcode(Opcodes.IRETURN))
     }
    sort == Type.LONG -> {
      super.visitLdcInsn(-1L)
      super.visitInsn(Opcodes.LRETURN)
     }
    sort == Type.FLOAT -> {
      super.visitLdcInsn(-1f)
      super.visitInsn(Opcodes.FRETURN)
     }
    sort == Type.DOUBLE -> {
      super.visitLdcInsn(-1.0)
      super.visitInsn(Opcodes.DRETURN)
     }
    else -> {
      super.visitInsn(Opcodes.ACONST_NULL)
      super.visitInsn(Opcodes.ARETURN)
     }
   }
}

简略转化的完成逻辑比较简略,笔者就不再剖析了,接下来咱们看看今天的主角:署理转化。

署理转化

署理转化的完成逻辑较为杂乱,以下几点是咱们需要考虑的:

  1. 原始函数是否是静态函数:非静态函数(不包括结构函数)的第零位参数始终是 this
  2. 怎么构建 InvocationHandler 完成类的实例,
  3. 怎么获取 InvocationHandler#invoke 函数所需的参数,
  4. 怎么调用 InvocationHandler#invoke 函数,
  5. 调用 InvocationHandler#invoke 函数后的成果怎么回来给原始函数。

脑图如下:

Sword - 为 Kotlin 函数增加代理功能(二)

下面咱们就依据上述几点依次剖析下:

1.是否是静态函数

val argumentTypes = t.argumentTypes
val argumentSize = argumentTypes.size
​
val isStaticMethod = methodAccess and Opcodes.ACC_STATIC != 0
var localSize = if (isStaticMethod) 0 else 1
val firstSlot = localSize
for (argType in argumentTypes) {
  localSize += argType.size
}

首要判别是否是静态函数,其间一个目的是为了找到函数第一个参数的开始方位,以及核算整个办法的 locals 巨细,为后续存储 InvocationHandler 完成类实例做准备:

  • firstSlot 即为第一个参数的开始方位,后边会使用到,
  • localSize 即为整个办法的 lcoals 巨细,经过遍历函数参数得到。

2.构建完成类实例

val realHandler = covertToClassDescriptor(handler)
super.visitTypeInsn(Opcodes.NEW, realHandler)
super.visitInsn(Opcodes.DUP)
super.visitMethodInsn(Opcodes.INVOKESPECIAL, realHandler, "<init>", "()V", false)
super.visitVarInsn(Opcodes.ASTORE, localSize)
super.visitVarInsn(Opcodes.ALOAD, localSize)
​
private fun covertToClassDescriptor(className: String): String {
  return className.replace("\.".toRegex(), "/")
}
  1. 首要需要把 handler 字段中的完成类全限定名(Full-Qualified Name)转化成 ASM 里的 InternalName,比方:com.guodong.android.TestInvocationHandler 转化为 com/guodong/android/TestInvocationHandler,即把 . 替换成 /
  2. 上述片段中的第 4 行代码经过调用完成类的无参结构办法来构建实例,这便是为什么完成类必须有无参结构办法的原因,
  3. 后边两行代码是把创建出来的实例存储在办法的 locals 上并再次加载出来以备后用。

3.获取所需参数

super.visitLdcInsn(className)
super.visitLdcInsn(methodName)
weaveInt(argumentSize)
super.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object")
if (argumentTypes.isNotEmpty()) {
  weaveArgs(argumentTypes, argumentSize, firstSlot)
}
​
--------------------------------------------------------------------------
​
// InvocationHandler
interface InvocationHandler {
  fun invoke(className: String, methodName: String, args: Array<Any?>): Any?
}

上面代码片段的最终是 InvocationHandler 接口的声明,如上所示,invoke 函数需要 3 个参数,分别为:

  1. 当时的类名,
  2. 当时的函数名,
  3. 当时函数声明参数的数组。

下面剖析下获取参数的逻辑:

  1. 代码片段的前两行代码咱们织入了前两个参数,
  2. 第 3 行代码咱们织入参数数组的巨细,
  3. 第 4 行代码构建参数数组实例,
  4. 最终面的 if 条件判别逻辑是把原始函数的参数放进数组内。

4.调用 invoke 函数

super.visitMethodInsn(
  Opcodes.INVOKEINTERFACE,
  KT_INVOCATION_HANDLER_OWNER,
  INVOKE_METHOD,
  INVOCATION_HANDLER_INVOKE_DESC,
  true /* isInterface */
)

调用 invoke 函数比较简略,经过调用 ASMvisitMethodInsn 办法传入正确的参数即可。留意最终一个参数要为 true,因为咱们调用的是一个接口办法。

5.invoke函数的成果回来给原始函数

val returnTypeSort = returnType.sort
when {
  returnTypeSort == Type.VOID -> {
    super.visitInsn(Opcodes.RETURN)
   }
  isPrimitiveType(returnTypeSort) -> {
    weavePrimitiveReturn(returnTypeSort)
   }
  else -> {
    val internalName = returnType.internalName
    super.visitTypeInsn(Opcodes.CHECKCAST, internalName)
    super.visitInsn(Opcodes.ARETURN)
   }
}

如前所示,invoke 函数的回来值是 Any? ,那么怎么回来给原始函数呢?咱们还是需要依据原始函数的回来值类型做判别:

  1. 如果是 voidd 类型,则直接 return
  2. 如果是根本数据类型,需要先强制类型转化为包装类型,再调用包装类型对应的 xxxValue 办法获取根本数据类型,最终再回来,
  3. 如果是引证类型,经过 returnType 获取回来值的 InternalName,然后进行强制类型转化,最终回来。

至此,署理转化剖析结束,happy~

总结

在想完成某个功用的时分,咱们或许会有好几种思路,怎么在这好几种思路中挑选一个进行完成,这其间考量与取舍的进程笔者觉得比较有趣。

本文记载了 Sword 的完成原理与源码剖析,同时记载了笔者完成代码时的一些思路与考虑,笔者个人认为这些思路与考虑远比完成这个功用更有意义。

下篇再见,happy~