问题
最近在用kotlin lambda的时候,遇到一个诡异的报错问题
java.lang.NoClassDefFoundError: Failed resolution of: Lcom/dependproject/AnonymousActivity$onCreate$s$1$1;复制代码
类定义找不到导致的异常。神奇了,这是个什么鬼类。我确实没有这个类,先贴上代码开始进行分析
val s = inlineLambda { object : AnonymousClass("") { override fun printName() { val context = this@AnonymousActivity } } } abstract class AnonymousClass { constructor(t:T) abstract fun printName() } inline fun T.inlineLambda(block : (T) -> R):R { return block(this) } 复制代码
概括
先观察一下这些代码,发现分别有lambda语法,inline内联函数还有匿名类。
好的,观察好了,并没有发现上面那个奇怪的类。这时候我们就需要借助工具去观察表面看不到的东西,也就是字节码。 使用Android Studio导航栏的Tool的Kotlin工具,把源码转成字节码如下:
L11 LINENUMBER 55 L11 NEW com/dependproject/AnonymousActivity$onCreate$$inlined$inlineLambda$lambda$1 DUP LDC "" ALOAD 0 INVOKESPECIAL com/dependproject/AnonymousActivity$onCreate$$inlined$inlineLambda$lambda$1.(Ljava/lang/Object;Lcom/dependproject/AnonymousActivity;)V L12 LINENUMBER 59 L12 L13 NOP L14 LINENUMBER 54 L14 CHECKCAST com/dependproject/AnonymousActivity$onCreate$s$1$1 ASTORE 2 L15复制代码
咦,发现没,那个诡异的类出现了,
CHECKCAST com/dependproject/AnonymousActivity$onCreate$s$1$1 复制代码
意思就是检查类型,对应上面的代码就是赋值给s的操作之一,赋值之前检查类的类型。那这个类不就是inlineLambda返回的类型吗?我们接着看下去,看inlineLambda方法,这个方法其实很简单,就是返回lambda表达式的类型,lambda表达式的类型其实是上面AnonymousClass匿名类的实现。
那为什么生成的类找不到呢,我们再看到上面的字节码,发现了一个也是奇怪名字的类
com/dependproject/AnonymousActivity$onCreate$$inlined$inlineLambda$lambda$1 复制代码
,迅速过一遍代码,发现这个类就是匿名类生成的。
那就奇怪了,为什么生成的匿名类名字和checkcast名字不一样呢。
分析
以前老师常常教我们,做题要大胆想象,认真推敲。其实分析也一样,大胆想象。观察checkcast的类名,发现名字有迹可循,是类名+方法名+$1$1,即com/dependproject/AnonymousActivity + onCreate + $1$1。那个new的匿名类名字比较长,不过规则一样。即com/dependproject/AnonymousActivity + onCreate + inlined + inlineLambda + 这些是编号来的,可以不用管,所以匿名类的名字组成都是有迹可循的。 那为什么名字不一样呢。
接下来就是我的大胆推断了。我认为通过inline内联函数传入的lambda表达式生成的匿名类名字组成规则 = class + method + inlined + method。而外部认为的 类名规则则为 class + method,所以因为编译器不够完善在inline的情况下导致生成的类名规则不统一导致的。哇, 这解释真牛逼。
乍听之下都是对的。我们来验证一下。
其实上面那个lambda表达式的干扰变量有点多,把方法里面的操作去掉,按照我上面的大胆解释,应该也是要报错的。
然而,啪啪打脸。非但不报错,而且生成的匿名类的名字恰恰是com/dependproject/AnonymousActivity$onCreate$s$1$1复制代码
这个。对比代码发现差异在于 this@AnonymousActivity这里。没办法,我们回头看看刚刚生成的字节码。
com/dependproject/AnonymousActivity$onCreate$$inlined$inlineLambda$lambda$1复制代码
发现没,有一个putfield其实就是赋值操作,给类的成员变量赋值AnonymousActivity对象。其实看到这里,我还是进行了大胆猜想。加上了指向外部的变量之后,匿名类的编译命名规则会有所改变,而规则就是上面刚刚说的那个规则。但是外部类在执行checkcast的时候,还是按照旧的规则去组装命名。这些纯粹是个人猜想,没有进行验证。
唠叨
对于内联函数,少了方法的调用,性能更加,通过字节码可以发现内联函数 + 匿名函数 + lambda其实会生成一个特定规则命名的类。
但是对于非内联函数,情况又不一样。我们先看到lambda这个方法的调用
lambda { object : AnonymousClass("") { override fun printName() { it } } }复制代码
对应的字节码
ALOAD 0 ALOAD 0 GETSTATIC com/dependproject/AnonymousActivity$onCreate$1.INSTANCE : Lcom/dependproject/AnonymousActivity$onCreate$1; CHECKCAST kotlin/jvm/functions/Function1 INVOKEVIRTUAL com/dependproject/AnonymousActivity.lambda (Ljava/lang/Object;Lkotlin/jvm/functions/Function1;)Ljava/lang/Object; POP复制代码
匿名类的返回字节码如下:
LINENUMBER 20 L1 NEW com/dependproject/AnonymousActivity$onCreate$1$1 DUP LDC "" INVOKESPECIAL com/dependproject/AnonymousActivity$onCreate$1$1.(Ljava/lang/Object;)V复制代码
发现调用lambda会产生两个类,一个是
AnonymousActivity$onCreate$1复制代码
另一个是
AnonymousActivity$onCreate$1$1复制代码
第一个用于lambda表达式的转换, 代码如下:
类继承于kotlin/jvm/internal/Lambda,其中有个方法,想象大家应该都认识,那就是invoke。平时对于lambda的调用有两种方式,
val lambdaFun = {i:Int -> i} lambdaFun(1) lambdaFun.invoke(1)复制代码
其中有一种就是invoke,就是在编译过程中产生的。 AnonymousActivity$onCreate$1$1就是返回的匿名类的实现类。
总结
大胆猜想,多多思考。
后话
感谢同事在kotlin论坛看到相关的问题以及讨论,。生成inlined命名的规则在,大家有兴趣可以瞧瞧。值得一提的是,这个bug在14年提出的。