一次弄清楚 Handler 可能导致的内存泄漏和解决办法

作者:小虾米君
转载地址:https://mp.weixin.qq.com/s/kzExhgdm4fIEk882w8cEkQ

去年发过这个话题的文章,但遗漏了几个重要的章节,而且其中子线程 Looper 的结论说得不太清楚。本次重制和补充了多个示意图和章节,期望一次性讲清楚!

  • 1. Handler 使用不当?
  • 2. 为什么会内存泄露?
  • 3. 子线程 Looper 会导致内存泄露吗?
  • 4. 非内部类的 Handler 会内存泄露吗?
  • 5. 网传的 Callback 接口真得能解决吗?
  • 6. 正确使用 Handler?
  • 结语

1. Handler 使用不当?

先搞清楚什么叫 Handler 使用不当?

一般具备这么几个特征:

  1. Handler 采用匿名内部类内部类扩展,默认持有外部类 Activity 的引用:
// 匿名内部类
override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val innerHandler: Handler = object : Handler(Looper.getMainLooper()) {
        override fun handleMessage(msg: Message) {
            Log.d(
                "MainActivity","Anonymous inner handler message occurred & what:${msg.what}"
            )
        }
    }
}

// 内部类
override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val innerHandler: Handler = MyHandler(Looper.getMainLooper())
}

inner class MyHandler(looper: Looper): Handler(looper) {
    override fun handleMessage(msg: Message) {
        Log.d(
            "MainActivity","Inner handler message occurred & what:\${msg.what}"
        )
    }
}
  1. Activity 退出的时候 Handler 仍可达,有两种情况:
  • 退出的时候仍有 Thread 在处理中,其引用着 Handler
  • 退出的时候虽然 Thread 结束了,但 Message 尚在队列中排队处理正在处理中,间接持有 Handler
override fun onCreate(savedInstanceState: Bundle?) {
    ...
    val elseThread: Thread = object : Thread() {
        override fun run() {
            Log.d(
                "MainActivity","Thread run"
            )
            
            sleep(2000L)
            innerHandler.sendEmptyMessage(1)
        }
    }.apply { start() }
}

2. 为什么会内存泄露?

上述的 Thread 在执行的过程中,如果 Activity 进入了后台,后续因为内存不足触发了 destroy。虚拟机在标记 GC 对象的时候,会发生如下两种情形:

  • Thread 尚未结束,处于活跃状态

活跃的 Thread 作为 GC Root 对象,其持有 Handler 实例,Handler 又默认持有外部类 Activity 的实例,这层引用链仍可达:

  • Thread 虽然已结束,但发送的 Message 还未处理完毕

Thread 发送的 Message 可能还在队列中等待,又或者正好处于 handleMessage() 的回调当中。此刻 Looper 通过 MessagQueue 持有该 Message,Handler 又作为 target 属性被 Message 持有,Handler 又持有 Activity,最终导致 Looper 间接持有 Activity。

大家可能没有注意到主线程的 Main Looper 是不同于其他线程的 Looper 的。

为了能够让任意线程方便取得主线程的 Looper 实例,Looper 将其定义为了静态属性 sMainLooper

public final class Looper {
    private static Looper sMainLooper;  // guarded by Looper.class
    ...
    public static void prepareMainLooper() {
        prepare(false);
        synchronized (Looper.class) {
            sMainLooper = myLooper();
        }
    }
}

静态属性也是 GC Root 对象,其通过上述的应用链导致 Activity 仍然可达。

这两种情形都将导致 Activity 实例将无法被正确地标记,直到 Thread 结束 且 Message 被处理完毕。在此之前 Activity 实例将得不到回收。

内部类 Thread 也会导致 Activity 无法回收吧?

为了侧重阐述 Handler 导致的内存泄漏,并没有针对 Thread 直接产生的引用链作说明。

上面的代码示例中 Thread 也采用了匿名内部类形式,其当然也持有 Activity 实例。从这点上来说,尚未结束的 Thread 会直接占据 Acitvity 实例,这也是导致 Activity 内存泄露的一条引用链,需要留意!

