抛砖引玉
大家都知道,针对任务处理、异步回调等操作,都喜欢通过匿名内部类构建一个对象,在任务代码执行完成后通过对象将结果回调回来。这里先抛个点出来,这样做是有风险的。
咦?有什么风险?一般大家不都是这样做的吗?
大家看下下面代码
class HandlerTestActivity : AppCompatActivity() {
private val mHandler = Handler(Looper.getMainLooper())
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mHandler.postDelayed({
val a = "Ignore"
Log.d("xiaoma", "a = $a")
}, 10000)
}
}
你认为这个代码有风险吗?为什么有?又为什么没有?
认为没有的,当Activity在打开10s内退出后,Handler设置的延时任务还会不会执行?
答案是会,那这会造成什么后果呢?
答案是会出现Activity的内存泄漏,为什么会泄漏?
答案是延时任务对象是通过匿名内部类构建的,其拥有外部类对象,这就让Activity退出后,GC释放不了Activity对象所占用的内存块,引发内存泄漏。
咦?为什么匿名内部类会持有外部类对象?
为什么匿名内部类会持有外部类对象?
先不直接解答这个问题
我们构建一个外部类包含内部类,然后查看其字节码,看看会发生什么。
构建一个包含内部类的外部类
public class Outer {
private String outerName = "Outer";
public void run() {
String str = "fwefwe";
int a = 1;
new Runnable() {
@Override
public void run() {
System.out.println(str + a + outerName);
}
};
}
}
外部类Outer中构建了一个匿名内部类对象,我们通过javac可以将源码编译成字节码,代码如下
javac Outer.java
执行命令后得到:
- Outer.class
- Outer$1.class
咦?为什么一个Outer.java编译后生成了两个class字节码文件?
我们看看Outer.class长什么样子,命令为
javap -c Outer.class
执行结果如下
xiaoma@xiaomadeMacBook-Pro handler % javap -c Outer.class
Compiled from "Outer.java"
public class com.example.test.ui.handler.Outer {
public com.example.test.ui.handler.Outer();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: aload_0
5: ldc #2 // String Outer
7: putfield #3 // Field outerName:Ljava/lang/String;
10: return
public void run();
Code:
0: ldc #4 // String fwefwe
2: astore_1
3: iconst_1
4: istore_2
5: new #5 // class com/example/test/ui/handler/Outer$1
8: dup
9: aload_0
10: aload_1
11: iload_2
12: invokespecial #6 // Method com/example/test/ui/handler/Outer$1."<init>":(Lcom/example/test/ui/handler/Outer;Ljava/lang/String;I)V
15: pop
16: return
}
可以看到,run方法中new了一个叫做“com/example/test/ui/handler/Outer$1”类的对象。
咦,不是应该new Runnable吗?另外,为什么new时构造器传参是(Outer , String)的?
想要解开这个谜题,我们得先看看“com/example/test/ui/handler/Outer$1”又是长什么样子的
我们直接AS打开Outer$1.class,可以更容易发现答案
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.test.ui.handler;
class Outer$1 implements Runnable {
Outer$1(Outer this$0, String var2, int var3) {
this.this$0 = this$0;
this.val$str = var2;
this.val$a = var3;
}
public void run() {
System.out.println(this.val$str + this.val$a + this.this$0.outerName);
}
}
发现了吗,java编译器构建了一个名为Outer$1的类去实现、继承匿名内部类对应的类,并构建了一个有参构造器,入参规则是:外部类对象 + 匿名内部类使用到的外部类中的成员变量(上面用到了外部类的字符串,构造的有参构造器就会增加这个String的参数)
结论:
经过上面验证,发现,Java编译器在编译代码时,会构建一个匿名内部类子类,并构建一个有参构造器传入外部类对象以及所需要的外部类成员对象,这样以来,就出现了我们说的匿名内部类持有外部类对象的说法
非静态内部类都会持有外部类的对象,静态内部类就不会,咦?为什么?
为什么静态内部类不会持有外部类对象?
同样的我们看看下面代码
public class Outer {
private static String outerName = "Outer";
public static class Inner {
public static void run() {
System.out.println(outerName);
}
}
}
javac Outer.java 得到两个文件
- Outer.class
- Outer$Inner.class
我们看看Outer.class长啥样
Compiled from "Outer.java"
public class com.example.test.ui.handler.Outer {
public com.example.test.ui.handler.Outer();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void run();
Code:
0: new #2 // class com/example/test/ui/handler/Outer$Inner
3: dup
4: invokespecial #3 // Method com/example/test/ui/handler/Outer$Inner."<init>":()V
7: pop
8: return
static {};
Code:
0: ldc #4 // String Outer
2: putstatic #5 // Field outerName:Ljava/lang/String;
5: return
}
可以看到,我们在构建Inner对象时,是通过无参构造器构建的,不会将外部类对象传入静态内部类的情况
我看也看看Outer$Inner.class
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package com.example.test.ui.handler;
public class Outer$Inner {
public Outer$Inner() {
}
public static void run() {
System.out.println(Outer.outerName);
}
}
发现,这里也不存在使用外部类对象访问其成员对象的情况。
咦,这里其实还可以佐证一个知识点:
静态内部类,只能访问到外部类的静态成员变量
为啥?因为静态内部类不持有外部类对象,要想访问外部类成员变量,那这个变量就得是静态的,方便通过 外部类.静态成员变量 的方式访问。