Android 内存泄露:详解 Handler 内存泄露的原因与解决方案

前言

  • 在Android开发中,内存泄露 十分常见

1.内存泄露的定义:本该被回收的对象不能被回收而停留在堆内存中
2.内存泄露出现的原因:当一个对象已经不再被使用时,本该被回收但却因为有另外一个正在使用的对象持有它的引用从而导致它不能被回收。这就导致了内存泄漏。

  • 本文将详细讲解内存泄露的其中一种情况:在Handler中发生的内存泄露

目录

1.png

1. 问题描述

Handler的一般用法 = 新建Handler子类(内部类) 、匿名Handler内部类

1 /** 2 * 方式1:新建Handler子类(内部类) 3 */ 4 public class MainActivity extends AppCompatActivity { 5 6 public static final String TAG = "carson:"; 7 private Handler showhandler; 8 9 // 主线程创建时便自动创建Looper & 对应的MessageQueue 10 // 之后执行Loop()进入消息循环 11 @Override 12 protected void onCreate(Bundle savedInstanceState) { 13 super.onCreate(savedInstanceState); 14 setContentView(R.layout.activity_main); 15 16 //1. 实例化自定义的Handler类对象->>分析1 17 //注:此处并无指定Looper,故自动绑定当前线程(主线程)的Looper、MessageQueue 18 showhandler = new FHandler(); 19 20 // 2. 启动子线程1 21 new Thread() { 22 @Override 23 public void run() { 24 try { 25 Thread.sleep(1000); 26 } catch (InterruptedException e) { 27 e.printStackTrace(); 28 } 29 // a. 定义要发送的消息 30 Message msg = Message.obtain(); 31 msg.what = 1;// 消息标识 32 msg.obj = "AA";// 消息存放 33 // b. 传入主线程的Handler & 向其MessageQueue发送消息 34 showhandler.sendMessage(msg); 35 } 36 }.start(); 37 38 // 3. 启动子线程2 39 new Thread() { 40 @Override 41 public void run() { 42 try { 43 Thread.sleep(5000); 44 } catch (InterruptedException e) { 45 e.printStackTrace(); 46 } 47 // a. 定义要发送的消息 48 Message msg = Message.obtain(); 49 msg.what = 2;// 消息标识 50 msg.obj = "BB";// 消息存放 51 // b. 传入主线程的Handler & 向其MessageQueue发送消息 52 showhandler.sendMessage(msg); 53 } 54 }.start(); 55 56 } 57 58 // 分析1:自定义Handler子类 59 class FHandler extends Handler { 60 61 // 通过复写handlerMessage() 从而确定更新UI的操作 62 @Override 63 public void handleMessage(Message msg) { 64 switch (msg.what) { 65 case 1: 66 Log.d(TAG, "收到线程1的消息"); 67 break; 68 case 2: 69 Log.d(TAG, " 收到线程2的消息"); 70 break; 71 72 73 } 74 } 75 } 76 } 77 78 /** 79 * 方式2:匿名Handler内部类 80 */ 81 public class MainActivity extends AppCompatActivity { 82 83 public static final String TAG = "carson:"; 84 private Handler showhandler; 85 86 // 主线程创建时便自动创建Looper & 对应的MessageQueue 87 // 之后执行Loop()进入消息循环 88 @Override 89 protected void onCreate(Bundle savedInstanceState) { 90 super.onCreate(savedInstanceState); 91 setContentView(R.layout.activity_main); 92 93 //1. 通过匿名内部类实例化的Handler类对象 94 //注:此处并无指定Looper,故自动绑定当前线程(主线程)的Looper、MessageQueue 95 showhandler = new Handler(){ 96 // 通过复写handlerMessage()从而确定更新UI的操作 97 @Override 98 public void handleMessage(Message msg) { 99 switch (msg.what) { 100 case 1: 101 Log.d(TAG, "收到线程1的消息"); 102 break; 103 case 2: 104 Log.d(TAG, " 收到线程2的消息"); 105 break; 106 } 107 } 108 }; 109 110 // 2. 启动子线程1 111 new Thread() { 112 @Override 113 public void run() { 114 try { 115 Thread.sleep(1000); 116 } catch (InterruptedException e) { 117 e.printStackTrace(); 118 } 119 // a. 定义要发送的消息 120 Message msg = Message.obtain(); 121 msg.what = 1;// 消息标识 122 msg.obj = "AA";// 消息存放 123 // b. 传入主线程的Handler & 向其MessageQueue发送消息 124 showhandler.sendMessage(msg); 125 } 126 }.start(); 127 128 // 3. 启动子线程2 129 new Thread() { 130 @Override 131 public void run() { 132 try { 133 Thread.sleep(5000); 134 } catch (InterruptedException e) { 135 e.printStackTrace(); 136 } 137 // a. 定义要发送的消息 138 Message msg = Message.obtain(); 139 msg.what = 2;// 消息标识 140 msg.obj = "BB";// 消息存放 141 // b. 传入主线程的Handler & 向其MessageQueue发送消息 142 showhandler.sendMessage(msg); 143 } 144 }.start(); 145 146 } 147}
  • 测试结果

    1.png

  • 上述例子虽可运行成功,但代码会出现严重警告:

1.警告的原因 = 该Handler类由于无设置为 静态类,从而导致了内存泄露
2.最终的内存泄露发生在Handler类的外部类:MainActivity类

1.png

那么,该Handler在无设置为静态类时,为什么会造成内存泄露呢?

2. 原因讲解

2.1 储备知识

  • 主线程的Looper对象的生命周期 = 该应用程序的生命周期
  • 在Java中,非静态内部类 & 匿名内部类都默认持有 外部类的引用

2.2 泄露原因描述

从上述示例代码可知:

  • 上述的Handler实例的消息队列有2个分别来自线程1、2的消息(分别 为延迟1s、6s)
  • 在Handler消息队列 还有未处理的消息 / 正在处理消息时,消息队列中的Message持有Handler实例的引用
  • 由于Handler = 非静态内部类 / 匿名内部类(2种使用方式),故又默认持有外部类的引用(即MainActivity实例),引用关系如下图

上述的引用关系会一直保持,直到Handler消息队列中的所有消息被处理完毕

1.png

  • 在Handler消息队列 还有未处理的消息 / 正在处理消息时,此时若需销毁外部类MainActivity,但由于上述引用关系,垃圾回收器(GC)无法回收MainActivity,从而造成内存泄漏。如下图:

    1.png

2.3 总结

  • 当Handler消息队列 还有未处理的消息 / 正在处理消息时,存在引用关系: “未被处理 / 正处理的消息 -> Handler实例 -> 外部类”

  • 若出现 Handler的生命周期 > 外部类的生命周期 时(即 Handler消息队列 还有未处理的消息 / 正在处理消息 而 外部类需销毁时),将使得外部类无法被垃圾回收器(GC)回收,从而造成 内存泄露

3. 解决方案

从上面可看出,造成内存泄露的原因有2个关键条件:

  1. 存在“未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系
  2. Handler的生命周期 > 外部类的生命周期

即 Handler消息队列 还有未处理的消息 / 正在处理消息 而 外部类需销毁

解决方案的思路 = 使得上述任1条件不成立 即可。

解决方案1:静态内部类+弱引用

  • 原理
    静态内部类 不默认持有外部类的引用,从而使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 的引用关系 不复存在。
  • 具体方案
    将Handler的子类设置成 静态内部类

同时,还可加上 使用WeakReference弱引用持有Activity实例
原因:弱引用的对象拥有短暂的生命周期。在垃圾回收器线程扫描时,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存

  • 解决代码
1public class MainActivity extends AppCompatActivity { 2 3 public static final String TAG = "carson:"; 4 private Handler showhandler; 5 6 // 主线程创建时便自动创建Looper & 对应的MessageQueue 7 // 之后执行Loop()进入消息循环 8 @Override 9 protected void onCreate(Bundle savedInstanceState) { 10 super.onCreate(savedInstanceState); 11 setContentView(R.layout.activity_main); 12 13 //1. 实例化自定义的Handler类对象->>分析1 14 //注: 15 // a. 此处并无指定Looper,故自动绑定当前线程(主线程)的Looper、MessageQueue; 16 // b. 定义时需传入持有的Activity实例(弱引用) 17 showhandler = new FHandler(this); 18 19 // 2. 启动子线程1 20 new Thread() { 21 @Override 22 public void run() { 23 try { 24 Thread.sleep(1000); 25 } catch (InterruptedException e) { 26 e.printStackTrace(); 27 } 28 // a. 定义要发送的消息 29 Message msg = Message.obtain(); 30 msg.what = 1;// 消息标识 31 msg.obj = "AA";// 消息存放 32 // b. 传入主线程的Handler & 向其MessageQueue发送消息 33 showhandler.sendMessage(msg); 34 } 35 }.start(); 36 37 // 3. 启动子线程2 38 new Thread() { 39 @Override 40 public void run() { 41 try { 42 Thread.sleep(5000); 43 } catch (InterruptedException e) { 44 e.printStackTrace(); 45 } 46 // a. 定义要发送的消息 47 Message msg = Message.obtain(); 48 msg.what = 2;// 消息标识 49 msg.obj = "BB";// 消息存放 50 // b. 传入主线程的Handler & 向其MessageQueue发送消息 51 showhandler.sendMessage(msg); 52 } 53 }.start(); 54 55 } 56 57 // 分析1:自定义Handler子类 58 // 设置为:静态内部类 59 private static class FHandler extends Handler{ 60 61 // 定义 弱引用实例 62 private WeakReference<Activity> reference; 63 64 // 在构造方法中传入需持有的Activity实例 65 public FHandler(Activity activity) { 66 // 使用WeakReference弱引用持有Activity实例 67 reference = new WeakReference<Activity>(activity); } 68 69 // 通过复写handlerMessage() 从而确定更新UI的操作 70 @Override 71 public void handleMessage(Message msg) { 72 switch (msg.what) { 73 case 1: 74 Log.d(TAG, "收到线程1的消息"); 75 break; 76 case 2: 77 Log.d(TAG, " 收到线程2的消息"); 78 break; 79 80 81 } 82 } 83 } 84}

解决方案2:当外部类结束生命周期时,清空Handler内消息队列

  • 原理
    不仅使得 “未被处理 / 正处理的消息 -> Handler实例 -> 外部类” 的引用关系 不复存在,同时 使得 Handler的生命周期(即 消息存在的时期) 与 外部类的生命周期 同步
  • 具体方案
    当 外部类(此处以Activity为例) 结束生命周期时(此时系统会调用onDestroy()),清除 Handler消息队列里的所有消息(调用removeCallbacksAndMessages(null))
  • 具体代码
1@Override 2 protected void onDestroy() { 3 super.onDestroy(); 4 mHandler.removeCallbacksAndMessages(null); 5 // 外部类Activity生命周期结束时,同时清空消息队列 & 结束Handler生命周期 6 }

使用建议

为了保证Handler中消息队列中的所有消息都能被执行,此处推荐使用解决方案1解决内存泄露问题,即 静态内部类 + 弱引用的方式

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

java中的GC和内存泄漏

java中的GC1.GC是什么?为什么要有GC? GC是垃圾回收的意思。是指JVM清理不再使用的对象释放内存。垃圾回收可以有效的防止内存泄露,有效的使用可以使用的内存.2\.需要GC的内存区域垃圾回收区域:主要针对无用堆对象回

记住几种出现内存泄漏的点

Android内存优化——常见内存泄露及优化方案如果一个无用对象(不需要再使用的对象)仍然被其他对象持有引用,造成该对象无法被系统回收,以致该对象在堆中所占用的内存单元无法被释放而造成内存空间浪费,这中情况就是内存泄露。在Android开发中,一些不好的编程习惯会导致我们的开发的app存在内存泄露的情况。下面介绍一些在Android开发中常见的内存泄

KVM调整cpu和内存

一.修改kvm虚拟机的配置1、virsheditcentos7找到“memory”和“vcpu”标签,将<namecentos7</name<uuid2220a6d1a36a4fbb8523e078b3dfe795</uuid