3. 子线程 Looper 会导致内存泄露吗?

为了便于每个线程方便拿到独有的 Looper 实例,Looper 类采用静态的 sThreadLocal 属性管理着各实例。

public final class Looper {
    static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
    ...
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }
}

可是作为静态属性的话,sThreadLocal 也是 GC Root 对象。那从这个角度讲会不会也间接导致 Message 无法回收呢?

会,但本质上不是因为 ThreadLocal,而是因为 Thread。

翻看 ThreadLocal 的源码,你会发现:目标对象并不存放在 ThreadLocal 中,而是其静态内部类 ThreadLocalMap 中。加上为了线程独有,该 Map 又被 Thread 持有,两者的生命周期等同

// TheadLocal.java
public class ThreadLocal<T> {
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this,value);
        else
            createMap(t,value);
    }

    // 创建 Map 并放到了 Thread 中
    void createMap(Thread t,T firstValue) {
        t.threadLocals = new ThreadLocalMap(this,firstValue);
    }
    
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
}

// Thread.java
public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

更进一步的细节是:ThreadLocalMap 中的元素 Entry 采用弱引用持有作为 key 的 ThreadLocal 对象,但作为 value 的目标对象则被强引用着的。

这就导致 Thread 间接持有着目标对象,比如本次的 Looper 实例。这样可以确保 Looper 的生命周期和 Thread 保持一致,但 Looper 生命周期过长会有内存泄漏的风险(当然这不是 Looper 设计者的锅)。

// TheadLocal.java
public class ThreadLocal<T> {
    ...
    static class ThreadLocalMap {
        private Entry[] table;

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k,Object v) {
                super(k);
                value = v;
            }
        }
    }
}

当弱引用的 key 实例因为 GC 发生会被切断和 Map 的引用关系,但直到下一次手动执行 Entry 的 setget 或 remove 前,value 都没被置为 null。这段时间 value 都被强引用着,造成隐患。

The entries in this hash map extend WeakReference,using its main ref field as the key (which is always a ThreadLocal object).

Note that null keys (i.e. entry.get() == null) mean that the key is no longer referenced,so the entry can be expunged from table.

Such entries are referred to as “stale entries” in the code that follows.

但需要注意的是:本处 Looper 持有的实例 sThreadLocal 是静态的,进程结束之前都不会被回收,不可能发生上述提及的 key 被回收 value 被孤立的情况

简言之,Looper 的 ThreadLocal 不会导致内存泄漏。

回到 Thread,因其强引用 Looper,所以 Thread 仍然会因为这个因素导致 Message 发生内存泄漏。 比如:向子线程 Handler 发送了内部类写法的 Runnable,当 Activity 结束的时候该 Runnable 尚未抵达或正在执行过程中,那么 Activity 会因为如下的引用链一直可达,即发生内存泄漏的可能。

解决办法不复杂:

  • Activity 结束的时候移除子线程 Handler 中所有未处理 Message,比如 Handler#removeCallbacksAndMessages()。这将会切断 MessageQueue 到 Message,以及 Message 到 Runnable 的引用关系
  • 更好的办法是调用 Looper#quit() 或 quitSafely(),它将清空所有的 Message 或未来的 Message,并促使 loop() 轮询的结束。子线程的结束则意味着引用起点 GC Root 不复存在

最后,明确几点共识:

  1. 管理 Looper 实例的静态属性 sThreadLocal,并不持有实际存放 Looper 的 ThreadLocalMap,而是通过 Thread 去读写 。这使得 sThreadLocal 虽贵为  GC Root,但无法达成到 Looper 的引用链,进而从这条路径上并不能构成内存泄漏!
  2. 另外,因其是静态属性,也不会发生 key 被回收 value 被孤立的内存泄漏风险~
  3. 最后,由于 Thread 持有 Looper value,从这条路径上来说是存在内存泄漏的可能的

4. 非内部类的 Handler 会内存泄露吗?

