Android:关于 Handler 消息传递机制(二)

写在前面

两年前的时候有写过一点对 Handler 的总结(Android:关于 Handler 消息传递机制),现在重新回顾,并增加一些东西和理解。

内容

内存泄露

在 Activity 里我们使用 Handler 的时候,这样写的话 IDE 会提示可能存在内存泄露的问题。

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

假设在一个这样的场景,我们通过 Handler 来发送一个延时的消息,在消息还没发送之前,我们就退出了 Activity,那么此时就会存在内存泄露的问题。

原因在于 Handler 在这里是一个匿名内部类,匿名内部类会持有外部类的引用,所以此时 Activity 退出,由于 Handler 里的内容还未处理完,就还持有 Activity 的引用。所以触发 GC 回收的时候,就无法回收这个 Activity。

那么 GC 不回收一些对象是因为GC Root 的可达性,这些对象在 GC Root 上还被引用着,所以就不会被回收。

在使用 Handler 的时候,我们需要Looper.prepare();:

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

 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));
    }

这里用到了 ThreadLocal 类型的 sThreadLocal,是一个静态变量,静态变量就可以作为一个 GC Root 。

那么 Looper 里面会有个 MessageQueue,MessageQueue 里会有 Message:

public final class Looper {
   ...
   final MessageQueue mQueue;
   ...
}

而 Message 的 target 是个 Handler 类型:

public final class Message implements Parcelable {
   ...
   Handler target;
   ...
}

Handler 在发送消息的时候,会把自己设置到这个 target 里:

public class Handler {
       ...
       private boolean enqueueMessage(@NonNull MessageQueue queue, @NonNull Message msg,
            long uptimeMillis) {
            msg.target = this;
            ...
            return queue.enqueueMessage(msg, uptimeMillis);
    }
    ...
}

于是这样一条 GC Root 的引用链就出现了:

ThreadLocal --> Looper --> MessageQueue --> Message --> Handler --> Activity

所以 Activity 退出后,因为消息未处理,就导致 Activity 无法被回收,从而导致了内存泄露。

解决方法

清空消息

在 Activity 退出的时候,把消息队列里的消息都清掉

   // MainActivity.java
    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);

    }
  
    // Handler.java
    /**
     * Remove any pending posts of callbacks and sent messages whose
     * <var>obj</var> is <var>token</var>.  If <var>token</var> is null,
     * all callbacks and messages will be removed.
     */
    public final void removeCallbacksAndMessages(Object token) {
        mQueue.removeCallbacksAndMessages(this, token);
    }
静态内部类和弱引用

由于静态内部类不会持有外部类 的引用,所以将 Handler 以这种方式呈现,解决到 Handler 与 Activity 的引用关系。

但在一些场景下,我们可能需要在 Handler 里通过持有 Activity 来调用它的一些方法,此时如果直接传入的话,显然就造成了一种直接引用的情况,所以在这种情况下我们需要弱引用,被标记为弱引用的对象,在 GC 回收的时候,就会被回收。因此在被回收的情况下,我们就需要做多一些判空的处理。

所以当我们这个 Handler 里不需要使用到 Activity 的方法的时候,也没必要引入这个 Activity 了,也就不需要多这个弱引用的关系。

private static class MyHandler extends Handler {
        WeakReference<MainActivity> mainActivityWeakReference;

        public MyHandler(MainActivity activity) {
            mainActivityWeakReference = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity activity = mainActivityWeakReference.get();
            if(activity != null) {
                activity.setTextViewText("hello");
            }
        }
    }

创建一个 Message

我们在创建一个 Message 的时候,可能会使用

Message message = new Message();

但官方里还是推荐我们使用 Handler 的 obtainMessage()方法,它会在一个全局的 Message 池里返回一个新的 Message 给你,从效率上会更快,并且也起到了复用的作用。

 Handler handler = new Handler();
 handler.obtainMessage();

这里面是一个享元的设计模式。

 /**
     * Returns a new {@link android.os.Message Message} from the global message pool. More efficient than
     * creating and allocating new instances. The retrieved message has its handler set to this instance (Message.target == this).
     *  If you don't want that facility, just call Message.obtain() instead.
     */
    @NonNull
    public final Message obtainMessage()
    {
        return Message.obtain(this);
    }

每个线程只能有一个 Looper

在使用 Handler 的时候,我们需要 Looper.prepare()

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

 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));
    }

ThreadLocal 里会在第一次的时候,创建 Looper,后续再调用的话就会抛出异常,提示每个线程里只能有一个 Looper 被创建。

因此我们要获取当前线程的 Looper 的话,就需要使用 Looper.myLooper()

 /**
     * Return the Looper object associated with the current thread.  Returns
     * null if the calling thread is not associated with a Looper.
     */
    public static @Nullable Looper myLooper() {
        return sThreadLocal.get();
    }

ThreadLocal

待续

Looper 死循环里为什么不会导致卡死

在 MessageQueue 里面会通过next()方法来获取下一条信息:

    // MessageQueue.java
    
    private native void nativePollOnce(long ptr, int timeoutMillis); 
    
    Message next() {
        ...
        int nextPollTimeoutMillis = 0;
        for (;;) {
            if (nextPollTimeoutMillis != 0) {
                Binder.flushPendingCommands();
            }

            nativePollOnce(ptr, nextPollTimeoutMillis);

            synchronized (this) {
                // Try to retrieve the next message.  Return if found.
                final long now = SystemClock.uptimeMillis();
                Message prevMsg = null;
                Message msg = mMessages;
                ...
                if (msg != null) {
                    if (now < msg.when) {
                        // Next message is not ready.  Set a timeout to wake up when it is ready.
                        nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
                    } else {
                        // Got a message.
                        mBlocked = false;
                        if (prevMsg != null) {
                            prevMsg.next = msg.next;
                        } else {
                            mMessages = msg.next;
                        }
                        msg.next = null;
                        if (DEBUG) Log.v(TAG, "Returning message: " + msg);
                        msg.markInUse();
                        return msg;
                    }
                } else {
                    // No more messages.
                    nextPollTimeoutMillis = -1;
                }
            }
                ...
            }
            ...
            nextPollTimeoutMillis = 0;
        }
    }

在这里面它会用 nextPollTimeoutMillis 来记录下一条消息要开始的时间点,如果消息列表里没有消息就设为 -1。

因此在循环的时候,会先判断这个变量,通过nativePollOnce(ptr, nextPollTimeoutMillis);可以让其在下个时间点到来之前,进入休眠状态。这里涉及到 Linux 下的多路复用机制(epoll)相关。

假设现在有一条消息被加入到消息队列里,而线程又还处于休眠状态,时间点未到,那么就会通过nativeWake()方法将其唤醒:

// MessageQueue.java    

boolean enqueueMessage(Message msg, long when) {
        ...
        synchronized (this) {
            ...
            msg.markInUse();
            msg.when = when;
            Message p = mMessages;
            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;
    }

更多这方面的内容还可以查看:《卡顿、ANR、死锁,线上如何监控? 》

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值