问题阐述

kotlin 运行这段代码报java.lang.NoClassDefFoundError错误(表示运行中找不到类的定义)。如下代码:

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

}
})
}
}

经过替换尝试,报错不是Glide的锅。根据kotlin默认最后一行是返回值的规则,这代码最后let下面最后一个返回对象是CustomTarget的匿名内部类对象。因为:

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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()函数代码会报一样的错误。

1
2
3
4
5
6
7
8
9
10
11
12
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代码:

1
2
3
4
5
6
7
8
9
10
11
//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出来捣乱?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//生成的类。
@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代码:

1
2
3
4
5
6
7
8
9
// 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);
}

1
2
3
4
5
6
7
8
9
10
11
12
//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;
}
}
1
2
3
4
5
6
7
8
9
10
11
//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);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
//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;
}

}
1
2
3
4
5
6
7
8
9
//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方法的定义。

1
2
3
4
5
6
7
@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,实际上严格用法应该这样:

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

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

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

1
2
3
4
5
6
7
8
9
fun f(u: String) {
a?.let<Any, Inter> {
object : Inter {
override fun a() {
print(u)
}
}
}
}

对应的Java代码。

1
2
3
4
5
6
7
8
9
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

1
2
3
fun <T, R> T.mylet(block: (T) -> R): R {
return block(this)
}

1
2
3
4
5
6
7
8
9
10
//No.7
fun f(u: String) {
a?.mylet {
object : Inter {
override fun a() {
print(u)
}
}
}
}

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

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
//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的时候,还是按照旧的规则去组装命名。(仅为猜想,未得原因)

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

配一张图