一、前言:
嗨,大家好,问大家一个“简单”的问题:
Handler内存泄露的原因是什么?
"内部类持有了外部类的引用,也就是Hanlder持有了Activity的引用,从而导致无法被回收呗。"
其实这样回答是错误的,或者说没回答到点子上。
二、Handler内存泄露的原因是什么?
1、原因一延迟消息
Handler导致内存泄漏一般发生在发送延迟消息的时候,当Activity关闭之后,延迟消息还没发出,那么主线程中的MessageQueue就会持有这个消息的引用,而这个消息是持有Handler的引用,而handler作为匿名内部类持有了Activity的引用,所以就有了以下的一条引用链。
主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity
其根本原因是因为这条引用链的头头,也就是主线程,是不会被回收的,所以导致Activity无法被回收,出现内存泄漏,其中Handler
只能算是导火索。
2、原因二子线程不回收
而我们平时用到的子线程通过Handler更新UI,其原因是因为运行中的子线程不会被回收,而子线程持有了Actiivty的引用(不然也无法调用Activity的Handler),所以就导致内存泄漏了,但是这个情况的主要原因还是在于子线程本身。
所以综合两种情况,在发生内存泄漏的情况中,Handler都不能算是罪魁祸首,罪魁祸首(根本原因)都是他们的头头——线程。
3、解决方案:
- 1、改写成静态内部类+弱引用;
- 2、activity在destroy的时候对handler回收;
三、 Handler发生内存泄漏的情况
1、发送延迟消息
第一种情况,是通过handler发送延迟消息:
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_handler)
btn.setOnClickListener {
//跳转到HandlerActivity
startActivity(Intent(this, HandlerActivity::class.java))
}
}
}
class HandlerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_handler2)
//发送延迟消息
mHandler.sendEmptyMessageDelayed(0, 20000)
btn2.setOnClickListener {
finish()
}
}
val mHandler = object : Handler() {
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
btn2.setText("2222")
}
}
}
我们在HandlerActivity中,发送一个延迟20s的消息。然后打开HandlerActivity后,马上finish。看看会不会内存泄漏。
我们的HandlerActivity发生了内存泄漏,从引用路径来看,是被匿名内部类的实例mHandler持有引用了,而Handler的引用是被Message持有了,Message引用是被MessageQueue持有了...
结合我们所学的Handler知识和这次引用路径分析,这次内存泄漏完整的引用链应该是:
主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity
所以这次引用的头头就是主线程,主线程肯定是不会被回收的,只要是运行中的线程都不会被JVM回收,跟静态变量一样被JVM特殊照顾。
这次内存泄漏的原因算是搞清楚了,当然Handler内存泄漏的情况不光这一种,
2、子线程运行没结束
第二个实例,是我们常用到的,在子线程中工作,比如请求网络,然后请求成功后通过Handler进行UI更新。
class HandlerActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_handler2)
//运行中的子线程
thread {
Thread.sleep(20000)
mHandler.sendEmptyMessage(0)
}
btn2.setOnClickListener {
finish()
}
}
val mHandler = object : Handler() {
override fun handleMessage(msg: Message?) {
super.handleMessage(msg)
btn2.setText("2222")
}
}
}
可以发现,这里的内存泄漏主要的原因是因为这个运行中的子线程,由于子线程这个匿名内部类持有了外部类的引用,而子线程本身是一直在运行的,刚才说过运行中的线程是不会被回收的,所以这里内存泄漏的引用链应该是:
运行中的子线程 —> Activity
当然,这里的Handler也是持有了Activity的引用的,但主要引起内存泄漏的原因还是在于子线程本身,就算子线程中不用Handler,而是调用Activity的其他变量或者方法还是会发生内存泄漏。
所以这种情况我觉得不能看作Handler引起内存泄漏的情况,其根本原因是因为子线程引起的,如果解决了子线程的内存泄漏,比如在Activity销毁的时候停止子线程,那么Activity就能正常被回收,那么也不存在Handler的问题了。
3、延伸问题1:内部类为什么会持有外部类的引用
- 这是因为内部类虽然和外部类写在同一个文件中,但是编译后还是会生成不同的class文件,其中内部类的构造函数中会传入外部类的实例,然后就可以通过this$0访问外部类的成员。
- 其实也挺好理解的吧,因为在内部类中可以调用外部类的方法,变量等等,所以肯定会持有外部类的引用的。
贴一段内部类在编译后用JD-GUI查看的class代码,也许你能更好的理解:
//原代码
class InnerClassOutClass{
class InnerUser {
private int age = 20;
}
}
//class代码
class InnerClassOutClass$InnerUser {
private int age;
InnerClassOutClass$InnerUser(InnerClassOutClass var1) {
this.this$0 = var1;
this.age = 20;
}
}
4、延伸问题2:kotlin中的内部类与Java有什么不一样吗
这是因为在kotlin中的匿名内部类分为两种情况:
- 在Kotlin中,匿名内部类如果没有使用到外部类的对象引用时候,是不会持有外部类的对象引用的,此时的匿名内部类其实就是个静态匿名内部类,也就不会发生内存泄漏。
- 在Kotlin中,匿名内部类如果使用了对外部类的引用,像我刚才使用了btn2,这时候就会持有外部类的引用了,就会需要考虑内存泄漏的问题。
所以我特意加了这一句,让匿名内部类持有外部类的引用,复现内存泄漏问题。
同样kotlin中对于内部类也是和Java有区别的:
- Kotlin中所有的内部类都是默认静态的,也就都是静态内部类。
- 如果需要调用外部的对象方法,就需要用inner修饰,改成和Java一样的内部类,并且会持有外部类的引用,需要考虑内存泄漏问题。
四、内存泄漏的代码
原因:在Activity内将Handler声明成
非静态内部类
或者匿名内部类
,这样Handle默认持有外部类Activity的引用。如果Activity在销毁时,Handler还有未执行完或者正在执行的Message,而Handler又持有Activity的引用,导致GC无法回收Activity,导致内存泄漏。如以下两种情形可能导致内存泄漏。
1、在Activity内将Handler声明成匿名内部类
//匿名内部类
private Handler mHandler = new Handler() {
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
};
或
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
//大量的操作,activity要销毁时还没结束
}
},1000);
2、在Activity内将Handler声明成非静态内部类:
//非静态内部类
private class MyHandler extends Handler{
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
}
private MyHandler mHandler = new MyHandler();
五、解决方案
1、静态内部类 + 弱引用
强引用就是对象被强引用后,无论如何都不会被回收。 弱引用就是在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。 软引用就是在系统将发生内存溢出的时候,回进行回收。 虚引用是对象完全不会对其生存时间构成影响,也无法通过虚引用来获取对象实例,用的比较少。
private static class MyHandler extends Handler {
//弱引用,在垃圾回收时,activity可被回收
private WeakReference<MainActivity> mWeakReference;
public MyHandler(MainActivity activity) {
this.mWeakReference = new WeakReference<>(activity);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
}
- 内部类写成静态类或者外部类
跟上面Hanlder情况一样,有时候内部类被不正当使用,容易发生内存泄漏,解决办法就是写成外部类或者静态内部类。
2、如果将Handler声明成可能导致内存泄漏的情况,在Activity销毁时,可清空Handler中未执行或正在执行的Callback以及Message:
- 在短周期结束的时候将可能发生内存泄漏的地方移除
比如Handler延迟消息,资源没关闭,集合没清理等等引起的内存泄漏,只要在Activity关闭的时候进行消除即可:
@Override
protected void onDestroy() {
super.onDestroy();
//清空handler管道和队列
mHandler.removeCallbacksAndMessages(null);
}
3、非静态内部类 + 弱引用,在activity要回收时清除引用(麻烦,不推荐)
private class MyHandler extends Handler {
//弱引用,在垃圾回收时,activity可被回收
private WeakReference<MainActivity> mWeakReference;
public MyHandler() {
this.mWeakReference = new WeakReference<>(MainActivity.this);
}
@Override
public void handleMessage(@NonNull Message msg) {
super.handleMessage(msg);
}
}
private MyHandler mHandler = new MyHandler();
@Override
protected void onDestroy() {
super.onDestroy();
//手动清除应用
mHandler.mWeakReference.clear();
}
六、静态内部类和内部类的区别
1、静态内部类(static修饰的内部类)没有对外部类的引用,所以静态内部类只能访问外部类的静态属性或方法。并且在初始化的时候可以单独存在,例如:
StaticClass staticClass = new StaticClass();
或者:
Users.StaticClass staticClass2 = new Users.StaticClass()
二种方式初始化,建议使用第二种初始化方法,比较清晰。
2、普通内部类有对外部类的引用,所以普通内部类不能独立存在,初始化的时候必须通过外部类的实例。
普通内部类的实例化如下:
Users.CommonClass commonClass = new Users().new CommonClass();
参考1:https://blog.csdn.net/cpcpcp123/article/details/122000274
参考2:https://blog.csdn.net/sqf251877543/article/details/123862179
网友评论