0%

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

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 方法的生成:

  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函数。

下面的流程图看起来可能更清楚一些:

code flow

解析注解

首先定义一个 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(
/**
* 是否有[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 注解了:

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()

// 覆写`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 中了:

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 函数中:

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

至此解析 Proxy 注解完成,我们已经拿到注解中的数据,下面我们就可以开始转换了。

转换分支

对函数代理功能的转换,笔者实现了两种转换分支:

  1. 简单转换:或者称为默认转换,就像 switchdefault 分支一样,
  2. 代理转换:真正的代理功能实现。

转换逻辑在 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() {
// 判断是否有`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

下面是简单转换的实现代码片段:

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)
}
}
}

简单转换的实现逻辑比较简单,笔者就不再分析了,接下来我们看看今天的主角:代理转换。

代理转换

代理转换的实现逻辑较为复杂,以下几点是我们需要考虑的:

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

脑图如下:

proxy mind

下面我们就根据上述几点依次分析下:

1.是否是静态函数

1
2
3
4
5
6
7
8
9
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.构建实现类实例

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(), "/")
}
  1. 首先需要把 handler 字段中的实现类全限定名(Full-Qualified Name)转换成 ASM 里的 InternalName,比如:com.guodong.android.TestInvocationHandler 转换为 com/guodong/android/TestInvocationHandler,即把 . 替换成 /
  2. 上述片段中的第 4 行代码通过调用实现类的无参构造方法来构建实例,这就是为什么实现类必须有无参构造方法的原因,
  3. 后面两行代码是把创建出来的实例存储在方法的 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)
}

--------------------------------------------------------------------------

// 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 函数

1
2
3
4
5
6
7
super.visitMethodInsn(
Opcodes.INVOKEINTERFACE,
KT_INVOCATION_HANDLER_OWNER,
INVOKE_METHOD,
INVOCATION_HANDLER_INVOKE_DESC,
true /* isInterface */
)

调用 invoke 函数比较简单,通过调用 ASMvisitMethodInsn 方法传入正确的参数即可。注意最后一个参数要为 true,因为我们调用的是一个接口方法。

5.invoke函数的结果返回给原始函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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~

欢迎关注我的其它发布渠道