Android Handler機制詳解

死神的記憶發表於2017-12-05

前言

Handler是Android的訊息機制,他能夠很輕鬆的線上程間傳遞資料。由於Android開發規範的限制,我們不能在主執行緒執行耗時操作(如網路,IO操作等),不能在子執行緒更新UI,所以Handler大部分用來在耗時操作與更新UI之間切換。這讓很多人誤以為Handler就是用來更新UI的,其實這只是它的一小部分應用。

開始

我相信大多數人對Handler的用法已經爛熟於心了,這篇文章不會去探討Handler的使用,而是著重從原始碼上分析Handler的執行機制。

想要了解Handler的執行機制,我們需要了解 MessageQueueMessageLooper 這幾個類。

  • MessageQueue 的意思就是訊息佇列,它儲存了我們需要用來處理的訊息Message
  • Message是訊息類,內部存在一個Bundle物件和幾個public欄位儲存資料,MessageQueue作為一個訊息佇列不能自己處理訊息,所以需要用到Looper
  • Looper是一個迴圈裝置,他負責從不斷從MessageQueue裡取出Message,然後回撥給HandlerhandleMessage來執行具體操作。
  • Handler在這裡面充當的角色更像是一個輔助類,它讓我們不用關係MessageQueueLooper的具體細節,只需要關係如何傳送訊息和回撥的處理就行了。

上面講了幾個關鍵類在Handler執行機制中的職責,相對大家對Handler機制有個粗略的瞭解。

我相信各位看官在閱讀這篇文章前都是帶著問題的,我們將通過問題來解答大家的疑惑。

分析

Looper

在分析Looper之前,我們還需要知道ThreadLocal這個類,如果對ThreadLocal還不太瞭解,可以去看我的另一篇文章《ThreadLocal詳解》。

Looper是如何建立?

Handler執行的執行緒和它持有的Looper有關。每個Thread都可以建立唯一的Looper物件。

 //為當前執行緒建立Looper物件的方法。
    public static void prepare() {
        prepare(true);
    }

    private static void prepare(boolean quitAllowed) {
    //使用ThreadLocal來儲存當前執行緒的Looper物件,這保證了每個執行緒有且僅有一個Looper物件。
    //這裡做了非空判斷,所以在同一個執行緒prepare方法是不允許被呼叫兩次的
    //第一次建立好的Looper物件不會被覆蓋,它是唯一的。
        if (sThreadLocal.get() != null) {
            throw new RuntimeException("Only one Looper may be created per thread");
        }
        sThreadLocal.set(new Looper(quitAllowed));
    }
    
複製程式碼

那麼主執行緒的Looper物件是怎麼建立的呢?

public static void prepareMainLooper() {
//其實主執行緒建立Looper和其他執行緒沒有區別,也是呼叫prepare()。
        prepare(false);
        synchronized (Looper.class) {
            if (sMainLooper != null) {
                throw new IllegalStateException("The main Looper has already been prepared.");
            }
            //但是Looper用sMainLooper這個靜態變數將主執行緒的Looper物件儲存了起來
            //可以通過getMainLooper()獲取,儲存MainLooper其實非常有作用,下面會講到。
            sMainLooper = myLooper();
        }
    }

    public static Looper getMainLooper() {
        synchronized (Looper.class) {
            return sMainLooper;
        }
    }
複製程式碼
Looper是如何從MessageQueue取出訊息並分發的?

