小题大做 | Handler内存泄露全面分析

简介: 嗨,大家好,问大家一个“简单”的问题:Handler内存泄露的原因是什么?

前言


嗨,大家好,问大家一个“简单”的问题:


Handler内存泄露的原因是什么?


你会怎么答呢?


这是错误的回答


有的朋友看到这个题表示,就这?太简单了吧。


"内部类持有了外部类的引用,也就是Hanlder持有了Activity的引用,从而导致无法被回收呗。"


其实这样回答是错误的,或者说没回答到点子上。


内存泄漏


Java虚拟机中使用可达性分析的算法来决定对象是否可以被回收。即通过GCRoot对象为起始点,向下搜索走过的路径(引用链),如果发现某个对象或者对象组为不可达状态,则将其进行回收。


内存泄漏指的就是有些对象(短周期对象)没有用了,但是却被其他有用的类(长周期对象)所引用,从而导致无用对象占据了内存空间,形成内存泄漏。


所以上面的问题,如果仅仅回答内部类持有了外部类的引用,没有指出内部类被谁所引用,那么按道理来说是不会发生内存泄漏的,因为内部类和外部类都是无用对象了,是可以被正常回收的。


所以这一题的关键在于,内部类被引用了?也就是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。看看会不会内存泄漏。


查看内存泄漏并分析


现在查看内存泄漏还是蛮方便的了,AndroidStudio自带对堆转储(Heap Dump)文件进行分析,并且会把内存泄漏点明确标出来。


我们运行项目,点击Profiler——Memory,就能看到以下图片了,一个正在运行的内存情况实时图:


3.png


可以看到图片中有两个按钮我标出来了:


  • 捕获堆转储文件按钮,也就是生成hprof文件,这个文件会展示Java堆的使用情况,点击这个按钮后,AndroidStudio会帮我们生成这个堆转储文件并且进行分析。
  • GC按钮,一般我们在我们捕获堆转储文件之前,点一下GC,就能把一些弱引用给回收,防止给我们分析带来干扰。


所以我们打开HandlerActivity后,马上finish,然后点击GC按钮,再点击捕获堆转储文件按钮。AndroidStudio会自动跳转到以下界面:


4.png


可以看到左上角有一个Leaks,这就是你内存泄漏的点,点击就能看到内存泄漏的类了。右下角就是内存泄漏类的引用路径。


从这张图可以看到,我们的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")
        }
    }
}


同样运行后看看内存泄漏情况:


5.jpg


可以发现,这里的内存泄漏主要的原因是因为这个运行中的子线程,由于子线程这个匿名内部类持有了外部类的引用,而子线程本身是一直在运行的,刚才说过运行中的线程是不会被回收的,所以这里内存泄漏的引用链应该是:


运行中的子线程 —> Activity


当然,这里的Handler也是持有了Activity的引用的,但主要引起内存泄漏的原因还是在于子线程本身,就算子线程中不用Handler,而是调用Activity的其他变量或者方法还是会发生内存泄漏。


所以这种情况我觉得不能看作Handler引起内存泄漏的情况,其根本原因是因为子线程引起的,如果解决了子线程的内存泄漏,比如在Activity销毁的时候停止子线程,那么Activity就能正常被回收,那么也不存在Handler的问题了。


延伸问题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;
     }
}


延伸问题2:kotlin中的内部类与Java有什么不一样吗


其实可以看到,在上述的代码中,我都加了一句


btn2.setText("2222")


这是因为在kotlin中的匿名内部类分为两种情况:


  • 在Kotlin中,匿名内部类如果没有使用到外部类的对象引用时候,是不会持有外部类的对象引用的,此时的匿名内部类其实就是个静态匿名内部类,也就不会发生内存泄漏。
  • 在Kotlin中,匿名内部类如果使用了对外部类的引用,像我刚才使用了btn2,这时候就会持有外部类的引用了,就会需要考虑内存泄漏的问题。


所以我特意加了这一句,让匿名内部类持有外部类的引用,复现内存泄漏问题。


同样kotlin中对于内部类也是和Java有区别的:


  • Kotlin中所有的内部类都是默认静态的,也就都是静态内部类
  • 如果需要调用外部的对象方法,就需要用inner修饰,改成和Java一样的内部类,并且会持有外部类的引用,需要考虑内存泄漏问题。


解决内存泄漏


说了这么多,那么该怎么解决内存泄漏问题呢?其实所有内存泄漏的解决办法都大同小异,主要有以下几种:


  • 不要让长生命周期对象持有短生命周期对象的引用,而是用长生命周期对象持有长生命周期对象的引用。


比如Glide使用的时候传的上下文不要用Activity而改用Application的上下文(这句有问题,并无此说法,在此修正)。还有单例模式不要传入Activity上下文。


  • 将对象的强引用改成弱引用


强引用就是对象被强引用后,无论如何都不会被回收。

弱引用就是在垃圾回收时,如果这个对象只被弱引用关联(没有任何强引用关联他),那么这个对象就会被回收。

软引用就是在系统将发生内存溢出的时候,回进行回收。

虚引用是对象完全不会对其生存时间构成影响,也无法通过虚引用来获取对象实例,用的比较少。


所以我们将对象改成弱引用,就能保证在垃圾回收时被正常回收,比如Handler中传入Activity的弱引用实例:


MyHandler(WeakReference(this)).sendEmptyMessageDelayed(0, 20000)
    //kotlin中内部类默认为静态内部类
    class MyHandler(var mActivity: WeakReference<HandlerActivity>):Handler(){
        override fun handleMessage(msg: Message?) {
            super.handleMessage(msg)
            mActivity.get()?.changeBtn()
        }
    }


  • 内部类写成静态类或者外部类