上面说过匿名内部类或内部类是 Handler 造成内存泄漏的一个特征,那如果 Handler 不采用内部类的写法,会造成泄露吗?

比如这样:

override fun onCreate(...) {
    Handler(Looper.getMainLooper()).apply {
        object : Thread() {
            override fun run() {
                sleep(2000L)
                post { 
                    // Update ui
                }
            }
        }.apply { start() }
    }
}

仍然可能造成内存泄漏。

虽然 Handler 不是内部类,但 post 的 Runnable 也是内部类,其同样会持有 Activity 的实例。另外,post 到 Handler 的 Runnable 最终会作为 callback 属性被 Message 持有。

基于这两个表现,即便 Handler 不是内部类了,但因为 Runnable 是内部类,同样会发生 Activity 被 Thread 或 Main Looper 不当持有的风险。

5. 网传的 Callback 接口真得能解决吗?

网上有种说法:创建 Handler 时不覆写 handleMessage(),而是指定 Callback 接口实例,这样子可以避免内存泄漏。理由是这种写法之后 AndroidStudio 就不会再弹出如下的警告:

This Handler class should be static or leaks might occur.

事实上,Callback 实例如果仍然是匿名内部类或内部类的写法,仍然会造成内存泄漏,只是 AS 没弹出这层警告而已。

private Handler mHandler = new Handler(new Handler.Callback() {
    @Override  
    public boolean handleMessage(Message msg) {  
        return false;  
    }  
});  

比如上面的这种写法,Handler 会持有传递进去的 Callback 实例,而 Callback 作为内部类写法,默认持有外部类 Activity 的引用。

public class Handler {
    final Callback mCallback;

 public Handler(@NonNull Looper looper,@Nullable Callback callback) {
        this(looper,callback,false);
    }

    public Handler(@NonNull Looper looper,@Nullable Callback callback,boolean async) {
        ...
        mCallback = callback;
    }
}

无论是从 Thread 活跃的角度,还是从 Thread 结束但 Message 仍然未执行完的角度来说,都将导致 Activity 仍然可被 GC Root 间接引用而发生内存泄漏的风险。

本质来说和上面的 Runnable 例子是一样的问题:

6. 正确使用 Handler?

GC 标记的时候 Thread 已结束并且 Message 已被处理的条件一旦没有满足,Activity 的生命周期就将被错误地延长,继而引发内存泄露!

那如何避免这种情况的发生呢?针对上面的特征,其实应该已经有了答案:

  • 一则将强引用 Activity 改为弱引用
  • 二则及时地切断两大 GC Root 的引用链关系:Main Looper 到 Message,以及结束子线程

代码示例简述如下:

  1. 将 Handler 或 Callback 或 Runnable 定义为静态内部类
class MainActivity : AppCompatActivity() {
    private class MainHandler(looper: Looper?,referencedObject: MainActivity?) :
            WeakReferenceHandler<MainActivity?>(looper,referencedObject) {
        override fun handleMessage(msg: Message) {
            val activity: MainActivity? = referencedObject
            if (activity != null) {
                // ...
            }
        }
    }
}
  1. 还需要弱引用外部类的实例:
open class WeakReferenceHandler<T>(looper: Looper?,referencedObject: T) :     Handler(looper!!) {
    private val mReference: WeakReference<T> = WeakReference(referencedObject)

    protected val referencedObject: T?
        protected get() = mReference.get()
}
  1. onDestroy 的时候切断引用链关系,纠正生命周期:
  • Activity 销毁的时候,如果子线程任务尚未结束,及时中断 Thread:
override fun onDestroy() {
    ...
    thread.interrupt()
}

如果子线程中创建了 Looper 并成为了 Looper 线程的话,须手动 quit。比如 HandlerThread

override fun onDestroy() {
    ...
    handlerThread.quitSafely()
}
  • 主线程的 Looper 无法手动 quit,所以还需手动清空主线程中 Handler 未处理的 Message
override fun onDestroy() {
    ...
    mainHandler.removeCallbacksAndMessages(null)
}