Looper分發訊息的主要邏輯在loop方法裡


 /**
     * Run the message queue in this thread. Be sure to call
     * {@link #quit()} to end the loop.
     */
    public static void loop() {
        final Looper me = myLooper();
        //保證當前執行緒必須有Looper物件,如果沒有則丟擲異常,呼叫Looper.loop()之前應該先呼叫Looper.prepare().
        if (me == null) {
            throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
        }
        //Looper需要不斷從MessageQueue中取出訊息,所以它持有MessageQueue物件
        final MessageQueue queue = me.mQueue;

        // Make sure the identity of this thread is that of the local process,
        // and keep track of what that identity token actually is.
        Binder.clearCallingIdentity();
        final long ident = Binder.clearCallingIdentity();

        for (;;) {
           //這裡開始執行死迴圈,queue通過呼叫next方法來取出下一個訊息。
           //很多人很疑惑死迴圈不會相當耗費效能嗎,如果沒有那麼多訊息怎麼辦?
           //其實當沒有訊息的時候,next方法會阻塞在這裡,不會往下執行了,效能問題不存在。
            Message msg = queue.next(); // might block
            if (msg == null) {
                // No message indicates that the message queue is quitting.
                //這裡滿足了死迴圈跳出的條件,即取出的訊息為null
                //沒有訊息next不是會阻塞嗎,怎麼會返回null呢?
                //其實只有MessageQueue停止的時候(呼叫quit方法),才會返回null
                //MessageQueue停止後,呼叫next返回null,且不再接受新訊息,下面還有詳細介紹。
                return;
            }

            // This must be in a local variable, in case a UI event sets the logger
            Printer logging = me.mLogging;
            if (logging != null) {
                logging.println(">>>>> Dispatching to " + msg.target + " " +
                        msg.callback + ": " + msg.what);
            }

            //這裡的msg.target是Handler物件,分發訊息到Handler去執行。
            //有人問主執行緒可以建立這麼多Handler,怎麼保證這個Handler傳送的訊息不會跑到其它Handler去執行呢?
            //那是因為在傳送Message時,他會繫結傳送的Handler,在此處分發訊息時,也只會回撥傳送該條訊息的Handler。
            //那麼分發訊息具體在哪個執行緒執行呢?
            //我覺得這個不該問,那當然是當前方法在哪個執行緒呼叫就在哪個執行緒執行啦。
            msg.target.dispatchMessage(msg);

            if (logging != null) {
                logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }

            // Make sure that during the course of dispatching the
            // identity of the thread wasn't corrupted.
            final long newIdent = Binder.clearCallingIdentity();
            if (ident != newIdent) {
                Log.wtf(TAG, "Thread identity changed from 0x"
                        + Long.toHexString(ident) + " to 0x"
                        + Long.toHexString(newIdent) + " while dispatching to "
                        + msg.target.getClass().getName() + " "
                        + msg.callback + " what=" + msg.what);
            }

        //這裡對Message物件進行回收,會清空所有之前Message設定的資料。
        //正是因為Message有回收機制,我們在建立訊息的時候應該優先選擇Message.obtain(). 
        //如果傳送的訊息足夠多,Message快取的Message物件不夠了,obtain內部會呼叫new Message()建立一個新的物件。
            msg.recycleUnchecked();
        }
    }
複製程式碼
Looper 分發的訊息在哪個執行緒執行?

先給大家展示一段Looper文件上的示例程式碼

class LooperThread extends Thread {
   public Handler mHandler;
  
   public void run() {
        Looper.prepare(); //建立LooperThread的Looper物件
  
        mHandler = new Handler() {
            public void handleMessage(Message msg) {
              //處理髮送過來的訊息
            }
        };
    
        Looper.loop(); //開始迴圈訊息佇列
   }
}
複製程式碼

上面這段程式碼相信很多人都寫過,這是一段在子執行緒建立Handler的案例,其中handleMessage所執行的執行緒為LooperThread,因為Looper.loop()執行在LooperThreadrun方法裡。可以在其他執行緒通過mHandler傳送訊息到LooperThread

如果不呼叫Looper.prepare()直接new Handler()會怎麼樣呢?

我們可以檢視Handler的原始碼看看無參構造是如何執行的

public Handler() {
//呼叫兩參構造
    this(null, false);
}
    
public Handler(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());
        }
    }
//獲取當前執行緒的Looper,如果不建立Looper會丟擲異常。
//主執行緒我也沒看到有呼叫Looper.prepare()啊,怎麼在主執行緒不會拋異常呢?這個看下一個問題。
    mLooper = Looper.myLooper();
    if (mLooper == null) {
        throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");
    }
    mQueue = mLooper.mQueue;
    mCallback = callback;
    mAsynchronous = async;
}
複製程式碼
主執行緒的Looper物件在哪裡建立的?

從上一個問題可以看出如果不呼叫Looper.prepare()直接new Handler()就會丟擲異常`

throw new RuntimeException(
            "Can't create handler inside thread that has not called Looper.prepare()");`
複製程式碼

那麼主執行緒的Looper在哪裡建立的呢?首先它是建立了的,因為Looper.getMainLooper() != null,其實MainLooper建立的時間比我們想象的早,它在ActivityThread類裡面,ActivityThreadAndroid的啟動類,main方法就在裡面(如果有人問你Android有沒有main方法,你應該知道怎麼回答了吧),而MainLooper就是在main方法裡面建立的。

上程式碼:

//android.app.ActivityThread
public final class ActivityThread {
    ...
    public static void main(String[] args) {
         
        SamplingProfilerIntegration.start();
    
        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);
    
        Environment.initForCurrentUser();
    
        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());
    
        Security.addProvider(new AndroidKeyStoreProvider());
    
        // Make sure TrustedCertificateStore looks in the right place for CA certificates
        final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
        TrustedCertificateStore.setDefaultUserDirectory(configDir);
    
        Process.setArgV0("<pre-initialized>");
    
        //注意這裡,這裡建立了主執行緒的Looper
        Looper.prepareMainLooper();
    
        ActivityThread thread = new ActivityThread();
        thread.attach(false);
    
        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }
    
        AsyncTask.init();
    
        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }
        //開啟訊息迴圈
        Looper.loop();
    
        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
    
}
複製程式碼
MainLooper可以用來做什麼
判斷當前執行緒是否為主執行緒

