千家信息网

如何理解Handler内存泄露

发表于:2024-11-12 作者:千家信息网编辑
千家信息网最后更新 2024年11月12日,本篇内容介绍了"如何理解Handler内存泄露"的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!这是错误
千家信息网最后更新 2024年11月12日如何理解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):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内存泄露"的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注网站,小编将为大家输出更多高质量的实用文章!

内存 对象 线程 情况 运行 就是 文件 问题 周期 静态 分析 按钮 时候 消息 实例 延迟 代码 原因 生命 路径 数据库的安全要保护哪些东西 数据库安全各自的含义是什么 生产安全数据库录入 数据库的安全性及管理 数据库安全策略包含哪些 海淀数据库安全审计系统 建立农村房屋安全信息数据库 易用的数据库客户端支持安全管理 连接数据库失败ssl安全错误 数据库的锁怎样保障安全 数据库教材信息课程设计 变速箱软件开发工程师 房产销售网络技术 数据库表四种链接方式 dell 服务器安装引导 浙江党员党性体检软件开发 罕王集团有软件开发 乐清企事业erp软件开发企业 南山坪中学网络安全课 软件开发小组组织机构 联想服务器不知道ip地址 网络安全总监招聘 广州天象网络技术有限公司程序员 电子计算机网络技术学院 制作一个连接服务器的软件 信息技术与软件开发大学 法师变形术别的服务器能用吗 密云区信息化软件开发概况 朗新科技互联网电视 开县网络安全工程师 计算机网络技术转物流管理 江西网络安全知识有奖竞赛 网络技术支持岗位简介 怎么样快速删除多行数据库 服务器被人打多久能恢复 英雄联盟服务器名称排行 手机服务器设置密码 服务器运维工作的空闲时间多吗 网络安全赛制游戏 数字计算机网络技术考研方向
0