跟上面Hanlder情况一样,有时候内部类被不正当使用,容易发生内存泄漏,解决办法就是写成外部类或者静态内部类。


  • 在短周期结束的时候将可能发生内存泄漏的地方移除


比如Handler延迟消息,资源没关闭,集合没清理等等引起的内存泄漏,只要在Activity关闭的时候进行消除即可:


@Override
protected void onDestroy() {
  //移除handler所有消息
  if(mHanlder != null){
  mHandler.removeCallbacksAndMessages(null)
  }
  super.onDestroy();
}


总结


Handler内存泄露的原因是什么?


Handler导致内存泄漏一般发生在发送延迟消息的时候,当Activity关闭之后,延迟消息还没发出,那么主线程中的MessageQueue就会持有这个消息的引用,而这个消息是持有Handler的引用,而handler作为匿名内部类持有了Activity的引用,所以就有了以下的一条引用链。


主线程 —> threadlocal —> Looper —> MessageQueue —> Message —> Handler —> Activity


根本原因是因为这条引用链的头头,也就是主线程,是不会被回收的,所以导致Activity无法被回收,出现内存泄漏,其中Handler只能算是导火索。


而我们平时用到的子线程通过Handler更新UI,其原因是因为运行中的子线程不会被回收,而子线程持有了Actiivty的引用(不然也无法调用ActivityHandler),所以就导致内存泄漏了,但是这个情况的主要原因还是在于子线程本身。


所以综合两种情况,在发生内存泄漏的情况中,Handler都不能算是罪魁祸首,罪魁祸首(根本原因)都是他们的头头——线程


参考


https://www.cnblogs.com/shoshana-kong/p/10449648.html


https://www.jianshu.com/p/825cca41d962


https://www.jianshu.com/p/0ee88812d73e

目录
相关文章
|
17天前
|
Web App开发 监控 JavaScript
监控和分析 JavaScript 内存使用情况
【10月更文挑战第30天】通过使用上述的浏览器开发者工具、性能分析工具和内存泄漏检测工具,可以有效地监控和分析JavaScript内存使用情况,及时发现和解决内存泄漏、过度内存消耗等问题,从而提高JavaScript应用程序的性能和稳定性。在实际开发中,可以根据具体的需求和场景选择合适的工具和方法来进行内存监控和分析。
|
1月前
|
编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(上)
动态内存分配与管理详解(附加笔试题分析)
49 1
|
2月前
|
程序员 编译器 C++
【C++核心】C++内存分区模型分析
这篇文章详细解释了C++程序执行时内存的四个区域:代码区、全局区、栈区和堆区,以及如何在这些区域中分配和释放内存。
53 2
|
12天前
|
开发框架 监控 .NET
【Azure App Service】部署在App Service上的.NET应用内存消耗不能超过2GB的情况分析
x64 dotnet runtime is not installed on the app service by default. Since we had the app service running in x64, it was proxying the request to a 32 bit dotnet process which was throwing an OutOfMemoryException with requests >100MB. It worked on the IaaS servers because we had the x64 runtime install
|
22天前
|
Web App开发 JavaScript 前端开发
使用 Chrome 浏览器的内存分析工具来检测 JavaScript 中的内存泄漏
【10月更文挑战第25天】利用 Chrome 浏览器的内存分析工具,可以较为准确地检测 JavaScript 中的内存泄漏问题,并帮助我们找出潜在的泄漏点,以便采取相应的解决措施。
138 9
|
26天前
|
并行计算 算法 IDE
【灵码助力Cuda算法分析】分析共享内存的矩阵乘法优化
本文介绍了如何利用通义灵码在Visual Studio 2022中对基于CUDA的共享内存矩阵乘法优化代码进行深入分析。文章从整体程序结构入手,逐步深入到线程调度、矩阵分块、循环展开等关键细节,最后通过带入具体值的方式进一步解析复杂循环逻辑,展示了通义灵码在辅助理解和优化CUDA编程中的强大功能。
|
1月前
|
程序员 编译器 C语言
动态内存分配与管理详解(附加笔试题分析)(下)
动态内存分配与管理详解(附加笔试题分析)(下)
46 2
|
2月前
|
算法 程序员 Python
程序员必看!Python复杂度分析全攻略,让你的算法设计既快又省内存!
在编程领域,Python以简洁的语法和强大的库支持成为众多程序员的首选语言。然而,性能优化仍是挑战。本文将带你深入了解Python算法的复杂度分析,从时间与空间复杂度入手,分享四大最佳实践:选择合适算法、优化实现、利用Python特性减少空间消耗及定期评估调整,助你写出高效且节省内存的代码,轻松应对各种编程挑战。
41 1
|
2月前
|
存储 Prometheus NoSQL
Redis 内存突增时,如何定量分析其内存使用情况
【9月更文挑战第21天】当Redis内存突增时,可采用多种方法分析内存使用情况:1)使用`INFO memory`命令查看详细内存信息;2)借助`redis-cli --bigkeys`和RMA工具定位大键;3)利用Prometheus和Grafana监控内存变化;4)优化数据类型和存储结构;5)检查并调整内存碎片率。通过这些方法,可有效定位并解决内存问题,保障Redis稳定运行。
|
1月前
|
SQL 安全 算法
ChatGPT高效提问—prompt实践(漏洞风险分析-重构建议-识别内存泄漏)
ChatGPT高效提问—prompt实践(漏洞风险分析-重构建议-识别内存泄漏)