從原始碼角度來讀Handler

憤怒的西瓜發表於2018-11-21

最近打算從原始碼角度來讀一下Handler,MessageQueue,Message,Looper,這四個面試必考項。所以今天先從Handler開始。

一.Handler的作用

原始碼定義

There are two main uses for a Handler: 
- (1) to schedule messages and
  runnables to be executed as some point in the future; and 
- (2) to enqueue
 an action to be performed on a different thread than your own.
複製程式碼
  1. 在未來的某個時刻呼叫某事件

  2. 執行緒之間互相通訊

這就是Handler設計出來最主要的兩種用途

二.Handler的構造方法

1.public Handler()
2.public Handler(Callback callback)
3.public Handler(Looper looper)
4.public Handler(Looper looper, Callback callback)
5.public Handler(boolean async)
6.public Handler(Callback callback, boolean async)
7.public Handler(Looper looper, Callback callback, boolean async)
複製程式碼

從Handler原始碼看,Handler建構函式一共7個,不過仔細觀察可發現這七個歸根到底都是圍繞著三個引數而來,即looper,callback,async。

Looper大家應該都知道,就是負責訊息進行迴圈的,每個Handler都必定要對應一個獨一無二的Looper。這樣你Handler sendMessage以後,你的Message才能進入到對應的Looper去做迴圈,然後被呼叫。

Callback,估計大家有點陌生,說實話我之前也沒怎麼用過。先放上原始碼

  /**
     * Callback interface you can use when instantiating a Handler to avoid
     * having to implement your own subclass of Handler.
     */
    public interface Callback {
        /**
         * @param msg A {@link android.os.Message Message} object
         * @return True if no further handling is desired
         */
        public boolean handleMessage(Message msg);
    }
複製程式碼

看原始碼就是一個簡單的介面。

然後再仔細看該介面的呼叫會明白這是在Handler呼叫自身handleMessage方法之前做的一次呼叫,若Callback引數不為Null,則處理訊息時會走Callback的handleMessage,而不會走Handler的handleMessage方法。相關原始碼在接下來的第四模組Handler處理訊息的方法中放出。這個具體使用場景歡迎補充。

async,官方文件說明

If true, the handler calls {@link Message#setAsynchronous(boolean)} for each {@link Message} that is sent to it or {@link Runnable} that is posted to it.
複製程式碼

然後查詢哪裡使用到了這個引數,發現在訊息進入MessageQueue時使用。程式碼如下

 private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
        msg.target = this;
        if (mAsynchronous) {
            msg.setAsynchronous(true);
        }
        return queue.enqueueMessage(msg, uptimeMillis);
    }
複製程式碼

相當於所有通過這個Handler來傳送訊息的Message,若Handler的async為true,則這些Message會被標記為async為true,那麼這個Message引數為true或false有什麼作用麼? 參考這篇文章Android訊息處理機制(Handler、Looper、MessageQueue與Message) 以及原始碼可知,預設的訊息該值都為false,若為true,則Message不再受同步障礙影響,即使設定了同步障礙,這些訊息也能不間斷的被執行,反過來預設的訊息若被設了同步障礙,則這個Looper取Message過程則會被同步障礙中斷,原因又是一個類似死迴圈。因為自己沒用過,看原始碼,說在系統View.invalidate程式碼中有使用,接下來可研究相關使用示例。

(剛看了相關程式碼感覺挺有意思的下面補充一下)

首先看ViewRootImpl 中scheduleTraversals的程式碼,重新整理開始的地方

 void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
            notifyRendererOfFramePending();
            pokeDrawLockIfNeeded();
        }
    }
複製程式碼

其中

mHandler.getLooper().getQueue().postSyncBarrier();
複製程式碼

程式碼往MessageQueue中加入了一個同步屏障(說白了同步屏障是一個特殊的Message)然後由於同步屏障的作用MessageQueue中那些非非同步的訊息都不進行獲取操作了,這麼做就是保障重新整理的Message能夠第一時間得到Looper的呼叫。 接著看mChoreographer.postCallback的內部程式碼

   Message msg = mHandler.obtainMessage(MSG_DO_SCHEDULE_CALLBACK, action);
                msg.arg1 = callbackType;
                msg.setAsynchronous(true);
                mHandler.sendMessageAtTime(msg, dueTime);
