深入探索Android訊息機制之Handler

沈敏傑發表於2017-12-14

之前寫了一篇關於Message的文章,感興趣的朋友可以去看一下【Android訊息機制之Message解析(面試)】,這一次我們們來聊一下訊息機制中用得最多的Handler,也是面試中問得最多的之一,在這裡我先拋幾個問題出來:

1.Handler、Looper、Thread有什麼關係?

2.為什麼在子執行緒建立handler會拋異常 "Can't create handler inside thread that has not called Looper.prepare()"?

3.如何使用handler來處理message?

4.為什麼不能在子執行緒更新UI?

我們帶著問題去看原始碼:

大家先對一下的物件,腦補一下工廠的情景:

Handler:訊息的處理者,工廠中流水線的工人。

Message:系統傳遞的訊息,工廠中流水線上的產品。

MessageQueue:訊息佇列,工廠中流水線上的傳送帶。

Looper:發動機,工廠中使流水線的傳送帶運動的發動機。

我們先從程式入口來進行分析,Android應用程式的入口在ActivityThread的main函式中,我們先從main函式進行分析:

ActivityThread.java:

public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
    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());

    // 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>");

    //在android應用程式的入口其實在ActivityThread的main方法
    //在這裡,主執行緒會建立一個Looper物件。
    Looper.prepareMainLooper();

    ActivityThread thread = new ActivityThread();
    thread.attach(false);

    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");
}
複製程式碼

程式碼很長,我們挑關鍵的程式碼來看,在程式碼中我已寫上註釋,在main函式中,Looper呼叫了prepareMainLooer(),我們再進去Looper看看。

Looper.java:

/**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. The main looper for your application
 * is created by the Android environment, so you should never need
 * to call this function yourself.  See also: {@link #prepare()}
 */
public static void prepareMainLooper() {
    //在主執行緒中,其預設初始化一個Looper物件,因此我們在主執行緒的操作中是不需要自己去調prepare()。
    prepare(false);
    synchronized (Looper.class) {
        //這裡先進行判斷,在主執行緒是否已經存在Looper了,
        // 避免我們手動去呼叫prepareMainLooper(),因為這個是給程式入口初始化的時候系統會自動呼叫的
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        //設定全域性變數,主執行緒的looper
        sMainLooper = myLooper();
    }
}

複製程式碼

Initialize the current thread as a looper, marking it as an* application's main looper. The main looper for your application* is created by the Android environment, so you should never need* to call this function yourself.

注意這個函式的註釋,大概意思是:在主執行緒建立一個looper,是這個主執行緒的主looper,當這個app在初始化的時候就會自行建立,因此這個函式不是給你們呼叫的,是給系統自身在程式建立的時候呼叫的。

我們繼續往下看,有個prepare(boolean)函式,我們去看看這個到底是用來幹什麼的。 Looper.java:

private static void prepare(boolean quitAllowed) {
    //先判斷當前執行緒是否已經存在Looper了,如果存在,不允許設定新的Looper物件,一個執行緒只允許存在一個Looper
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //在當前執行緒中,建立新的Looper物件,並繫結當前執行緒
    sThreadLocal.set(new Looper(quitAllowed));
}
複製程式碼

在這裡,我們看到了sThreadLocal,我們先看看這個sThreadLocal在Looper是幹什麼用的。

Looper.java:

// sThreadLocal.get() will return null unless you've called prepare().
//sThreadLocal在Looper中作為全域性變數,用於儲存每個執行緒中的資料,可以看做是容器
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
複製程式碼

Looper中,sThreadLocal作為一個全域性變數,sThreadLocal其實是儲存Looper的一個容器,我們繼續往ThreadLocal的get、set進行分析。

ThreadLocal.java:

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    //獲取當前執行緒儲存的物件--通過get函式來獲取Looper物件
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T) e.value;
    }
    return setInitialValue();
}

/**
 * Sets the current thread's copy of this thread-local variable
 * to the specified value.  Most subclasses will have no need to
 * override this method, relying solely on the {@link #initialValue}
 * method to set the values of thread-locals.
 *
 * @param value the value to be stored in the current thread's copy of
 *              this thread-local.
 */
