自定义Handler内存泄漏 (图文版)

引言

最近在Github上看到一个面试Issue的讨论: 自定义 Handler 时如何有效地避免内存泄漏问题? 里面有高赞的答案,有详细的文字分析,也有盖楼灌水,但是读后感觉缺少了什么----直观,直观的显示内存是否泄漏,想着结合自己在实际项目中使用Profiler的经历,把他们混合在一起,(中年待业时间多 手动拍脸.jpg)

本篇文章你能get什么?

  • Java/Kotlin内部类、外部类
  • Android Studio Profiler使用
  • 图文并茂展示自定义内部类Handler内存泄漏的情况
  • 自定义Handler的最佳实践

Java/Kotlin内部类&外部类

1. Java内部类

Java内部类按照是否静态来分类: 静态内部类,非静态内部类。

csharp 复制代码
public class JavaOuter {
    // Class member variables and function
    private String name;
    private int age;

    public void printInfo() {
        System.out.println("name = " + name + ", age = " + age);
    }

    // Class lass static properties and method
    private static String staticName;
    private static int staticAge;

    static void printStaticInfo() {
        System.out.println("static name = " + staticName + ", static age = " + staticAge);
    }

    // Non-static inner class
    class InnerClass {
        public void testInnerClass() {
            name = "tancolo";
            age = 35;
            printInfo();
        }
    }

    // Static inner class
    static class StaticInnerClass {
        public void testInnerClass() {
            staticName = "tancolo";
            staticAge = 35;
            printStaticInfo();
        }
    }
}

Java内部类的使用测试代码:

java 复制代码
public class JavaDemo {
    public static void main(String[] args) {
        // Create an object of normal inner class
        JavaOuter.InnerClass innerClass = new JavaOuter().new InnerClass();
        innerClass.testInnerClass();

        // Create an object of static inner class
        JavaOuter.StaticInnerClass staticInnerClass = new JavaOuter.StaticInnerClass();
        staticInnerClass.testInnerClass();
    }
}

注意:

  • Java的非静态内部类的实例化需要依赖构建外部类的实例, 静态内部类不需要。
  • Java的非静态内部类可以访问外部类实例的非静态属性/方法,静态内部类无法访问。

2. Kotlin内部类

kotlin 复制代码
// Use static variables & function with global variable and function in Kotlin.
private const val staticName = ""
private const val staticAge = 0
fun printStaticName() {
    println("staticName = $staticName, staticAge = $staticAge")
}

class KotlinOuter {
    private var name: String = ""
    private var age: Int = 0

    fun printName() {
        println("name = $name, age = $age")
    }

    // Inner class (non-static inner class)
    inner class RealInnerClass {
        fun testInner() {
            name = "tancolo"
            age = 35
            printName()
        }
    }

    // Nest inner class, the same with static inner class of Java
    class NestClass {
        fun testInner() {
            printStaticName()
        }
    }
}

注意:

  • Kotlin没有staic关键字,没有静态内部类这个说法, 取代的是 内部类 + 嵌套类,对应Java中的非静态内部类 + 静态内部类;
  • Kotlin默认写的是嵌套类(贴心),要想声明为普通的内部类,需要加上inner关键字。
  • Kotlin的内部类、嵌套类同样需要遵守Java非静态/静态内部类的规则。(因为他们是一伙的)

Kotlin是在Java上添加了"魔法",以嵌套类为例,查看反编译后的字节码,还是Java的静态内部类那套。

Okay, 做了这么长的铺垫,是为了告知Java/Kotlin中的普通内部类的对象会持有外部类的对象引用,存在内存泄漏的风险。于是,关联上了前文的引言,Activity之类的组件中自定义内部类Handler,是如何导致内存泄漏的?铺垫不嫌多,一个接一个,Android Studio Profiler性能分析工具,来一个。

Profiler工具

不记得是从Android Studio 3.x 还是 4.x开始,Profiler取代了以前的Monitor, 集成度高、观察直观、使用方便。本文只涉及Profiler快速上手部分,目的是直观的展示自定义Handler是否有泄漏的不同情况。

Profiler快速入门图鉴

自定义Handler可能导致内存泄漏情况分析

泄漏的原因?

在正常情况中 Activity等组件 类中有个自定义Handler,Handler实例化中有Activity实例,然后Handler发送消息,handlerMessage处理消息,处理完了, 当Activity finish后,这些资源也就会被GC掉。那么怎么就泄漏了呢?

这里的泄漏是指Activity等组件 当要被GC回收时候,发现该Activity被其他的对象实例持有着,就没办法去回收掉,假如这种Activity不断的被创建,又不能被回收,内存就这么被消耗掉了。