※1:Message 在执行 recycle() 后会清除其与和 Main Handler 的引用关系
※2:Looper 子线程调用 quit 时会清空 Message,所以无需针对子线程的 Handler 再作 Message 的清空处理了

结语

回顾一下本文的几个要点:

  • 持有 Activity 实例的 Handler 处理,其生命周期应当和 Activity 保持一致
  • 如果 Activity 本该销毁了,但异步 Thread 仍然活跃或发送的 Message 尚未处理完毕,将导致 Activity 实例的生命周期被错误地延长
  • 造成本该回收的 Activity 实例被子线程或  Main Looper  占据而无法及时回收

简单来讲的正确做法:

  1. 使用 Handler 机制的时候,无论是覆写 Handler 的 handleMessage() 方式,还是指定回调的 Callback 方式,以及发送任务的 Runnable 方式,尽量采用静态内部类 + 弱引用,避免其强引用持有 Activity 的实例。

确保即便错误地延长了生命周期,Activity 也能及时被 GC 回收

  1. 同时在 Activity 结束的时候,及时地清空 Message、终止 Thread 或退出 Looper,以便回收 Thread 或 Message。

确保能彻底切断 GC Root 抵达 Activity 的引用链

原文地址:https://blog.csdn.net/weixin_61845324

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


