问题阐述
如下代码:
private fun shareUrlToFriend(logoUrl: String) {
activity?.let {
Glide.with(this)
.asBitmap()
.load(logoUrl)
.into(object : CustomTarget() {
override fun onLoadCleared(placeholder: Drawable?) {
}
override fun onResourceReady(resource: Bitmap, transition: Transition?) {
print(logoUrl)//就是一个方法使用了logoUrl
}
})
}
}
运行这段代码报java.lang.NoClassDefFoundError错误(表示运行中找不到类的定义)。经过替换尝试,报错不是Glide的锅。根据kotlin默认最后一行是返回值的规则,这代码最后let
下面最后一个返回对象是CustomTarget的匿名内部类对象。因为:
// Glide into()方法
@NonNull
public > 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"
)不一致。
实验
对上诉代码简单修改测试。发现只需要简单修改就不报错。比如:
- 不用
let
第一行用if(a==null)return
。(规避了let的问题,不讨论)。 -
a
后面的?
去掉。 - 方法
e()
里不使用u
-
object: Inter
前面加上val x=
进行赋值掉。 - 在
let
最后一行写个1,true或其他明确类型的东西。
而这样做,报一样错误。
-
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.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 {
return@let it.toString()
}
只不过,kotlin的自动类型识别帮我们做了类型区分。
问题中的类型1
估计就是不明确的R
。所以我们手动指定累行试试。
fun f(u: String) {
a?.let {
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.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) {
var2 = ()TestKt.mylet(var10000, (Function1)(new Function1() {
// $FF: synthetic method
// $FF: bridge method
public Object invoke(Object var1) {
return this.invoke(var1);
}
@NotNull
public final 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的时候,还是按照旧的规则去组装命名。(仅为猜想,未得原因)
结论
其实还没有具体结论!遇到此类问题,大胆猜想,动手实践,总结规律。