詳解Android Handler的使用

Handler

概要

Handler用於線程間的消息傳遞,它可以將一個線程中的任務切換到另一個線程執行。切換的目標線程與Handler內部持有的Looper所在線程一致。若初始化Handler時未手動設置Looper,Handler會通過ThreadLocal獲取並持有當前(初始化Handler時)線程的Looper。當Handler發送一條消息後,這條消息會進入目標線程的MessageQueue,目標線程的Looper掃描並且取出消息,最終由Handler執行這條消息。

構造器

Handler的構造器大致分為以下兩種:

public Handler(Callback callback, boolean async){}
public Handler(Looper looper, Callback callback, boolean async){}

構造器的參數列表:

  • callback:Handler處理消息的接口回調,執行消息時可能會調用該接口。
  • async:默認false,若該值為true,則消息隊列中的所有消息均是AsyncMessage。AsyncMessage的概念請看後續章節。
  • looper:消息的查詢者,會不斷輪詢檢查MessageQueue是否有消息。

若調用者傳遞Looper,直接使用該Looper;否則通過ThreadLocal從當前線程中獲取Looper。所以執行任務所在的目標線程不是創建Handler時所在的線程,而是Looper所在的線程。

sendMessageAtTime

無論是使用post(Runnable r)還是sendMessage(Message m)發送消息,最終都會執行到sendMessageAtTime方法。該方法指定瞭Message的執行者(msg.target=handler)和調用時機(msg.when)。

dispatchMessage

dispatchMessage方法用於執行事先註冊的Message和Handler回調,源碼如下:

public void dispatchMessage(Message msg) {
     if (msg.callback != null) {
         handleCallback(msg);
     } else {
         if (mCallback != null) {
             if (mCallback.handleMessage(msg)) {
                 return;
             }
         }
         handleMessage(msg);
     }
 }

可以發現回調的優先級是:Message的回調>Handler的回調(構造器章節中的callback)>Handler子類重寫的handleMessage方法。

ThreadLocal

ThreadLocal是一個線程內部的數據存儲類,用於存放以線程為作用域的數據,在不同的線程中可以持有不同的數據副本。通過ThreadLocal就可以很方便的查找到當前線程的Looper。ThreadLocal內部實現的UML類圖如下:

通過ThreadLocal查找Looper的流程如下:

  1. 通過Thread.currentThread()獲取當前線程對象。
  2. 取出線程對象持有的ThreadLocalMap對象。
  3. 以自身為key,獲取ThreadLocalMap中對應Entry的value。

Looper

Looper在Handler中扮演著消息循環的角色。它會不斷查詢MessageQueue中是否有消息。當沒有消息時Looper將一直阻塞。

若當前線程沒有Looper,且調用者未傳Looper,Handler會因為未獲取Looper而報錯。解決辦法是通過Looper.prepare在當前線程手動創建一個Looper,並通過Looper.loop開啟消息循環:

new Thread("Thread#2") {
    @override
    public void run() {
        Looper.prepare();
        Handler handler = new Handler();
        Looper.loop();
    }
}

Looper提供瞭quit和quitSafely兩種方式來退出一個Looper。區別在於前者會直接退出;後者則是在處理完消息隊列的已有消息後才安全退出。

Looper所在的線程會一直處於運行狀態,所以建議消息處理完畢後及時退出Looper,釋放線程。

MessageQueue

MessageQueue是消息的存儲隊列,內部提供瞭很多精彩的機制。

IdleHandler

IdleHandler本質上隻是一個抽象的回調接口,沒有做任何操作:

/**
 * Callback interface for discovering when a thread is going to block
 * waiting for more messages.
 */
public static interface IdleHandler {
    /**
     * Called when the message queue has run out of messages and will now
     * wait for more.  Return true to keep your idle handler active, false
     * to have it removed.  This may be called if there are still messages
     * pending in the queue, but they are all scheduled to be dispatched
     * after the current time.
     */
    boolean queueIdle();
}

看上述註釋可以瞭解,MessageQueue會在將要進入阻塞時執行IdleHandler的queueIdle方法,隊列阻塞的觸發時機是:

  • 消息隊列沒有消息。
  • 隊首消息的執行時間大於當前時間。

