Sword - 为 Kotlin 函数增加代理功能(二)
简介
Sword:一个可以给 Kotlin 函数增加代理的第三方库,基于 KCP 实现。
前言
续接 上篇 ,在上篇文章中笔者记录了搭建 Sword 的基础开发环境以及技术选型为:注解 + KCP + ASM。本文主要记录使用 ASM 的实现过程。
首先看下上篇文章最后没有记录的 ClassBuilder
。
ClassBuilder
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 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 方法的生成:
首先判断是否是函数描述符,否则直接返回,
若是操作符重载、中缀、内联、挂起以及尾递归函数,不予处理,直接返回,
函数若是不存在 Proxy
注解,不予处理,直接返回,
获取真实的类名,交予 SwordAdapter
处理。
可以看出在 ClassBuilder
中主要是实现了一些校验逻辑,第 2 步中的过滤逻辑可增加配置参数提供给集成方在外部灵活配置。
接下来我们看下 SwordAdapter
是如何处理的吧。
SwordAdapter
SwordAdapter
的逻辑较为复杂,笔者先描述下自己的实现思路,然后再按照思路一点点分析。
首先通过 ASM 判断当前函数是否存在 Proxy
注解,若存在则解析出注解中的数据暂存起来,否则不予转换,
若存在Proxy
注解并解析出注解中的数据,则根据注解中的 enable
字段判断是否启用代理,若启用则进行转换,否则不予转换,
若进行转换,再判断注解中的 handler
字段是否为空字符串,若是空字符串则进行简单的转换,否则进行代理转换,
简单转换:根据函数返回类型判断
无返回值类型返回 void
,
基本数据类型返回:-1
,char
类型返回 48
,
引用类型返回 null
。
代理转换:替换handler
字段中的全限定名,调用InvocationHandler#invoke
函数。
下面的流程图看起来可能更清楚一些:
解析注解
首先定义一个 Proxy
注解数据实体类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 internal data class SwordParam ( var hasProxyAnnotation: Boolean = false , var enable: Boolean = true , var handler: String = "" ) { companion object { internal const val PARAM_ENABLE = "enable" internal const val PARAM_HANDLER = "handler" } }
此实体类存储 Proxy
注解中解析出来的数据,下面就是解析 Proxy
注解了:
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 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()override fun visitAnnotation (descriptor: String , visible: Boolean ) : AnnotationVisitor { var av = super .visitAnnotation(descriptor, visible) if (proxyDesc.contains(descriptor)) { param.hasProxyAnnotation = true if (av != null ) { av = AnnotationAdapter(api, av, param) } } return av }
解析注解数据主要覆写 visitAnnotation
函数,在此函数中首先判断是否存在 Proxy
注解,若存在则进行解析,否则不予处理。
解析逻辑就在下面代码的 AnnotationAdapter
中了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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
函数中:
第一个参数 name
表示注解中参数的名称,第二参数 value
表示注解中参数的值,
通过比对 name
参数的名称来解析注解中的数据并存储在实体中。
至此解析 Proxy
注解完成,我们已经拿到注解中的数据,下面我们就可以开始转换了。
转换分支
对函数代理功能的转换,笔者实现了两种转换分支:
简单转换 :或者称为默认转换,就像 switch
有 default
分支一样,
代理转换 :真正的代理功能实现。
转换逻辑在 visitCode
函数中处理,我们先看看转换分支的选择:
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 override fun visitCode () { if (param.hasProxyAnnotation && param.enable) { super .visitInsn(Opcodes.ICONST_1) val label = Label() super .visitJumpInsn(Opcodes.IFEQ, label) val methodType = Type.getMethodType( methodDescriptor ) val returnType = methodType.returnType val handler = param.handler if (handler.isNotEmpty()) { weaveHandler(methodType, returnType, handler) } else { weaveDefaultValue(returnType) } super .visitLabel(label) } super .visitCode() }
visitCode
函数的前部分是一些判断处理:
如果有Proxy
注解且启用了代理,则通过 ASM
先织入 if (true)
条件判断语句,
接下来获取函数的 methodType
和 returnType
,分别表示在 ASM
眼中的函数类型和返回值类型,
最后判断 handler
是否是空字符串来决定执行哪种转换分支。
简单转换
简单转换的实现是根据函数返回类型判断:
无返回值类型返回 void
,
基本数据类型返回:-1
,char
类型返回 48
,
引用类型返回 null
。
下面是简单转换的实现代码片段:
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 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) } } }
简单转换的实现逻辑比较简单,笔者就不再分析了,接下来我们看看今天的主角:代理转换。
代理转换
代理转换的实现逻辑较为复杂,以下几点是我们需要考虑的:
原始函数是否是静态函数:非静态函数(不包括构造函数)的第零位参数始终是 this
,
如何构建 InvocationHandler
实现类的实例,
如何获取 InvocationHandler#invoke
函数所需的参数,
如何调用 InvocationHandler#invoke
函数,
调用 InvocationHandler#invoke
函数后的结果如何返回给原始函数。
脑图如下:
下面我们就根据上述几点依次分析下:
1.是否是静态函数
1 2 3 4 5 6 7 8 9 val argumentTypes = t.argumentTypesval argumentSize = argumentTypes.sizeval isStaticMethod = methodAccess and Opcodes.ACC_STATIC != 0 var localSize = if (isStaticMethod) 0 else 1 val firstSlot = localSizefor (argType in argumentTypes) { localSize += argType.size }
首先判断是否是静态函数,其中一个目的是为了找到函数第一个参数的起始位置,以及计算整个方法的 locals
大小,为后续存储 InvocationHandler
实现类实例做准备:
firstSlot
即为第一个参数的起始位置,后面会使用到,
localSize
即为整个方法的 lcoals
大小,通过遍历函数参数得到。
2.构建实现类实例
1 2 3 4 5 6 7 8 9 10 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(), "/" ) }
首先需要把 handler
字段中的实现类全限定名(Full-Qualified Name
)转换成 ASM
里的 InternalName
,比如:com.guodong.android.TestInvocationHandler
转换为 com/guodong/android/TestInvocationHandler
,即把 .
替换成 /
。
上述片段中的第 4 行代码通过调用实现类的无参构造方法来构建实例,这就是为什么实现类必须有无参构造方法的原因,
后面两行代码是把创建出来的实例存储在方法的 locals
上并再次加载出来以备后用。
3.获取所需参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 super .visitLdcInsn(className)super .visitLdcInsn(methodName)weaveInt(argumentSize) super .visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/Object" )if (argumentTypes.isNotEmpty()) { weaveArgs(argumentTypes, argumentSize, firstSlot) } -------------------------------------------------------------------------- interface InvocationHandler { fun invoke (className: String , methodName: String , args: Array <Any ?>) : Any? }
上面代码片段的最后是 InvocationHandler
接口的声明,如上所示,invoke
函数需要 3 个参数,分别为:
当前的类名,
当前的函数名,
当前函数声明参数的数组。
下面分析下获取参数的逻辑:
代码片段的前两行代码我们织入了前两个参数,
第 3 行代码我们织入参数数组的大小,
第 4 行代码构建参数数组实例,
最后面的 if
条件判断逻辑是把原始函数的参数放进数组内。
4.调用 invoke
函数
1 2 3 4 5 6 7 super .visitMethodInsn( Opcodes.INVOKEINTERFACE, KT_INVOCATION_HANDLER_OWNER, INVOKE_METHOD, INVOCATION_HANDLER_INVOKE_DESC, true )
调用 invoke
函数比较简单,通过调用 ASM
的 visitMethodInsn
方法传入正确的参数即可。注意最后一个参数要为 true
,因为我们调用的是一个接口方法。
5.invoke
函数的结果返回给原始函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 val returnTypeSort = returnType.sortwhen { 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?
,那么如何返回给原始函数呢?我们还是需要根据原始函数的返回值类型做判断:
如果是 voidd
类型,则直接 return
,
如果是基本数据类型,需要先强制类型转换为包装类型,再调用包装类型对应的 xxxValue
方法获取基本数据类型,最后再返回,
如果是引用类型,通过 returnType
获取返回值的 InternalName
,然后进行强制类型转换,最后返回。
至此,代理转换分析完毕,happy~
总结
在想实现某个功能的时候,我们可能会有好几种思路,如何在这好几种思路中选择一个进行实现,这其中考量与取舍的过程笔者觉得比较有趣。
本文记录了 Sword
的实现原理与源码分析,同时记录了笔者实现代码时的一些思路与思考,笔者个人认为这些思路与思考远比实现这个功能更有意义。
下篇再见,happy~