美文网首页
kotlin使用let报java.lang.NoClassDef

kotlin使用let报java.lang.NoClassDef

作者: wzmyyj | 来源:发表于2019-11-13 16:27 被阅读0次

    问题阐述

    如下代码:

      private fun shareUrlToFriend(logoUrl: String) {
            activity?.let {
                Glide.with(this)
                        .asBitmap()
                        .load(logoUrl)
                        .into(object : CustomTarget<Bitmap>() {
                            override fun onLoadCleared(placeholder: Drawable?) {
    
                            }
    
                            override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
                                print(logoUrl)//就是一个方法使用了logoUrl
    
                            }
                        })
            }
        }
    

    运行这段代码报java.lang.NoClassDefFoundError错误(表示运行中找不到类的定义)。经过替换尝试,报错不是Glide的锅。根据kotlin默认最后一行是返回值的规则,这代码最后let下面最后一个返回对象是CustomTarget的匿名内部类对象。因为:

    // Glide into()方法
    @NonNull
      public <Y extends Target<TranscodeType>> Y into(@NonNull Y target) {
        return into(target, /*targetListener=*/ null, Executors.mainThreadExecutor());
      }
    

    所以,可以把上面代码替换成如下简单代码:

    class T {
        var a: Any? = null
        fun f(u: String) {
            a?.let {
                object : Inter {
                    override fun e() {
                        print(u)
                    }
                }
            }
        }
    
    }
    
    fun main() {
        val t = T()
        t.a = Any()
        t.f("u")
    }
    
    interface Inter {
        fun e()
    }
    

    这段执行f()函数代码会报一样的错误。

    Exception in thread "main" java.lang.NoClassDefFoundError: com/a/wzm/shere/ui/T$f$1$1
        at com.a.wzm.shere.ui.T.f(Test.kt:14)
        at com.a.wzm.shere.ui.TestKt.main(Test.kt:28)
        at com.a.wzm.shere.ui.TestKt.main(Test.kt)
    Caused by: java.lang.ClassNotFoundException: com.a.wzm.shere.ui.T$f$1$1
        at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
        ... 3 more
    
    Process finished with exit code 1
    

    T$f$1$1是个什么鬼?

    问题追溯

    查看翻译后的Java代码:

    //NO.0   主要看f()函数。
    public final void f(@NotNull String u) {
          Intrinsics.checkParameterIsNotNull(u, "u");
          if (this.a != null) {
             boolean var3 = false;
             boolean var4 = false;
             int var6 = false;
             1 var10000 = (1)(new T$f$$inlined$let$lambda$1(u));// 1是啥?
          }
    
       }
    

    里面这个1就是不没定义的类型(也就是报错里面需要定义的T$f$1$1)。为什么有个1出来捣乱?

    //生成的类。
    @Metadata(
       mv = {1, 1, 15},
       bv = {1, 0, 3},
       k = 1,
       d1 = {"\u0000\u0011\n\u0000\n\u0002\u0018\u0002\n\u0000\n\u0002\u0010\u0002\n\u0000*\u0001\u0000\b\n\u0018\u00002\u00020\u0001J\b\u0010\u0002\u001a\u00020\u0003H\u0016¨\u0006\u0004¸\u0006\u0000"},
       d2 = {"com/a/wzm/shere/ui/Te$f$1$1", "Lcom/a/wzm/share/ui/Inter;", "a", "", "app"}
    )
    public final class Te$f$$inlined$let$lambda$1 implements Inter {
       // $FF: synthetic field
       final String $u$inlined;
    
       Te$f$$inlined$let$lambda$1(String var1) {
          this.$u$inlined = var1;
       }
    
       public void a() {
          String var1 = this.$u$inlined;
          boolean var2 = false;
          System.out.print(var1);
       }
    }
    

    类名跟注解里的("com/a/wzm/share/ui/Te$f$1$1")不一致。

    实验

    对上诉代码简单修改测试。发现只需要简单修改就不报错。比如:

    1. 不用 let第一行用if(a==null)return。(规避了let的问题,不讨论)。
    2. a后面的?去掉。
    3. 方法e()里不使用u
    4. object: Inter前面加上val x=进行赋值掉。
    5. let最后一行写个1,true或其他明确类型的东西。

    而这样做,报一样错误。

    1. a?.let前面加val c=进行赋值操作。此时就算?(如2所诉) 去掉也报错。结合3,4,5不报错。

    把上诉实验通过转换,查看翻译后的Java代码:

    // NO.2
     public final void f(@NotNull String u) {
          Intrinsics.checkParameterIsNotNull(u, "u");
          Object var2 = this.a;
          boolean var3 = false;
          boolean var4 = false;
          int var6 = false;
          new T$f$$inlined$let$lambda$1(u);
       }
    
    //NO.3
    public final void f(@NotNull String u) {
          Intrinsics.checkParameterIsNotNull(u, "u");
          if (this.a != null) {
             boolean var3 = false;
             boolean var4 = false;
             int var6 = false;
             new T$f$1$1();
          } else {
             Object var10000 = null;
          }
       }
    
    //NO.4
    public final void f(@NotNull String u) {
          Intrinsics.checkParameterIsNotNull(u, "u");
          if (this.a != null) {
             boolean var3 = false;
             boolean var4 = false;
             int var6 = false;
             new T$f$$inlined$let$lambda$1(u);
          }
    
       }
    
    //NO.5,最后一行加了个true
    public final void f(@NotNull String u) {
          Intrinsics.checkParameterIsNotNull(u, "u");
          if (this.a != null) {
             boolean var3 = false;
             boolean var4 = false;
             int var6 = false;
             new T$f$$inlined$let$lambda$1(u);
             boolean var10000 = true;
          }
    
       }
    
    //NO.6 又出现未知类型:1
    public final void f(@NotNull String u) {
          Intrinsics.checkParameterIsNotNull(u, "u");
          Object var3 = this.a;
          boolean var4 = false;
          boolean var5 = false;
          int var7 = false;
          1 x = (1)(new T$f$$inlined$let$lambda$1(u));
       }
    
    区别 匿名类 是否出现类型(1)
    NO.0(原始) T$f$$inlined$let$lambda$1
    NO.2 T$f$$inlined$let$lambda$1
    NO.3 T$f$1$1
    NO.4 T$f$$inlined$let$lambda$1
    NO.5 T$f$$inlined$let$lambda$1
    NO.6 T$f$$inlined$let$lambda$1

    首先分析第4点和第5点。第4点把匿名类对象赋值给了x,这意味这let只能取下一行做返回值(没有下行,就是Unit)。所以也就是说let有明确返回值就不报错。
    没有明确返回值类型且“不关心”返回值(如NO.2),也不会错。NO.0也不“关心”返回值啊?可是NO.0又对a的空判断,对let返回又两种结果,要么有返回,要么没返回,将也它归纳为“关心”结果。
    至于NO.3的情况(就算结合NO.6也不报错),生成的类就是NO.0中报错中没定义的类(为什么会这样,稍后讨论)。所以也就没问题了。

    问题1

    为什么let“关心”返回值会有区别?let的返回值究竟是个啥?

    分析

    首先,查看let方法的定义。

    @kotlin.internal.InlineOnly
    public inline fun <T, R> T.let(block: (T) -> R): R {
        contract {
            callsInPlace(block, InvocationKind.EXACTLY_ONCE)
        }
        return block(this)
    }
    

    let 是泛型的扩展方法,参数是个高阶函数。作用是将一种类型T,通过block方法变换变成类型R

    所以let返回值是个R。那R是什么呢?

    当然是自己定喽。我们一直都是直接用let,实际上严格用法应该这样:

        val a = 1
        val s = a.let<Int, String> {
            return@let it.toString()
        }
    

    只不过,kotlin的自动类型识别帮我们做了类型区分。

    问题中的类型1估计就是不明确的R。所以我们手动指定累行试试。

    fun f(u: String) {
           a?.let<Any, Inter> {
                object : Inter {
                    override fun a() {
                        print(u)
                    }
                }
            }
        }
    

    对应的Java代码。

    public final void f(@NotNull String u) {
         Intrinsics.checkParameterIsNotNull(u, "u");
         if (this.a != null) {
            boolean var3 = false;
            boolean var4 = false;
            int var6 = false;
            Inter var10000 = (Inter)(new T$f$$inlined$let$lambda$1(u));
         }
      }
    

    原本是1的地方变成了我们指定的类型Inter。代码跑起来也不在跌跟斗。

    那为什么“不关心”结果的时候翻译过来就不需要转 R 的类型呢?(目前找到的解释是编译器优化掉明确不需要的过程)

    问题2

    看这个问题之前,我们应该发现了,let里的代码直接被拷到f()函数里面,而不是生成高阶函数block: (T) -> R表示的接口的实现类。而且Inter本该内部类的实现,也变成了定义了外部独立的类。为什么会这样?

    解释

    这是因为inline关键字的作用:inline 的工作原理就是将内联函数的函数体复制到调用处实现内联。详情见参考资料

    实验

    写一个没有inline的仿let方法,再替换原let

    fun <T, R> T.mylet(block: (T) -> R): R {
        return block(this)
    }
    
    //No.7
    fun f(u: String) {
            a?.mylet {
                object : Inter {
                    override fun a() {
                        print(u)
                    }
                }
            }
        }
    

    运行不报错,看法Java代码。

    //NO.7
    public final void f(@NotNull final String u) {
          Intrinsics.checkParameterIsNotNull(u, "u");
          Object var10000 = this.a;
          if (var10000 != null) {
             <undefinedtype> var2 = (<undefinedtype>)TestKt.mylet(var10000, (Function1)(new Function1() {
                // $FF: synthetic method
                // $FF: bridge method
                public Object invoke(Object var1) {
                   return this.invoke(var1);
                }
    
                @NotNull
                public final <undefinedtype> invoke(@NotNull Object it) {
                   Intrinsics.checkParameterIsNotNull(it, "it");
                   return new Inter() {
                      public void a() {
                         String var1 = u;
                         boolean var2 = false;
                         System.out.print(var1);
                      }
                   };
                }
             }));
          }
    
       }
    

    内部用Function1 代表block: (T) -> R表示的接口的实现类。Inter依旧是内部类的方式实现。

    可见,inline关键字在处理方法体中的内部类,做了明显的优化处理(即生成一个独立的外部类)。

    问题3

    结合问题1中的NO.3和问题2以及实验,发现Inter中用到let外部信息时,生成的类是T$f$$inlined$let$lambda$1(但后面还会强转为T$f$1$1,也就是那个1,然后报找不到类的定义的错误),而不使用外部的信息时,生成的类是T$f$1$1。区分度是什么?

    猜想

    通过inline内联函数传入的lambda表达式生成的匿名类,如果有指向外部的变量,那么命名为:class + method + inlined + method + lambda + number。如果没有,命名为: class + method + number + number(第一个number表示let同层级编号,第二个number表示内部类的编号。而然运行中外部类在执行checkcast的时候,还是按照旧的规则去组装命名。(仅为猜想,未得原因)

    结论

    其实还没有具体结论!遇到此类问题,大胆猜想,动手实践,总结规律。

    配一张图

    相关文章

      网友评论

          本文标题:kotlin使用let报java.lang.NoClassDef

          本文链接:https://www.haomeiwen.com/subject/ctcxictx.html