Handler内存泄漏

文章详细分析了Android中Handler导致内存泄漏的根本原因,即Handler、Message、Looper和ThreadLocal之间的引用关系,使得Activity无法被垃圾回收。解决方法包括将Handler改为静态内部类并使用弱引用,以及在Activity的onDestroy()中清除Handler的消息队列。此外,文章还介绍了GCRoots的概念及其在内存管理中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近看了handler相关代码,将所学handler内存泄漏相关总结一下。

1.handler内存泄漏根本原因:

sThreadLocal是GC Roots,可达Activity。故Activity不会被回收。

2.下面分析整个流程

2.1我们常使用Handler的方式是在主线程中构造一个Handler对象。如下

Handler handler = new Handler() {
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
};

java匿名内部类会持有外部类引用,故handler持有activity,即handler->activity。

这里说下java匿名内部类是怎么持有外部类引用的(知道这个知识的,可以直接看2.2)
我们简单定义一个外部类和匿名内部类

public class Out {
    private String tag = "Out";
    InnerClass innerClass = new InnerClass() {
        @Override
        void printTag() {
            System.out.printf(tag);
        }
    };
}
public class InnerClass {
    void printTag() {

    }
}

在命令行中使用javac Out.java InnerClass.java,将Out.java和InnerClass.java编译成class文件,会编译出3个class文件,

  • Out.class - 外部类Out的字节码文件
  • InnerClass.class - InnerClass类的字节码文件
  • Out$1.class - 匿名内部类的字节码文件
    Out$1.class就是匿名内部类的字节码文件,在AS中打开这个class文件,如下:
class Out$1 extends InnerClass {
    Out$1(final Out var1) {
        this.this$0 = var1;
    }

    void printTag() {
        System.out.printf(this.this$0.tag);
    }
}

可以看到内部类的构造方法中传入了外部类的对象, 并赋值给了内部类的一个成员变量this$0,所以匿名内部类会持有外部类的引用

2.2下面看Handler类的代码

private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
        long uptimeMillis) {
    msg.target = this;
    msg.workSourceUid = ThreadLocalWorkSource.getUid();

    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    return queue.enqueueMessage(msg, uptimeMillis);
}

Handler类的enqueueMessage()方法第一句:msg.target = this;message持有handler,即message->handler。
之后是MessageQueue持有Message,这就不作分析了。现有持有链:messageQueue->message->handler->activity

2.3然后看下messageQueue是被谁持有了,在Looper的构造方法中

private Looper(boolean quitAllowed) {
    mQueue = new MessageQueue(quitAllowed);
    mThread = Thread.currentThread();
}

从这里看出looper持有messageQueue。即looper->messageQueue。

2.4接着看下Looper.prepare()方法做了什么

private static void prepare(boolean quitAllowed) {
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    sThreadLocal.set(new Looper(quitAllowed));
}

这里将looper对象放进sThreadLocal。sThreadLocal持有looper。

2.5我们继续看sThreadLocal是什么。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();

sThreadLocal是一个静态变量,即GC Roots。
至此,我们完整的可达链为sThreadLocal->looper->messageQueue->message->handler->activity。

总结:由于sThreadLocal是GC Root,activity是可达的,所以activity不会被回收,就会造成内存泄漏。

3.修改handler内存泄漏

  1. 修改Handler为static类,并使用弱引用,这样才能在静态内部类中调用外部类方法。
    此种方式是打断handler->activity。因为java静态匿名内部类不会持有外部类引用。

下面代码是Kotlin中使用Handler的方式(静态内部类 + 弱引用)

class MainActivity : AppCompatActivity() {
	companion object {
    	private const val MY_MESSAGE = 1
	}
    
    private val handler by lazy { StaticHandler(this) }
    
    lateinit var binding: ActivityMainBinding
    
    /**
     * 静态内部类 + 弱引用
     */
    class StaticHandler(obj: MainActivity) : Handler(Looper.getMainLooper()) {
        private val ref = WeakReference(obj)
        override fun handleMessage(msg: Message) {
            ref.get()?.apply {
                when(msg.what) {
                    MY_MESSAGE -> Log.d("mainActivity", "handleMessage: ${getData()}")
                }
            }
        }
    }

    fun getData() : Int {
        return 2
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)
        val message = Message.obtain()
        message.what = myMessage
        handler.sendMessage(message)
    }
}

注意:在Kotlin中,嵌套类默认是static的,如果想让内部类持有外部类,使用 inner 关键字

  1. 在activity.onDestory()方法中,将messageQueue中的消息清除掉。
    此种方式是打断messageQueue->message。
    override fun onDestroy() {
        handler.removeCallbacksAndMessages(null)
        super.onDestroy()
    }

4.GC Roots是什么

以下引用自刘望舒大佬的著作《android进阶解密》:
选定一些对象作为GC Roots,并组成根对象集合,然后以这些GC Roots的对象作为起始点,向下搜索,如果目标对象到GC Roots是连接着的,我们则称该目标对象是可达的,如果目标对象不可达则说明目标对象是可以被回收的对象。

在java中,可以作为GC Roots的对象主要有以下几种:
1.java栈中引用的对象
2.本地方法栈中JNI引用的对象
3.方法区中运行时常量池引用的对象
4.方法区中静态属性引用的对象
5.运行中的线程
6.由引导类加载器加载的对象
7.GC控制的对象

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值