public void set(T value) {
    //把當前的looper儲存到當前執行緒中
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
複製程式碼

我們看到關鍵的程式碼: Thread t=Thread.currentThread();

也就是說,我們的Looper物件分別儲存在相對應的執行緒中。我們看回來我們的prepare(boolean)函式: looper.java:

private static void prepare(boolean quitAllowed) {
    //先判斷當前執行緒是否已經存在Looper了,如果存在,不允許設定新的Looper物件,一個執行緒只允許存在一個Looper
    if (sThreadLocal.get() != null) {
        throw new RuntimeException("Only one Looper may be created per thread");
    }
    //在當前執行緒中,建立新的Looper物件,並繫結當前執行緒
    sThreadLocal.set(new Looper(quitAllowed));
}

複製程式碼

Looper.prepare(boolean)的作用就是建立一個Looper物件,並與當前執行緒繫結在一起。在程式碼中,首先判斷當前執行緒是否已經存在looper,如果不存在則建立新的looper並且繫結到當前的執行緒上。

再看回之前的程式碼: looper.java:

**
 * Initialize the current thread as a looper, marking it as an
 * application's main looper. The main looper for your application
 * is created by the Android environment, so you should never need
 * to call this function yourself.  See also: {@link #prepare()}
 */
public static void prepareMainLooper() {
    //在主執行緒中,其預設初始化一個Looper物件,因此我們在主執行緒的操作中是不需要自己去調prepare()。
    prepare(false);
    synchronized (Looper.class) {
        //這裡先進行判斷,在主執行緒是否已經存在Looper了,
        // 避免我們手動去呼叫prepareMainLooper(),因為這個是給程式入口初始化的時候系統會自動呼叫的
        if (sMainLooper != null) {
            throw new IllegalStateException("The main Looper has already been prepared.");
        }
        //設定全域性變數,主執行緒的looper
        sMainLooper = myLooper();
    }
}
複製程式碼

分別看一下sMainLooper是什麼,myLooper()又是什麼? Looper.java:

//儲存一個主執行緒的looper
private static Looper sMainLooper;  // guarded by Looper.class

/**
 * Return the Looper object associated with the current thread.  Returns
 * null if the calling thread is not associated with a Looper.
 */
public static Looper myLooper() {
    //使用當前執行緒的looper
    return sThreadLocal.get();
}

複製程式碼

sMainLooper在Looper做為一個全域性變數,儲存主執行緒繫結的looper,myLooper()則是獲取當前執行緒繫結的Looper。在prepareMainLooper()中,在主執行緒中建立一個新的Looper,並且繫結主執行緒中,同時把這個主執行緒的looper賦值給sMainLooer這個全域性變數。

ActivityThread.java:

   public static void main(String[] args) {
        ......

        //在android應用程式的入口其實在ActivityThread的main方法
        //在這裡,主執行緒會建立一個Looper物件。
        Looper.prepareMainLooper();

        ......
        ......
        ......        
        //執行訊息迴圈
        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }
}
複製程式碼

在應用程式ActivityThread.main入口中,系統除了呼叫Looper.prepareMainLooper,而且在最後還呼叫了Looper.loop(),這個函式有什麼?大家腦補一下,工廠裡的流水線上,除了有傳送帶外,如果你不讓它動起來,那傳送帶也沒什麼作用,那麼Looper.loop的作用就是讓這個傳送帶動起來,也就是我們的讓我們的訊息佇列動起來。

Looper.java:

/**
 * 呼叫此函式用於啟動訊息佇列迴圈起來,作用相當於工廠流水線中的傳送帶的開關,
 * 只有把開關開啟,傳送帶才跑起來
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    //先進行判斷當前執行緒是否有繫結looper
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
    }
    //獲取這個looper的訊息佇列
    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 (; ; ) {
        Message msg = queue.next(); // might block
        if (msg == null) {
            // No message indicates that the message queue is quitting.
            return;
        }

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

        final long traceTag = me.mTraceTag;
        if (traceTag != 0) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        try {
            //關鍵點,這裡的msg.target也就是hanlder.看回程式碼hanlder.enqueueMessage()
            msg.target.dispatchMessage(msg);
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }

        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
        msg.recycleUnchecked();
    }
}

複製程式碼

這一段程式碼比較長,我們挑有中文註釋的來看,先判斷當前的執行緒是否存在looper,如果存在獲取儲存在Looper的訊息佇列messagequeue,然後無限迴圈這個訊息佇列來獲取message,注意我們留到了一段程式碼:

//關鍵點,這裡的msg.target也就是hanlder.看回程式碼hanlder.enqueueMessage()
msg.target.dispatchMessage(msg);
複製程式碼

msg.target其實就是我們的handler,無論是handler通過post或者sendEmptyMessage,最終都會呼叫到調到這個enqueueMessage(),在這裡會將handler賦值到msg.target中.

Handler.java:

private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) {
    //在message中放一個標記
    msg.target = this;
    if (mAsynchronous) {
        msg.setAsynchronous(true);
    }
    //在這裡把訊息放到佇列裡面去
    return queue.enqueueMessage(msg, uptimeMillis);
}
複製程式碼

既然Looper中的loop()呼叫了msg.target.dispatchMessage,我們就看看Handler的dispatchMessage是如何進行處理這個msg的。

Handler.java:

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    //這裡先判斷callback是否為空
    // callback就是我們使用handler.post(Runnable r)的入參runnable
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        //如果hanlder的入參callback不為空,優先處理
        if (mCallback != null) {
            //如果回撥返回true.則攔截了handler.handleMessage的方法
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        //這就是為什麼我們使用hanlder的時候,需要重寫handleMessage的方法
        handleMessage(msg);
    }
}
複製程式碼

在dispatchMessage函式中,意思就是分發這個訊息,在程式碼中先判斷msg.callback是否為空,msg.callback是什麼?在上一篇文章已詳細介紹過了,其實就是handler.post中的runnable物件,通俗的來說就是handler如果有post操作的,就處理post的操作,我們在看看handlerCallback這個函式。 Handler.java:

private static void handleCallback(Message message) {
    message.callback.run();
}
複製程式碼

很簡單,就一行程式碼,我們看到了熟悉的run方法,這個不就是我們使用post的時候傳進去的Runnbale物件的run方法嗎?

/**
 * 模擬開始
 */
private void doSth() {
    //開啟個執行緒,處理複雜的業務業務
    new Thread(new Runnable() {
        @Override
        public void run() {
            //模擬很複雜的業務,需要1000ms進行操作的業務
            ......
            handler.post(new Runnable() {
                @Override
                public void run() {
                    //在這裡可以更新ui
                    mTv.setText("在這個點我更新了:" + System.currentTimeMillis());
                }
            });
        }
    }).start();
}
複製程式碼

我們回到handler.dispatchMessage(Message)中,如果不是通過post那麼callback就為空,我們看到了一個mCallback變數,我們看看這個Callback的定義:

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

/**
 * Constructor associates this handler with the {@link Looper} for the
 * current thread and takes a callback interface in which you can handle
 * messages.
 * <p>
 * If this thread does not have a looper, this handler won't be able to receive messages
 * so an exception is thrown.
 *
 * @param callback The callback interface in which to handle messages, or null.
 */
public Handler(Callback callback) {
    this(callback, false);
}
複製程式碼

The callback interface in which to handle messages, or null. 我們可以通過實現這個介面,並作為一個引數傳進去Handler來達到處理這個訊息的效果。

Handler.java:

/**
 * Handle system messages here.
 */
public void dispatchMessage(Message msg) {
    //這裡先判斷callback是否為空
    // callback就是我們使用handler.post(Runnable r)的入參runnable
    if (msg.callback != null) {
        handleCallback(msg);
    } else {
        //如果hanlder的入參callback不為空,優先處理
        if (mCallback != null) {
            //如果回撥返回true.則攔截了handler.handleMessage的方法
            if (mCallback.handleMessage(msg)) {
                return;
            }
        }
        //這就是為什麼我們使用hanlder的時候,需要重寫handleMessage的方法
        handleMessage(msg);
    }
}
複製程式碼

最後一行程式碼中,我們看到了熟悉的handleMessage,這不就是我們經常handler.handlerMessage的方法嗎? Demo:

Handler handler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        super.handleMessage(msg);
        //處理訊息
    }
};
複製程式碼