原因是明确的:从Java/Kotlin语言层面来说内部类(非静态内部类)持有外部类对象实例这个性质,在内存泄漏方面存在隐患,需要规范编程。具体到自定义Handler这个例子。

  • 自定义内部类Handler持有外部类Activity的引用,
  • Message持有该Handler的引用, MessageQueue持有Message, Looper持有MessageQueue.
  • 在某些case下,比如消息延迟执行或者Activity中的其他耗时线程要跟Handler协作,会导致当前Activity即使finish掉,走了onDestroy()方法,然后遇到GC,还是不能被回收,因为这个持有链存在,该Activity是被引用的。

如何复现?采用Kotlin

知道原因后就好复现了,采用Handler发送延时消息的方式来复现,这里涉及到几个测试因素:

  1. Inner class (内部类)
  2. Nest class (嵌套类)
  3. WeakReference
  4. RemoveCallbackAndMessage (后文用 remove替代)

根据上述测试因素,我将他们分为8种情况,目的是测试出最佳的工程实践 , 代码在我Github仓库中有详细的说明 => 传送门

  1. 内部类 ===> 会泄漏
  2. 内部类 + WeakReference ===> 会泄漏
  3. 内部类 + remove ===> 没有泄漏
  4. 内部类 + WeakReference + remove ===> 没有泄漏
  5. 嵌套类 ===> 会泄漏
  6. 嵌套类 + WeakReference ===> 没有泄漏
  7. 嵌套类 + remove ===> 没有泄漏
  8. 嵌套类 + WeakReference + remove ===> 没有泄漏

内部类(Only)

自定义Handler

