从源码角度看Handler为何发生内存泄漏及其解决方法

从源码角度看Handler为何发生内存泄漏及其解决方法



前言

在一次开发工作中,由于平时使用的测试机坏了,我拿了一个很老版本的手机来测试app,结果发现在某个复杂页面的性能表现有很大的问题。于是打开相关的性能统计平台,果不其然,发现了内存泄漏问题。于是我打开了小伙伴写的代码,发现了这么一串类似代码。

Handler handler = new Handler(){
        @Override
        public void handleMessage(Message msg){
            super.handleMessage(msg);
            mPriceEditText.setText("1000");
        }
    };

于是我立马联系小伙伴说不能这么写,这样子会导致内存泄漏,小伙伴听到之后问了我一句,为什么会导致内存泄漏,我一下子不知道该如何回答,于是直接把写这段代码时,官方的提示给他看。
官方提示如果加上static修饰词,容易导致内存泄漏

小伙伴哦的一句,就默默改代码去了。问题挺好解决,小伙伴很好糊弄,自己却糊弄不了自己,不只要知道怎么解决,还得静下心来自己摸索摸索,为什么handler会导致能存泄漏呢?是因为内部类持有了对外部类的引用引起的么?但为何其他内部类不会有这个问题?怀着这个问题,我觉得看看handler的相关源码。


下面我们从源码来了解Handler发生内存泄漏的原因,以及解决方法

一、内存泄漏的本质是什么?

在了解handler为什么会导致内存泄漏,是否是因为是内部类,其他的内部类是否会导致内存泄漏之前,我觉得我们的先了解内存泄漏的本质是什么。

内存泄漏的本质和原因是:长生命周期的对象持有短生命周期对象的引用.
在android里面,很多情况下的短生命周期最有可能就是我们的Activity。假如用户退出了activity,但是有一个长生命周期的对象引用了该activity,就会导致activity无法回收。

而我们App里面最长的生命周期是主线程。任何被主线程持有的对象都是无法被回收的。

二、Handler内存泄漏的原因

1.什么是Handler

首先我们了解下handler是什么:

Handler是android子线程和主线程之间通信的一种机制。
主要是通过looper轮询检测发送消息到MessagerQuerue,如果MessageQueue对Message入列,就会取出消息,交给Handler去处理。
也就是说,在Handler通信过程中,一共涉及到了Hanler、Looper、MessageQueue和Message这四个类。所以我们就需要对这4个类进行具体分析,看他们之间的持有链是什么样的。

2.Looper的创建与持有

如果我们打开Handler的源码,我们可以看到他的构造方法有这么一串。

public Handler(@Nullable Callback callback, boolean async) {
        if (FIND_POTENTIAL_LEAKS) {
            final Class<? extends Handler> klass = getClass();
            if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                    (klass.getModifiers() & Modifier.STATIC) == 0) {
                Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                    klass.getCanonicalName());
            }
        }

        mLooper = Looper.myLooper();// 获取该线程的Looper对象
        if (mLooper == null) {
            throw new RuntimeException(
                "Can't create handler inside thread " + Thread.currentThread()
                        + " that has not called Looper.prepare()");
        }
        mQueue = mLooper.mQueue;
        mCallback = callback;
        mAsynchronous = async;
    }

在这个构造方法中,我们可以看到如果我们不传入Looper对象,那么将会使用本线程的Looper。比如我们在非UI线程之后,会首先调用Looper.prepare()来制定Looper对象。

那如果在UI线程中初始化的Handler,它的Looper是在哪里赋值的?