但注意之前我們所看到的,如果我們mCallback.handlerMessage(msg)返回為true的話,這樣就不交給handler.handleMessage處理了。

我們繼續看回來我們的Looper.loop() Looper.java:

/**
 * 呼叫此函式用於啟動訊息佇列迴圈起來,作用相當於工廠流水線中的傳送帶的開關,
 * 只有把開關開啟,傳送帶才跑起來
 * Run the message queue in this thread. Be sure to call
 * {@link #quit()} to end the loop.
 */
public static void loop() {
    .....
    .....
    //迴圈通過訊息佇列來獲取訊息
    for (; ; ) {
        ......
        //最後回收這個message
        msg.recycleUnchecked();
    }
}

複製程式碼

在無限迴圈每個訊息的時候,除了呼叫handler.dispatchMessage,最後還會呼叫msg.recycleUnchecked()進行回收這個訊息,至於message怎麼回收我們就不討論了,詳情的大家可以去看看我上一篇文章。

現在我們回過頭看看我之前說的那2個問題:為什麼子執行緒建立handler會拋異常? 我們先看看Handler的關鍵的構造器:

/**
 * Default constructor associates this handler with the {@link Looper} for the
 * current thread.
 * <p>
 * If this thread does not have a looper, this handler won't be able to receive messages
 * so an exception is thrown.
 */
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
     mLooper = Looper.myLooper();