當我們希望一個任務在隊列下次將要阻塞時調用,就可以使用IdleHandler。在Android工程中最常見的例子就是:給Activity提供生命周期以外的回調。

比如我希望在佈局繪制完成後執行某個操作,但是Activity的onStart和onResume回調均在View繪制完成之前執行,可以看看onResume的官方註釋:

/**
 * ...
 * <p>Keep in mind that onResume is not the best indicator that your activity
 * is visible to the user; a system window such as the keyguard may be in
 * front.  Use {@link #onWindowFocusChanged} to know for certain that your
 * activity is visible to the user (for example, to resume a game).
 * ...
 */
  @CallSuper
  protected void onResume() {...}

這種情況下就可以給MessageQueue設置一個IdleHandler,等當前隊列中的消息(包括繪制任務)執行完畢並將要進入阻塞狀態時,調用IdleHandler的任務,確保任務在繪制結束後執行。

使用方式如下所示:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
     Looper.myLooper().getQueue().addIdleHandler(new MessageQueue.IdleHandler() {
         @Override
         public boolean queueIdle() {
             // do something when queue is idle
             // 返回值表示bKeepAlive標識:true->繼續使用,false->銷毀該Handler
             return false;
         }
     });
}

AsyncMessage和SyncBarrier

顧名思義,SyncBarrier表示同步柵欄(也叫作障礙消息),用於阻塞SyncMessage,優先執行AsyncMessage。該機制大大提升瞭MessageQueue的操作靈活性。

在進一步瞭解這兩個概念之前,需要先瞭解MessageQueue插入消息的機制,MessageQueue的enqueueMessage源碼如下(省略瞭喚醒隊列的相關代碼):