複製程式碼

發現了這個是傳送非同步Message的程式碼。由於其他同步Message都無法呼叫,所以這個非同步訊息可以第一時間得到呼叫。 同時繪製程式碼如下

 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

複製程式碼

在繪製程式碼中通過 mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier)這個程式碼把這個同步屏障給去除了,使整個Looper又恢復正常的呼叫。

三.Handler發訊息的主要方法

Handler發訊息的幾種方式(之前看某個談面試心得的時候說過)

1. public final boolean post(Runnable r)
2. public final boolean postDelayed(Runnable r, long delayMillis)
3. public final boolean sendEmptyMessage(int what)
4. public final boolean sendMessage(Message msg)
5. public boolean sendMessageAtTime(Message msg, long uptimeMillis) 
6. public final boolean sendMessageDelayed(Message msg, long delayMillis)
複製程式碼

這些方法最後都是呼叫的 public boolean sendMessageAtTime(Message msg, long uptimeMillis) 方法。

其中Runnable r被getPostMessage 方法包裝成了Message引數

 private static Message getPostMessage(Runnable r) {
        Message m = Message.obtain();
        m.callback = r;
        return m;
    }
複製程式碼

四.Handler處理訊息的方法

主要為dispatchMessage方法

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

看程式碼首先第一步會去取msg中callback欄位,該欄位在第三部分中(Handler發訊息的主要方法)已經提到,是在Handler.post(Runnable r)方法呼叫中,引數r被getPostMessage方法封裝而來。 如果msg中callback為null,則走第二步,呼叫Handler中引數mCallback中的handlerMessage方法,這個mCallback則是在Handler構造時傳入,可參考第二部分。若這個也沒有,才會走我們熟悉的第三步呼叫Handler的複寫handleMessage(msg)方法。

其他相關問題:

  1. 由於Looper.loop()方法會進入死迴圈,那麼子執行緒在Looper.loop()之後的程式碼是否會被執行?

答:不會

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_second);
        initView();
        new Thread(new Runnable() {
            public void run() {
                Looper.prepare();
                Handler handler = new Handler(){
                    @Override
                    public void handleMessage(Message msg) {
                        Toast.makeText(getApplicationContext(), "handler msg", Toast.LENGTH_LONG).show();
                    }
                };
                handler.sendEmptyMessage(1);
                Looper.loop();
                Toast.makeText(getApplicationContext(), "after loop", Toast.LENGTH_LONG).show();
            }
        }).start();
    }
複製程式碼

如上程式碼進行了測試,after loop 不會被呼叫,這樣就留下了一個坑,會導致子執行緒一直在迴圈中不會被回收。所以子執行緒用了Looper.loop以後一定要呼叫looper.quit() or looper.quitSafely()進行退出。否則你這個執行緒就不會被回收,後果就是資源洩漏。


2.The following Handler class should be static or leaks might occur: <classCanonicalName> 記憶體洩漏

原因:

由於Handler有可能會被Looper#mQueue#mMessages#target引用,而很有可能由於訊息還未到達處理的時刻,導致引用會被長期持有,如果Handler是一個非靜態內部類,就會持有一個外部類例項的引用,進而導致外部類很有可能出現無法及時gc的問題。

通用解決方法:

直接靜態化內部類,這樣內部類Handler就不再持有外部類例項的引用,再在Handler的建構函式中以弱引用(當所指例項不存在強引用與軟引用後,GC時會自動回弱引用指向的例項)傳入外部類供使用即可。

示例程式碼

public static class WeakUiHandler<T> extends Handler {
    WeakReference<T> classInstance; 

    public WeakUiHandler(T instance) {
        classInstance = new WeakReference<T>(instance);
    }

    public T getClassInstance() {
        return classInstance.get();
    }
}
複製程式碼


參考資料:

  1. blog.dreamtobe.cn/2016/03/11/… 強烈推薦
  2. www.cnblogs.com/angeldevil/…


相關文章