//如果當前執行緒沒有繫結looper則拋異常
    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;
}
複製程式碼

看構造器可得,如果在子執行緒建立handler,必須在子執行緒中先建立一個Looper物件,不然hanlder在初始化的時候獲取不了當前執行緒的looper,會丟擲異常"Can't create handler inside thread that has not called Looper.prepare()"。

因此,如果我們在子執行緒中建立Handler的時候,我們可以這樣:

new Thread(new Runnable() {
    @Override
    public void run() {
        //在當前子執行緒建立一個Looper物件
        Looper.prepare();
        Handler handler=new Handler();
        //讓訊息佇列動起來
        Looper.loop();
    }
}).start();
複製程式碼

這樣就能成功在子執行緒建立handler。

總結:

1.為什麼在主執行緒中建立Handler不需要我們呼叫Looper.prepare().因為在程式的入口中系統會呼叫Looper.prepareMainLooper()來建立,並且讓其主執行緒的Looper啟動起來。如果我們在子執行緒建立handler,需要手動建立looper並且啟動。

2.每一個執行緒只能存在一個Looper, Looper有一個全域性變數sThreadLocal用來儲存每一個執行緒的looper,通過get、set進行存取looper。

3.Handler可以通過通過post或者sendMessage進行傳送訊息,因為其最終會呼叫sendMessageDelayed,我們可以通過runnable方式或者重寫handleMessage進行訊息的處理,當然如果通過handler.sendMessage(msg)的方式的話,我們可以實現Callback介面達到訊息的處理。

4.為什麼不能在子執行緒更新UI?其實更準確的來說應該是UI只能在建立UI的執行緒中進行更新,也就是主執行緒,如果子執行緒建立UI,其可以在子執行緒進行更新。

注: 網上也有很多文章關於Android訊息機制的文章,大神們都寫得很透徹,但為什麼我還要去寫這樣的文章,一方便想通過讀原始碼來提高個人的學習能力,另一方面也想將自己學到的分享給大家,哪怕多年以後看回自己的部落格,也是別有一番滋味。從14年開始出來做android,看了不少文章,學到了很多東西,我很敬佩他們。特別感謝郭霖、李紀鋼、徐宜生等等,因為他們的無私分享讓很多人在工作上解決了不少難題,因此我也希望我能成為這樣的人。

相關文章