边实验边分析 - 非静态内部类引发的内存泄漏问题
对于Java程序员来说,内存泄漏想必是大家开发过程中经常会遇到的问题,有很多情况都会导致内存泄漏的发生,其根本问题是Java的内存回收管理器(GC)没法正常回收不在使用的对象,导致该对象一直残存在内存中,从而引起的内存泄漏,最终会导致的结果就是内存溢出。
在Android中,因为Android的架构和生态等因素,该问题也变得相对容易引起人们的注意,想必大家经常听有经验的Android开发同事说到这样一句话:“非静态的内部类会持有外部类的引用,使用时需要注意内存泄漏问题”。
听到这句话,我们就应该自己去分析一下,如何理解?为什么?以及怀疑其正确性,毕竟只有这样自己才会有所提高,所以我们从这边作为此次的突破口,先来分析一下这句话。
首先如何理解这句话,这句话里面有几个关键词,非静态、内部类、持有、外部类,
从非静态和内部类我们知道了我们肯定是定义了一个非静态的内部类,比如下面的代码
public class MainActivity2 extends AppCompatActivity {
private MessageCall messageCall;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
messageCall = new MessageCall();
findViewById(R.id.doSomething).setOnClickListener(view -> {
messageCall.doSomething();
});
}
class MessageCall {
public void doSomething(){
System.out.println("doSomething");
}
}
}
为了方便查看内存泄漏的情况,我们使用leakcanary去监测我们的内存情况,运行程序,点击按钮,然后按返回按钮,退回桌面,你会发现,不会造成内存泄漏,WHY?非静态,内部类都满足。
那么它会持有外部类的引用吗?我们来看一下Bytecode就可以知道了(可以从Build -> Analyze APK -> 选择相应dex -> 找到包名下的MainActivity2的Class -> 右键Show Bytecode查看)
可以看到onCreate方法里面我们new-instance了MainActivity2$ MessageCall,并且在init的时候传入了我们的MainActivity2,看到这里我们不妨看一下MainActivity2$MessageCall这个class
这里的 this$0其实就是MainActivity2,所以MessageCall持有了MainActivity2的引用。
那又是WHY?WHY?不会造成内存泄漏呢?
其实非静态的内部类会持有外部类的引用,会导致内存泄漏这句话不是一个绝对条件,还有一个条件,那就是内部类的生命周期要比外部类的生命周期要长,则会发生内存泄漏的问题。
我们来尝试一下在刚刚的doSomething方法中,加入一个线程,让它在后台运行20秒
class MessageCall {
public void doSomething(){
Runnable work = new Runnable() {
@Override public void run() {
System.out.println("doSomething start");
SystemClock.sleep(20000);
System.out.println("doSomething end");
}
};
new Thread(work).start();
}
}
我们再运行程序,点击按钮,然后按返回按钮,退回桌面,这次真的内存泄漏了
可以看出,MainActivity2 destory,但是MessageCall的this$0有泄漏而MessageCall又是由于MessageCall$1.this$1导致泄漏再来真真的泄漏源头是Thread-4,这个大家可以自行查看其字节码,可以发现Thread其实也是持有了外部类既MessageCall的引用的,所以导致了一连串的连锁反应,Thread- -> MessageCall -> MainActivity2。
那么,我们有什么办法可以解决这个问题呢?有一个很简单的解决方法,想必大家也都知道,那就是使用静态内部类,就可以了,这样内部类的生命周期就和外部类的生命周期没有直接联系,当外部类destory的时候,静态的内部类不受影响,自然不会内存泄漏。
我们分析并解决问题到这里,其实有这样做的人肯定会发现在实际操作上还是有一些问题的,
其中最主要的问题是使用静态内部类的话,就不能直接引用到外部类的一些成员变量了,这样代码感觉就变得复杂了,那么,我们还有什么办法呢?
其实在这个例子中,还存在一个很巧妙的技巧在里面,那就是使用lambda,我们来看一下这个方式,修改一下代码
class MessageCall {
public void doSomething(){
Runnable work = () -> {
System.out.println("doSomething start");
SystemClock.sleep(20000);
System.out.println("doSomething end");
};
new Thread(work).start();
}
}
运行程序你会发现,没有出现内存溢出
我们来研究一下字节码会有什么变化呢?我们直接看到MainActivity2$MessageCall里的字节码,主要看doSomething方法
这边可以看到会有个lambda,我们看一下lambda
发现其内部调用了MessageCall里的一个静态方法叫lambda$doSomething,回到MessageCall字节码中,找到了这个静态方法,看到这边想必知道原因了,因为这边是一个静态方法,所以不会导致持有MessageCall的引用,从而就不会导致了MessageCall里持有的MainActivity的引用不会释放,起到了一连串的连锁效应,这就是lambda的神奇之处了。
不过别高兴的太早,因为不是说lambda就完事了,我们使用的只是lambda中的一种,叫做Non-instance-capturing lambdas,就是说我们在lambda方法里面没有使用到外部实例,相反的,当然还有另一种,就是Instance-capturing lambdas,即对外部实例进行了绑架的lambda,我们来看看,如果我们在MainActivity里面定义一个int类型的值为0,然后在run的lambda线程里面让这个值重新赋值为1
private Integer i = 0;
class MessageCall {
public void doSomething(){
Runnable work = () -> {
System.out.println("doSomething start");
i = 1;
SystemClock.sleep(20000);
System.out.println("doSomething end");
};
new Thread(work).start();
}
}
这样我们再来运行一下的话,会发现,依然内存溢出,所以我们来看一下字节码,经过之前的分析,我们直接定位到了MessageCall字节码里面的doSomething方法
发现其不是static了,所以,当然也就会和我们之前不使用lambda的写法一样,内存溢出。
总结一下的话,在内部类的生命周期要比外部类的生命周期长的前提条件下:
1.不使用lambda,非静态类,方法内不使用外部实例,会导致内存泄漏
2.不使用lambda,非静态类,方法内使用外部实例,会导致内存泄漏
3.使用lambda,非静态类,方法内不使用外部实例,不会内存泄漏
4.使用lambda,非静态类,方法内使用外部实例,会导致内存泄漏
5.静态类,则不会导致内存溢出
这样一总结,大家是不是对lambda的理解又更近一步了呢。