这时需要我们打开UI线程源码,也就是主线程ActivityThread,因为ActivityThread是系统文件,并没有被打包到jar包中,所以反编译是看不到的。但我们有其他办法可以看到,就是在SDK目录下,打开任一个SDK目录的app目录中,可以找到ActivityThread文件,具体目录如下:
FrameWork源码的文件目录
分析源码,我们首要看其main函数,在main函数中有一段

    public static void main(String[] args) {
        //省略与本文无关的代码
        ...
        Process.setArgV0("<pre-initialized>");

        Looper.prepareMainLooper();
        long startSeq = 0;
        if (args != null) {
            for (int i = args.length - 1; i >= 0; --i) {
                if (args[i] != null && args[i].startsWith(PROC_START_SEQ_IDENT)) {
                    startSeq = Long.parseLong(
                            args[i].substring(PROC_START_SEQ_IDENT.length()));
                }
            }
        }
        ActivityThread thread = new ActivityThread();
        thread.attach(false, startSeq);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        // End of event ActivityThreadMain.
        Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

可以看到在ActivityThread的main函数中,调用了Looper的prepareMainLooper()方法,于是我们进入looper的源码查看相关方法

public static void prepareMainLooper() {
        prepare(false);//执行了looper.prepare()
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            sMainLooper = myLooper();
        }
    }
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));
    }

这里可以看到在主线程中ActivityThread的main方法中会调用looper.prepareMainLopper方法,进而构造一个looper。然后把looper放进ThreadLocal,并放到主线程的ThreadLocalMap中。也就是说这个looper是主线程持有的,是无法被释放掉的。

3.MessageQueue的持有链

在前面我们知道Handler的原理是Looper调用loop()方法不断的轮询MessageQueue,那按照道理,Looper应该是持有对MessageQueue的引用的,于是我们带着问题来查找下源码,能看到以下的片段:

public static void loop() {
        //与本文无关代码...
        final MessageQueue queue = me.mQueue;
        //与本文无关代码...
        for (;;) {
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                return;
            }
        //与本文无关代码...
        }
    }

在looper.loop()方法中,我们看到了死循环中对MessageQueue的轮询操作,也就是looper是实现了对MessageQueue的引用,实现了引用链ActivityThread->looper->MessageQueue。

4.Message和Handler的持有链

我们知道MessageQueue是消息队列,Message是消息,那Message是肯定入列的,也就是MessageQueue是肯定持有Message的,但是这也就引出一个问题,Message是什么时候入列的。

这也就引出了Handler造成内存泄漏有一个前提,必须曾经发送过消息,即使用过sendMessage()方法,为什么使用了sendMessage()方法才会造成内存泄漏,我们看看sendMessage方法的源码。

public final boolean sendMessage(@NonNull Message msg) {
        return sendMessageDelayed(msg, 0);
    }
public final boolean sendMessageDelayed(@NonNull Message msg, long delayMillis) {
        if (delayMillis < 0) {
            delayMillis = 0;
        }
        return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
    }
public boolean sendMessageAtTime(@NonNull Message msg, long uptimeMillis) {
        MessageQueue queue = mQueue;
        if (queue == null) {
            RuntimeException e = new RuntimeException(
                    this + " sendMessageAtTime() called with no mQueue");
            Log.w("Looper", e.getMessage(), e);
            return false;
        }
        return enqueueMessage(queue, msg, uptimeMillis);
    }
private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
        msg.target = this; //把handler赋值给message的target
        msg.workSourceUid = ThreadLocalWorkSource.getUid();

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

在我们调用Handler的sendMessage()方法之后,其实会一步步最终执行Handler的enqueueMessage()方法,这里可以看到把handler自身赋值给了Message的target。这是一个很重要的操作。保证了一个线程中多个handler发消息处理消息时,能准确的找到对应的Handler来处理。但是这里也就意味着Message持有了Handler。那意味着Message -> handler。此时只要MessageQueue持有了Message,那持有链就连接成了,下面我们看看Message的入列。

在上面的queue.enqueueMessage方法中引导我们来到了MessageQueue的源码中。

enqueueMessage()是MessageQueue的入列方法。虽然 MessageQueue 的名字包含队列(Queue),但是其底层实现采用的是单链表,这是因为链表在插入和删除方面的性能好,下面是enqueueMessage()源码:

boolean enqueueMessage(Message msg, long when) {
        if (msg.target == null) {
            throw new IllegalArgumentException("Message must have a target.");
        }
        synchronized (this) {
            if (msg.isInUse()) {
                throw new IllegalStateException(msg + " This message is already in use.");
            }
            if (mQuitting) {
                IllegalStateException e = new IllegalStateException(
                        msg.target + " sending message to a Handler on a dead thread");
                Log.w(TAG, e.getMessage(), e);
                msg.recycle();
                return false;
            }
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;//p 代表下一个消息
            boolean needWake;
            if (p == null || when == 0 || when < p.when) {//插在消息列表的最前面
                // New head, wake up the event queue if blocked.
                msg.next = p;
                mMessages = msg;
                needWake = mBlocked;
            } else {//按照时间顺序从小到大进行的
                // Inserted within the middle of the queue.  Usually we don't have to wake
                // up the event queue unless there is a barrier at the head of the queue
                // and the message is the earliest asynchronous message in the queue.
                needWake = mBlocked && p.target == null && msg.isAsynchronous();
                Message prev;
                for (;;) {
                    prev = p;
                    p = p.next;
                    if (p == null || when < p.when) {
                        break;
                    }
                    if (needWake && p.isAsynchronous()) {
                        needWake = false;
                    }
                }
                msg.next = p; // invariant: p == prev.next
                prev.next = msg;
            }
            // We can assume mPtr != 0 because mQuitting is false.
            if (needWake) {
                nativeWake(mPtr);
            }
        }
        return true;
    }

在上面代码中,我们可以看到如果Handler调用了sendMessage的话,就会推动把Message加入到MessageQueue队列之中,至于添加到哪个位置,主要根据时间而定。并且会把handler自身赋值给Message的target。此时完整的持有链ActivityThread -> Looper ->MessageQueue -> Message -> Handler就成立了。也就是说Handler此时被主线程间接持有,是无法被回收的。
在我们的Actvity中,Handler也包括了对Activity外部类的引用,如果在我们关闭界面时,有消息还未处理完,就会导致Activity无法被回收,导致内存泄漏。

三、Handler内存泄漏的解决方案

刚刚说到内存泄漏时MessageQueue里面还有消息未处理导致的,那是否只要保证MessageQueue里面没有队列就可以了?

确实当Message被回收时,会自动调用recycle方法把message置为空,即target不引用Handler。也就是说能把Message -> Handler的链条断开。即message都被消费后,messageQueue为空时,就不会指向handler,就不会导致activity内存泄漏。
但是我们在开发中,是无法保证message全部被消费的,特别是发送延时消息,当然我们可以通过监听生命周期,一旦Activity被销毁,则清空MessageQueue里面所有的Message,但是如果忘记重写onDestroy()方法,就会导致内存泄漏,所以这个方法不太合理。

那该如何更合理的解决呢?

其实在前言那里加油提到,在我们写下Handler内部类时,AndroidStudio就会提示我们需要使用static修饰符,否则会内存泄漏,所以最简单的解决方法就是乖乖听劝,设置为静态内部类。
我们也可以使用弱引用,可以防止Handler持有Activity,断开Handler -> Activity这条链。
下面是静态内部类和弱引用的写法,仅供参考:

private static class FragmentHandler extends Handler{
        private WeakReference<TradeDetailFragment> mWeakReference;
        public FragmentHandler(TradeDetailFragment fragment) {
            mWeakReference = new WeakReference<>(fragment);
        }
        @Override
        public void handleMessage(Message msg) {
            TradeDetailFragment host = mWeakReference.get(); // 判断所在的 Activity 的引用是否被回收了
            if (host != null) {
                switch (msg.what){
                    case 99:
                        // do something
                        sendEmptyMessageDelayed(100, 2000);
                        break;
                    case 100:
                        // do something
                        break;
                    default:break;
                }
            }
        }
    }

这种方式在Activity和Fragment都是可以实现的。

总结一下今天的内容:

主要从源码分析了Handler为何会导致内存泄漏,其实不是因为是内部类导致,主要是因为Handler被长生命周期对象所持有,形成了 ActivityThread -> Looper ->MessageQueue -> Message -> Handler的持有链。而Handler又持有了外部类的引用,所以导致了Activity无法被回收。
然后提供了解决Handler内存泄漏的方法:
1、保证Activity退出时,清空MessageQueue的所有Message。
2、使用static修饰内部类,内部类写成静态内部类或者外部类。
3、在Handler中,将对象改成弱引用,就能保证在垃圾回收时被正常回收。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值