小题大做 | 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
,就能看到以下图片了,一个正在运行的内存情况实时图:
可以看到图片中有两个按钮我标出来了:
捕获堆转储文件按钮
,也就是生成hprof文件,这个文件会展示Java堆的使用情况,点击这个按钮后,AndroidStudio会帮我们生成这个堆转储文件并且进行分析。GC按钮
,一般我们在我们捕获堆转储文件之前,点一下GC,就能把一些弱引用给回收,防止给我们分析带来干扰。
所以我们打开HandlerActivity
后,马上finish
,然后点击GC按钮,再点击捕获堆转储文件按钮。AndroidStudio
会自动跳转到以下界面:
可以看到左上角有一个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")
}
}
}
同样运行后看看内存泄漏情况:
可以发现,这里的内存泄漏主要的原因是因为这个运行中的子线程
,由于子线程这个匿名内部类
持有了外部类的引用,而子线程本身是一直在运行的,刚才说过运行中的线程是不会被回收的,所以这里内存泄漏的引用链
应该是:
运行中的子线程 —> 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
的引用(不然也无法调用Activity
的Handler
),所以就导致内存泄漏了,但是这个情况的主要原因还是在于子线程本身。
所以综合两种情况,在发生内存泄漏的情况中,Handler
都不能算是罪魁祸首,罪魁祸首(根本原因)都是他们的头头——线程。
参考
拜拜
有一起学习的小伙伴可以关注下️ 我的公众号——码上积木,每天剖析一个知识点,我们一起积累知识。公众号回复111可获得面试题《思考与解答》以往期刊。
小题大做 | Handler内存泄露全面分析的更多相关文章
- (转)专项:Android 内存泄露实践分析
今天看到一篇关于Android 内存泄露实践分析的文章,感觉不错,讲的还算详细,mark到这里. 原文发表于:Testerhome: 作者:ycwdaaaa ; 原文链接:https://teste ...
- 五、jdk工具之jmap(java memory map)、 mat之四--结合mat对内存泄露的分析、jhat之二--结合jmap生成的dump结果在浏览器上展示
目录 一.jdk工具之jps(JVM Process Status Tools)命令使用 二.jdk命令之javah命令(C Header and Stub File Generator) 三.jdk ...
- Android中使用Handler造成内存泄露的分析和解决
什么是内存泄露?Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用所指向 ...
- Android使用Handler造成内存泄露的分析及解决方法
一.什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引用 ...
- Android handler 内存泄露分析及解决方法
1. 什么是内存泄露? Java使用有向图机制,通过GC自动检查内存中的对象(什么时候检查由虚拟机决定),如果GC发现一个或一组对象为不可到达状态,则将该对象从内存中回收.也就是说,一个对象不被任何引 ...
- 关于Android 的内存泄露及分析
一. Android的内存机制Android的程序由Java语言编写,所以Android的内存管理与Java的内存管理相似.程序员通过new为对象分配内存,所有对象在java堆内分配空间:然而对象的释 ...
- 关于Android 的内存泄露及分析(转)
一. Android的内存机制Android的程序由Java语言编写,所以Android的内存管理与Java的内存管理相似.程序员通过new为对象分配内存,所有对象在java堆内分配空间:然而对象的释 ...
- 查看w3wp进程占用的内存及.NET内存泄露,死锁分析
一 基础知识 在分析之前,先上一张图: 从上面可以看到,这个w3wp进程占用了376M内存,启动了54个线程. 在使用windbg查看之前,看到的进程含有 *32 字样,意思是在64位机器上已32位方 ...
- handler内存泄露
原因: 在Activity中新建一个Handler后,Handler执行计时操作,如果Activity销毁,Handler是不会主动销毁的,而且会占用Activity的空间,不使其回收,积累久了就会内 ...
随机推荐
- 盘点腾讯Linux、 C++后台开发面试题,做好充足准备,不怕被Pass
一.C/C++ const 多态 什么类不能被继承 二.网络 网络的字节序 网络知识 TCP三次握手 各种细节 timewait状态 TCP与UDP的区别 概念 适用范围 TCP四次挥 ...
- jdk1.8hashmap常见的面试问题
1.HashMap原理,内部数据结构? 底层使用哈希表(数组加链表)来存储,链表过长会将链表转成红黑树,以实现在O(logn)时间复杂度内查找 2.讲一下HashMap中的put方法过程? 对key求 ...
- java42
1.Random类 随机生成某个整数 Random r = new Random(); System.out.println(r.nextInt()); 伪随机数:第一次打印为随机,再次运行,数字将保 ...
- HTML 和CSS
1 HTML 介绍1.1 web 服务本质import socket sk = socket.socket() sk.bind(("127.0.0.1", 8080))sk.lis ...
- Springboot核心注解
运行文中的代码需要在项目构建中引入springboot 相关依赖. ① @configuration configuration,用来将bean加入到ioc容器.代替传统xml中的bean配置.代码示 ...
- sql server full join拼接表数据,按组加序号
--查询所有数据select A.*,B.* from(select z.id,z.requestId,z.FBillNo,dt5.FCauseAndProcess,dt5.FEquipmentNo, ...
- 初学者值得拥有【Hadoop伪分布式模式安装部署】
目录 1.了解单机模式与伪分布模式有何区别 2.安装好单机模式的Hadoop 3.修改Hadoop配置文件---五个核心配置文件 (1)hadoop-env.sh 1.到hadoop目录中 2.修 ...
- moviepy音视频剪辑:TextClip.list(font)和search搜索字体报错UnicodeDecodeError:utf-8 codec cannott decode byte 问题
☞ ░ 前往老猿Python博文目录 ░ 在moviepy2.0.0.Dev版本中,执行如下语句: from moviepy.editor import * TextClip.search('gb', ...
- 第9.4节 Python中用readline读取二进制文件方式打开文件
在<第9.3节 Python的文件内容读取:readline>中介绍了使用readline读取文件的处理,readline除了使用文本文件方式打开文件读外,也可以读取二进制方式打开的文件, ...
- Linux用户配置文件
一,用户信息文件 /etc/passwd 1,用户管理简介 1,越是对服务器安全性要求高的服务器,越需要建立合理的用户权限等级制度和服务器操作规范 2,在Linux中主要是通过用户配置文件来查看和修改 ...