更新Android SDK到3.0版本时,遇到Failed to rename directory E:\android\tools to E:\android\temp\ToolPackage.old01问题,导致无法更新,出现该问题的原因是由于3.0版本与较早的sdk版本之间文件结构有冲突,解决
Android 如何解决dialog弹出时无法捕捉Activity的back事件 在一些情况下,我们需要捕捉back键事件,然后在捕捉到的事件里写入我们需要进行的处理,通常可以采用下面三种办法捕捉到back事件: 1)重写onKeyDown或者onKeyUp方法 2)重写onBackPressed方
Android实现自定义带文字和图片的Button 在Android开发中经常会需要用到带文字和图片的button,下面来讲解一下常用的实现办法。一.用系统自带的Button实现 最简单的一种办法就是利用系统自带的Button来实现,这种方式代码量最小。在Button的属性中有一个是drawable
Android中的&quot;Unable to start activity ComponentInfo&quot;的错误 最近在做一款音乐播放器的时候,然后在调试的过程中发现一直报这个错误&quot;Unable to start activity ComponentInfo&quot;,从字面
Android 关于长按back键退出应用程序的实现最近在做一个Android上的应用,碰到一个问题就是如何实现长按back键退出应用程序。在网上查找了很多资料,发现几乎没有这样的实现,大部分在处理时是双击back键来退出应用程序。参考了一下双击back键退出应用程序的代码,网上主流的一种方法是下面
android自带的时间选择器只能精确到分,但是对于某些应用要求选择的时间精确到秒级,此时只有自定义去实现这样的时间选择器了。下面介绍一个可以精确到秒级的时间选择器。 先上效果图: 下面是工程目录: 这个控件我也是用的别人的,好像是一个老外写的,com.wheel中的WheelView是滑动控件的主
Android平台下利用zxing实现二维码开发 现在走在大街小巷都能看到二维码,而且最近由于项目需要,所以研究了下二维码开发的东西,开源的二维码扫描库主要有zxing和zbar,zbar在iPos平台上应用比较成熟,而在Android平台上主流还是用zxing库,因此这里主要讲述如何利用zxing
Android ListView的item背景色设置以及item点击无响应等相关问题 在Android开发中,listview控件是非常常用的控件,在大多数情况下,大家都会改掉listview的item默认的外观,下面讲解以下在使用listview时最常见的几个问题。1.如何改变item的背景色和按
如何向Android模拟器中导入含有中文名称的文件在进行Android开发的时候,如果需要向Android模拟器中导入文件进行测试,通过DDMS下手动导入或者在命令行下通过adb push命令是无法导入含有中文文件名的文件的。后来发现借用其他工具可以向模拟器中导入中文名称的文件,这个工具就是Ultr
Windows 下搭建Android开发环境一.下载并安装JDK版本要求JDK1.6+,下载JDK成功后进行安装,安装好后进行环境变量的配置【我的电脑】-——&gt;【属性】——&gt;【高级】 ——&gt;【环境变量】——&gt;【系统变量】中点击【新建】:变量名:CLASSPATH变量值:……
如何利用PopupWindow实现弹出菜单并解决焦点获取以及与软键盘冲突问题 在android中有时候可能要实现一个底部弹出菜单,此时可以考虑用PopupWindow来实现。下面就来介绍一下如何使用PopupWindow实现一个弹出窗。 主Activity代码:public void onCreat
解决Android中的ERROR: the user data image is used by another emulator. aborting的方法 今天调试代码的时候,突然出现这个错误,折腾了很久没有解决。最后在google上找到了大家给出的两种解决方案,下面给出这两种方法的链接博客:ht
AdvserView.java package com.earen.viewflipper; import android.content.Context; import android.graphics.Bitmap; import android.graphics.BitmapFactory;
ImageView的scaleType的属性有好几种,分别是matrix(默认)、center、centerCrop、centerInside、fitCenter、fitEnd、fitStart、fitXY。 |值|说明| |:--:|:--| |center|保持原图的大小,显示在ImageVie
文章浏览阅读8.8k次,点赞9次,收藏20次。本文操作环境:win10/Android studio 3.21.环境配置 在SDK Tools里选择 CMAKE/LLDB/NDK点击OK 安装这些插件. 2.创建CMakeLists.txt文件 在Project 目录下,右键app,点击新建File文件,命名为CMakeLists.txt点击OK,创建完毕! 3.配置文件 在CMa..._link c++ project with gradle
文章浏览阅读1.2w次,点赞15次,收藏69次。实现目的:由mainActivity界面跳转到otherActivity界面1.写好两个layout文件,activity_main.xml和otherxml.xmlactivity_main.xml&lt;?xml version="1.0" encoding="utf-8"?&gt;&lt;RelativeLayout ="http://schemas..._android studio 界面跳转
文章浏览阅读3.8w次。前言:最近在找Android上的全局代理软件来用,然后发现了这两款神作,都是外国的软件,而且都是开源的软件,因此把源码下载了下来,给有需要研究代理这方面的童鞋看看。不得不说,国外的开源精神十分浓,大家相互使用当前基础的开源软件,然后组合成一个更大更强的大开源软件。好吧,废话不多说,下面简单介绍一下这两款开源项目。一、ProxyDroid:ProxyDroid功能比较强大,用到的技术也比较多,源码也_proxydroid
文章浏览阅读2.5w次,点赞17次,收藏6次。创建项目后,运行项目时Gradle Build 窗口却显示错误:程序包R不存在通常情况下是不会出现这个错误的。我是怎么遇到这个错误的呢?第一次创建项目,company Domain我使用的是:aven.com,但是创建过程在卡在了Building 'Calculator' Gradle Project info这个过程中,于是我选择了“Cancel”第二次创建项目,我还是使用相同的项目名称和项目路_r不存在
文章浏览阅读8.9w次,点赞4次,收藏43次。前言:在Android上使用系统自带的代理,限制灰常大,仅支持系统自带的浏览器。这样像QQ、飞信、微博等这些单独的App都不能使用系统的代理。如何让所有软件都能正常代理呢?ProxyDroid这个软件能帮你解决!使用方法及步骤如下:一、推荐从Google Play下载ProxyDroid,目前最新版本是v2.6.6。二、对ProxyDroid进行配置(基本配置:) (1) Auto S_proxydroid使用教程
文章浏览阅读1.1w次,点赞4次,收藏17次。Android Studio提供了一个很实用的工具Android设备监视器(Android device monitor),该监视器中最常用的一个工具就是DDMS(Dalvik Debug Monitor Service),是 Android 开发环境中的Dalvik虚拟机调试监控服务。可以进行的操作有:为测试设备截屏,查看特定进程中正在运行的线程以及堆栈信息、Logcat、广播状态信息、模拟电话_安卓摄像头调试工具