Kotlin的一次lambda探险

1,660 阅读4分钟

问题

最近在用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 + 1,这些是编号来的,可以不用管,所以匿名类的名字组成都是有迹可循的。 那为什么名字不一样呢。
接下来就是我的大胆推断了。我认为通过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年提出的。