kotlin 复制代码
inner class InnerClassHandlerCase1(activity: AppCompatActivity) :
        Handler(Looper.getMainLooper()) {
        private val mActivity: AppCompatActivity

        init {
            mActivity = activity
        }

        override fun handleMessage(msg: Message) {
            println("Inner class case 1 ===> msg arg is ${msg.arg1}")
            Toast.makeText(
                this@TestMemoryLeakHandlerAndActivity,
                "Receive the delay ${msg.arg1} seconds message",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

Handler发送延时消息

kotlin 复制代码
private fun testInnerClassHandlerCase1() {
        mInnerClassHandlerCase1 = InnerClassHandlerCase1(this)
        findViewById<Button>(R.id.btn_send_message).setOnClickListener {
            // Send message
            Message.obtain().let {
                it.arg1 = MSG_DELAY_TIME
                mInnerClassHandlerCase1.sendMessageDelayed(it, MSG_DELAY_TIME.toLong()) // 60秒延时
                Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

测试结果

如上描述,单独使用内部类,在我们设置的测试条件中会导致Activity泄漏。Note:这里有个小坑,mActivity: AppCompatActivity 是多余的,为了8个测试整体好看,在内部类构造时把Activity实例也传入,内部类本身是可以通过this@xxx访问外部类属性以及方法的。后面不再赘述

内部类 + WeakReference

自定义Handler

kotlin 复制代码
inner class InnerClassHandlerCase2(activity: AppCompatActivity) :
        Handler(Looper.getMainLooper()) {
        private val mWeakReferenceActivity: WeakReference<AppCompatActivity>

        init {
            mWeakReferenceActivity = WeakReference(activity)
        }

        override fun handleMessage(msg: Message) {
            println("Inner class case 2 ===> msg arg is ${msg.arg1}")
            Toast.makeText(
                this@TestMemoryLeakHandlerAndActivity,
                "Receive the delay ${msg.arg1} seconds message",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

Handler发送延时消息

kotlin 复制代码
private fun testInnerClassHandlerCase2() {
        mInnerClassHandlerCase2 = InnerClassHandlerCase2(this)
        findViewById<Button>(R.id.btn_send_message).setOnClickListener {
            // Send message
            Message.obtain().let {
                it.arg1 = MSG_DELAY_TIME
                mInnerClassHandlerCase2.sendMessageDelayed(it, MSG_DELAY_TIME.toLong()) // 60秒延时
                Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

测试结果

不是说加入了弱引用就不会泄漏, 这里的根本因素是内部类Handler持有外部类Activity实例,跟弱引用mWeakReferenceActivity: WeakReference<AppCompatActivity>没有关系,在设置的测试条件中还是会导致Activity泄漏。

内部类 + remove

自定义Handler

kotlin 复制代码
inner class InnerClassHandlerCase3(activity: AppCompatActivity) :
        Handler(Looper.getMainLooper()) {
        private val mActivity: AppCompatActivity = activity

        override fun handleMessage(msg: Message) {
            println("Inner class case 3 ===> msg arg is ${msg.arg1}")
            Toast.makeText(
                mActivity,
                "Receive the delay ${msg.arg1} seconds message",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

Handler发送延时消息

kotlin 复制代码
private fun testInnerClassHandlerCase3() {
        mInnerClassHandlerCase3 = InnerClassHandlerCase3(this)
        findViewById<Button>(R.id.btn_send_message).setOnClickListener {
            // Send message
            Message.obtain().let {
                it.arg1 = MSG_DELAY_TIME
                mInnerClassHandlerCase3.sendMessageDelayed(it, MSG_DELAY_TIME.toLong()) // 60秒延迟
                Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

测试结果

没有泄漏!这个case没有泄漏的原因是当Activity onDestroy时候把Handler对应的消息以及回调都remove了。这么一来,Activity的持有链不存在,就可以被GC回收。

内部类 + WeakReference + remove

自定义Handler

kotlin 复制代码
inner class InnerClassHandlerCase4(activity: AppCompatActivity) :
        Handler(Looper.getMainLooper()) {
        private val mWeakReferenceActivity = WeakReference(activity)

        override fun handleMessage(msg: Message) {
            println("Inner class case 4 ===> msg arg is ${msg.arg1}")
            Toast.makeText(
                mWeakReferenceActivity.get(),
                "Receive the delay ${msg.arg1} seconds message",
                Toast.LENGTH_SHORT
            ).show()
        }
    }
	

Handler发送延时消息

kotlin 复制代码
private fun testInnerClassHandlerCase4() {
        mInnerClassHandlerCase4 = InnerClassHandlerCase4(this)
        findViewById<Button>(R.id.btn_send_message).setOnClickListener {
            // Send message
            Message.obtain().let {
                it.arg1 = MSG_DELAY_TIME
                mInnerClassHandlerCase4.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
                Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

测试结果

没有泄漏!这种 内部类 + WeakReference + remove 是没有必要的, 起作用的还是因为remove了Handler中的消息,把Activity的持有链打破。

嵌套类(Only)

自定义Handler

kotlin 复制代码
class NestClassHandlerCase1(activity: AppCompatActivity) : Handler(Looper.getMainLooper()) {
        private val mActivity: AppCompatActivity

        init {
            mActivity = activity
        }

        override fun handleMessage(msg: Message) {
            println("Nest class case 1 ===> msg arg is ${msg.arg1}")
            Toast.makeText(
                mActivity,
                "Receive the delay ${msg.arg1} seconds message",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

Handler发送延时消息

kotlin 复制代码
private fun testNestClassHandlerCase1() {
        mNestClassHandlerCase1 = NestClassHandlerCase1(this)
        findViewById<Button>(R.id.btn_send_message).setOnClickListener {
            // Send message
            Message.obtain().let {
                it.arg1 = MSG_DELAY_TIME
                mNestClassHandlerCase1.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
                Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

测试结果

会泄漏!嵌套类是无法访问外部类对象内的方法的,需要传入Activity(Context)到嵌套类中,这在实践开发中普遍。这样一来,外部类Activity的强引用的持有链一直存在,即使Activity finish了,还是无法被GC回收。

嵌套类 + WeakReference

自定义Handler

kotlin 复制代码
class NestClassHandlerCase2(activity: AppCompatActivity) : Handler(Looper.getMainLooper()) {       
        private val mWeakReferenceActivity = WeakReference(activity)
        private val mWeakReferenceApplicationContext = WeakReference(activity.applicationContext)

        override fun handleMessage(msg: Message) {
            println("Nest class case 2 ===> msg arg is ${msg.arg1}")          
            Toast.makeText(
                //mWeakReferenceActivity.get(), // ERROR
                mWeakReferenceApplicationContext.get(), // CORRECT
                "Receive the delay ${msg.arg1} seconds message",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

Handler发送延时消息

kotlin 复制代码
private fun testNestClassHandlerCase2() {
        mNestClassHandlerCase2 = NestClassHandlerCase2(this)
        findViewById<Button>(R.id.btn_send_message).setOnClickListener {
            // Send message
            Message.obtain().let {
                it.arg1 = MSG_DELAY_TIME
                mNestClassHandlerCase2.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
                Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

测试结果

没有泄漏,从之前的分析上看,MessageQueue持有Message,Message持有Handler,Handler通过传入的Activity对象持有Activity引用,按说也是会泄漏的。其实不然,嵌套内实例本身是不依赖外部类Activity对象引用的,即使MessageQueue会持有传入的Activity的引用,因为弱引用的存在,该Activty的引用可以被GC回收。

嵌套类 + remove

自定义Handler

kotlin 复制代码
class NestClassHandlerCase3(activity: AppCompatActivity) : Handler(Looper.getMainLooper()) {
        private val mActivity = activity

        override fun handleMessage(msg: Message) {
            println("Nest class case 3 ===> msg arg is ${msg.arg1}")
            Toast.makeText(
                mActivity,
                "Receive the delay ${msg.arg1} seconds message",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

Handler发送延时消息

kotlin 复制代码
private fun testNestClassHandlerCase3() {
        mNestClassHandlerCase3 = NestClassHandlerCase3(this)
        findViewById<Button>(R.id.btn_send_message).setOnClickListener {
            // Send message
            Message.obtain().let {
                it.arg1 = MSG_DELAY_TIME
                mNestClassHandlerCase3.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
                Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

测试结果

没有泄漏!原因也简单,没有WeakReference,但是Activity的持有链被打破了。

嵌套类 + WeakReference + remove

自定义Handler

kotlin 复制代码
class NestClassHandlerCase4(activity: AppCompatActivity) : Handler(Looper.getMainLooper()) {
        private val mWeakReferenceActivity = WeakReference(activity)

        override fun handleMessage(msg: Message) {
            println("Nest class case 4 ===> msg arg is ${msg.arg1}")
            Toast.makeText(
                mWeakReferenceActivity.get(),
                "Receive the delay ${msg.arg1} seconds message",
                Toast.LENGTH_SHORT
            ).show()
        }
    }

Handler发送延时消息

kotlin 复制代码
private fun testNestClassHandlerCase4() {
        mNestClassHandlerCase4 = NestClassHandlerCase4(this)
        findViewById<Button>(R.id.btn_send_message).setOnClickListener {
            // Send message
            Message.obtain().let {
                it.arg1 = MSG_DELAY_TIME
                mNestClassHandlerCase4.sendMessageDelayed(it, MSG_DELAY_TIME.toLong())
                Toast.makeText(this, "Send delay ${it.arg1} seconds message", Toast.LENGTH_SHORT)
                    .show()
                finish()
            }
        }
    }

测试结果

没有泄漏,这个是双重buffer(弱引用 + remove消息回调)叠加啊。

总结 自定义Handler最佳实践

关于最佳实践,在引言中提到的issue讨论中,就有写道,本文"洋洋洒洒"的图文只是他们讨论的总结,基于Kotlin开发。

  1. 自定义的嵌套Handler
  2. 加入WeakReference 把传入的Context作为若引用
  3. 当Activity被销毁的时候如果还有消息没有发出去,在onDestroy() removeCallbacksAndMessages(null)清除Message和Runnable

不过对于3. 之前在实际项目中,并没有去remove, 一是,我需要去handler特定Message,即使Activity消失了。二是,这些handler message以及Runnable没有耗时操作,结束就结束了,资源也就会被释放掉。

Android Studio智能提醒

在Android Studio Hedgehog | 2023.1.1 Patch 2这个版本中,发现有很贴心的提醒,之前的版本估计也有。当用Kotlin写内部类时候,IDE会给出提醒,提示会有泄漏,并给出了解决方法,类似上面的 1.2.

求点赞、关注、Star

老实说,写技术博客是件耗时耗力的累活,既要保证写的正确,有逻辑条理,还得是图文并茂。本来觉得1天工夫足矣, 奈何断断续续耗费了几天。本文中所有的代码都会在 github.com/tancolo/wid... 这个仓库中。该仓库我会分门别类的去整理各种widget 不限于自定义view啦,纯Java/Kotlin代码片段啦。感谢掘金社区提供的平台,感谢亲们的阅读,我的首发文章终于完成了。

相关推荐
心软小念32 分钟前
外包干了27天,技术退步明显。。。。。
软件测试·面试
小k_不小6 小时前
C++面试八股文:指针与引用的区别
c++·面试
上海运维Q先生10 小时前
面试题整理13----deployment和statefulset区别
运维·面试·kubernetes
醒了就刷牙17 小时前
黑马Java面试教程_P9_MySQL
java·mysql·面试
黑客老陈20 小时前
面试经验分享 | 北京渗透测试岗位
运维·服务器·经验分享·安全·web安全·面试·职场和发展
测试老哥1 天前
外包干了两年,技术退步明显。。。。
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
ThisIsClark1 天前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
测试19981 天前
外包干了2年,技术退步明显....
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
Aphasia3111 天前
一次搞懂 JS 对象转换,从此告别类型错误!
javascript·面试