因為Looper是在某一執行緒唯一的,那麼可以在麼做。如果

 public static boolean isMainThread() {
        //如果當前執行緒的Looper和MainLooper是同一個物件,那麼可以認為當前執行緒是主執行緒
        return Looper.myLooper() == Looper.getMainLooper() ;
}
複製程式碼

但是也有人說下面這樣也可以

public static boolean isMainThread() {
    //這個方法其實是不準確的,執行緒的名稱是可以隨便更改的。
    return Thread.currentThread().getName().equals("main");
}
複製程式碼

所以用Looper來判斷主執行緒是很好的做法

建立執行在主執行緒的Handler

Handler除了有無參構造,還有一個可以傳入Looper的構造。通過指定Looper,可以在任意地方建立執行在主執行緒的Handler

    class WorkThread extends Thread{
        private Handler mHandler;

        @Override
        public void run() {
            super.run();
            mHandler = new Handler(Looper.getMainLooper()) {
                @Override
                public void handleMessage(Message msg) {
                    super.handleMessage(msg);
                    //執行在主執行緒
                }
            };
            mHandler.sendEmptyMessage(0);
        }
    }
複製程式碼
Looper的quit方法和quitSafely方法有什麼區別

下面是Looper兩個方法的原始碼

public void quit() {
    mQueue.quit(false);
}
public void quitSafely() {
    mQueue.quit(true);
}
複製程式碼

可以看出實際上是呼叫的MessageQueuequit方法 下面是MessageQueue的原始碼

//android.os.MessageQueue
void quit(boolean safe) {
        if (!mQuitAllowed) {
            throw new IllegalStateException("Main thread not allowed to quit.");
        }

        synchronized (this) {
            if (mQuitting) {
                return;
            }
            mQuitting = true;
            //如果呼叫的是quitSafely執行removeAllFutureMessagesLocked,否則removeAllMessagesLocked。
            if (safe) {
            //該方法只會清空MessageQueue訊息池中所有的延遲訊息,
            //並將訊息池中所有的非延遲訊息派發出去讓Handler去處理,
            //quitSafely相比於quit方法安全之處在於清空訊息之前會派發所有的非延遲訊息。
                removeAllFutureMessagesLocked();
            } else {
            //該方法的作用是把MessageQueue訊息池中所有的訊息全部清空,
            //無論是延遲訊息(延遲訊息是指通過sendMessageDelayed或通過postDelayed等方法傳送的需要延遲執行的訊息)還是非延遲訊息。
                removeAllMessagesLocked();
            }

            // We can assume mPtr != 0 because mQuitting was previously false.
            nativeWake(mPtr);
        }
    }

複製程式碼

無論是呼叫了quit方法還是quitSafely方法,MessageQueue將不再接收新的Message,此時訊息迴圈就結束,MessageQueuednext方法將返回null,結束loop()的死迴圈.這時候再通過Handler呼叫sendMessagepost等方法傳送訊息時均返回false,表示訊息沒有成功放入訊息佇列MessageQueue中,因為訊息佇列已經退出了。

Message

Message.obtain()和new Message()如何選擇

Message提供了obtain等多個過載的方法來建立Message物件,那麼這種方式和直接new該如何選擇。下面看看obtain的程式碼。

public static Message obtain() {
    synchronized (sPoolSync) {
        if (sPool != null) {
            Message m = sPool;
            sPool = m.next;
            m.next = null;
            m.flags = 0; // clear in-use flag
            sPoolSize--;
            return m;
        }
    }
    return new Message(); //只有當從物件池裡取不出Message才去new
}

void recycleUnchecked() {
        //清除所有使用過的痕跡
        flags = FLAG_IN_USE;
        what = 0;
        arg1 = 0;
        arg2 = 0;
        obj = null;
        replyTo = null;
        sendingUid = -1;
        when = 0;
        target = null;
        callback = null;
        data = null;

    //回收到物件池
        synchronized (sPoolSync) {
            if (sPoolSize < MAX_POOL_SIZE) {
                next = sPool;
                sPool = this;
                sPoolSize++;
            }
        }
    }
複製程式碼

從上面程式碼可以看出,通過obtain方法是從物件池取,而new是建立了一個新的物件。我們應該使用obtain來建立Message物件,每次使用完後都會自動進行回收,節省記憶體。

未完待續......

相關文章