boolean enqueueMessage(Message msg, long when) {
     synchronized (this) {
         msg.markInUse();
         msg.when = when;
         Message p = mMessages;
         if (p == null || when == 0 || when < p.when) {
             // New head.
             msg.next = p;
             mMessages = msg;
         } else {
             // Inserted within the middle of the queue.
             Message prev;
             for (;;) {
                 prev = p;
                 p = p.next;
                 if (p == null || when < p.when) {
                     break;
             }
         }
         msg.next = p; // invariant: p == prev.next
         prev.next = msg;
     }
     return true;
}

從上述源碼可知,消息按照調用時機(when)有序排列,當when等於0時,直接將消息插在隊頭;當when等於隊列中消息的when時,將消息插在這些消息的後方。

假設這樣一個場景:我們有一個非常緊急的任務,希望能夠優先執行,該如何處理?

很簡單,發送一個when為0的消息,它將自動被插到列表的頭部。Handler中也提供瞭現成的接口:

public final boolean postAtFrontOfQueue(Runnable r)
{
    return sendMessageAtFrontOfQueue(getPostMessage(r));
}

public final boolean sendMessageAtFrontOfQueue(Message msg) {
	return enqueueMessage(queue, msg, 0);
}

將場景升級一下:我們有一個任務A,其他所有任務都依賴於A,若A未執行,則其他所有任務都不允許執行。

A插入隊列的時間和執行時間都是不確定的,在此之前,所有任務都不允許執行。按照當前的機制無法實現該需求,此時SyncBarrier和AsyncMessage就派上瞭用場,實現流程如下:

  • 調用MessageQueue.postSyncBarrier將SyncBarrier插入隊列:SyncBarrier本質上是一個target為空的消息,插入邏輯和普通消息一致,也是按照when確定插入位置。SyncBarrier的when固定是SystemClock.uptimeMillis(),因此將其插入到隊列的中間(SyncBarrier前面可能會有一些無時延的消息,後面可能會有帶時延的消息)。
  • 插入SyncBarrier後,輪詢消息直至SyncBarrier排到隊列頭節點,此時使用next方法查詢消息將自動過濾同步消息,隻執行異步消息。源碼如下所示:
// mMessages表示隊首消息
Message msg = mMessages;
if (msg != null && msg.target == null) {
    // Stalled by a barrier.  Find the next asynchronous message in the queue.
    do {
        prevMsg = msg;
        msg = msg.next;
    } while (msg != null && !msg.isAsynchronous());
}
  • 插入任務A(將A定義為AsyncMessage),由於SyncBarrier的存在,A將優先被執行(不排除A有時延,此時隊列將進入阻塞狀態,即便隊列裡可能存在無時延的同步消息)。
  • 隻要SyncBarrier放在隊首,同步消息將一直被阻塞,消息隊列隻能輸出AsyncMessage。當任務A執行完畢後,需要調用removeSyncBarrier手動將SyncBarrier移除。

Handler提供瞭接口讓我們插入AsyncMessage,即構造器中的asyc參數。當async為true時,所有通過Handler傳遞的消息均會被定義為AsyncMessage(前提是要和SyncBarrier配合使用,不然AsyncMessage沒有效果)。

再重新思考SyncBarrier和AsyncMessage機制的應用場景,本質上就是為瞭阻塞從Barrier消息到AsyncMessage消息之間的同步消息的執行。

在Android源碼中,佈局的繪制就使用瞭這種機制。在ViewRootImpl的scheduleTraversals方法中,會事先往主線程的消息隊列設置Barrier,再去提交AsyncMessage,阻塞在此期間的所有同步消息。源碼如下:

void scheduleTraversals() {
	if (!mTraversalScheduled) {
	    mTraversalScheduled = true;
        // 設置Barrier
	    mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
        // 該方法最終會提交一個AsyncMessage
	    mChoreographer.postCallback(
	            Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
	    if (!mUnbufferedInputDispatch) {
	        scheduleConsumeBatchedInput();
	    }
	    notifyRendererOfFramePending();
	    pokeDrawLockIfNeeded();
	}
}

Tips:關於Barrier的概念在Java並發中多有涉及,比如CountDownLatch、CyclicBarrier等。詳情請查看《Thinking in Java》21.7章節。

阻塞和喚醒機制

阻塞和喚醒機制是MessageQueue的精髓,極大降低瞭Loop輪詢的頻率,減少性能開銷。

在IdleHandler章節已經提及MessageQueue阻塞的時機:

消息隊列沒有消息。
隊首消息的執行時間大於當前時間。
next方法的源碼如下:

Message next() {
    int nextPollTimeoutMillis = 0;
    for (;;) {
        if (nextPollTimeoutMillis != 0) {
            Binder.flushPendingCommands();
        }
        // 關鍵方法,將線程阻塞nextPollTimeoutMillis毫秒,若nextPollTimeoutMillis為-1,線程將一直處於阻塞狀態。
        nativePollOnce(ptr, nextPollTimeoutMillis);
        synchronized (this) {
            // Ignore SyncBarrier code 
            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;
                    msg.markInUse();
                    return msg;
                }
            } else {
                // No more messages.
                nextPollTimeoutMillis = -1;
            }
            // Ignore IdleHandler code
            if (pendingIdleHandlerCount <= 0) {
                // No idle handlers to run.  Loop and wait some more.
                mBlocked = true;
                continue;
            }
        }
    }
}

插入消息時喚醒MessageQueue的時機(假設隊列處於阻塞狀態):

  • 隊首插入一條SyncMessage。
  • 隊首是一個柵欄,且插入一條離柵欄最近的AsyncMessage。

enqueueMessage方法的源碼如下:

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

喚醒的第二種時機特意強調瞭插入離Barrier最近的AsyncMessage。對於如下的阻塞情況,插入AsyncMessage時不需要將其喚醒:

Handler內存泄漏分析

瞭解瞭Handler的內部原理後,再來分析由Handler引起的內存泄露問題:

  1. 當定義瞭一個非靜態的Handler內部類時,內部類會隱式持有外圍類的引用。
  2. Handler執行sendMessageAtTime方法時,Message的target參數會持有Handler對象。
  3. 當Message沒有被執行時(比如now<when),若退出瞭Activity,此時Message依然持有Handler對象,而Handler持有Activity的對象,導致內存泄露。

解決方案:

  1. 將Handler定義為靜態內部類。
  2. 退出Activity時清空MessageQueue中對應的Message。

以上就是詳解Android Handler的使用的詳細內容,更多關於Android Handler的資料請關註WalkonNet其它相關文章!