问题
最近在用kotlin lambda的时候,遇到一个诡异的报错问题
java.lang.NoClassDefFoundError: Failed resolution of: Lcom/dependproject/AnonymousActivity$onCreate$s$1$1;
类定义找不到导致的异常。神奇了,这是个什么鬼类。我确实没有这个类,先贴上代码开始进行分析
val s = inlineLambda {
object : AnonymousClass<String>("") {
override fun printName() {
val context = this@AnonymousActivity
}
}
}
abstract class AnonymousClass<T> {
constructor(t:T)
abstract fun printName()
}
inline fun <T, R> 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.<init> (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<String>("") {
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.<init> (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就是返回的匿名类的实现类。
总结
大胆猜想,多多思考。
后话
感谢同事salmon zhang在kotlin论坛看到相关的问题以及讨论,传送门。生成inlined命名的规则在这个文件,大家有兴趣可以瞧瞧。值得一提的是,这个bug在14年提出的。