Android 原始碼分析(二)handler 機制

diamond_lin發表於2019-02-27

深入淺出 Handler

這次我自己徹徹底底弄懂 handler 機制了,真的,不信我講給你聽。

從哪裡講起呢,我特意去翻了一下 Handler 的類註釋說明,然而好像並沒有 get 到我想講的東西,粗略看一下類註釋。

A Handler allows you to send and process {@link Message} and Runnable objects associated with a thread`s {@link MessageQueue}. Each Handler instance is associated with a single thread and that thread`s message queue. When you create a new Handler, it is bound to the thread / message queue of the thread that is creating it — from that point on, it will deliver messages and runnables to that message queue and execute them as they come out of the message queue.

沒看懂沒事,反正我看了翻譯也不想懂,我們換個角度來理解 handler。

Handler 我相信大家開發中肯定都用過。沒用過的出門左拐~~

一般我們用 Handler 都是用來做執行緒切換,可能說到這裡,有同學會想起一句話“子執行緒不能修改 ui,主執行緒不能做耗時操作”,沒錯,handler 的使用場景大多都是在非同步任務中需要修改 ui。然並卵,這個我們都知道,但是並不能讓我徹底理解 handler 的機制。

好了,不扯犢子了,耽誤大家的時間。

先來看一個錯誤的示範。

new Thread(new Runnable() {
	@Override
	public void run() {
		Handler handler = new Handler(){
			@Override
			public void handleMessage(Message msg) {
				Toast.makeText(MainActivity.this,"lalala",0).show();
			}
		};
		handler.sendEmptyMessage(0);
	}
}).start();
複製程式碼

根據大家的經驗求解,以上程式碼能否執行通過?為什麼

思考一分鐘再看答案。

好了,思考結束,我貼執行結果了。

java.lang.RuntimeException: Can`t create handler inside thread that has not called Looper.prepare()
//報錯行是 Handler handler = new Handler(){
複製程式碼

Why?Why?Why?稍後我再給大家解釋。

這時有經驗的同學會說,子執行緒在建立 Handler 之前,需要先呼叫Looper.prepare();

那麼,我們來看一下Looper$prepare 方法吧。

static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
public static void prepare() {
	prepare(true);
}

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

這個方法很簡單,如果sThreadLocal.get() != null則拋異常,不然就執行sThreadLocal.set(new Looper(quitAllowed));建立一個 Looper,並且賦值給sThreadLocal。

Looper 的構造方法很簡單,就儲存了當前執行緒物件、然後建立了一個MessageQueue 物件,MessageQueue我們稍後再介紹。

可能有些同學不知道 ThreadLocal(我之前也不知道),偷懶的同學可以直接把這個類丟到百度上一搜就知道了,ThreadLocal 解決多執行緒程式的併發問題提供了一種新的思路。說的接地氣一點,就是不同的執行緒從 ThreadLocal 能取出自己獨有的資料,泛型 T 則是ThreadLocal裡面取出來的資料型別。就是執行緒1呼叫ThreadLocal.set存了個物件 a,執行緒2再呼叫 ThreadLocal.get 方法是取不到資料的,只有執行緒1呼叫ThreadLocal.get方法才能取到這個資料。

ThreadLocal 在多執行緒篇好像沒有講,但是沒關係,我們有紮實的 java 基礎,如果讓我們自己手動實現一個ThreadLocal,也不過就半個小時的事。我的實現思路:基於 HashMap 做實現,key 是執行緒 id,vaule 是執行緒對應的值,然後建立一個 MyThreadLocal 來管理這個HashMap即可。

好,扯遠了。Looper.prepare()就是給當前執行緒建立了一個Looper物件,存在了靜態變數sThreadLocal裡面。

然後我們再來看看 Handler 的構造方法,看看為什麼沒呼叫Looper.prepare()的情況下直接new 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());
		}
	}

	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.myLooper()分析這個 mLooper為什麼會為 null。

public static @Nullable Looper myLooper() {
	return sThreadLocal.get();
}
複製程式碼

噢,不說了,大家都看得懂。到這裡,我們解決了剛剛那個 demo 為什麼會丟擲異常的原因。得出了一個結論

  • 在子執行緒中建立 Handler 的時候必須先呼叫 Looper.prepare()方法。

但是?這個結論有什麼卵用?別急,接著往下看。

new Thread(new Runnable() {
	@Override
	public void run() {
		Looper.prepare();
		Handler handler = new Handler(){
			@Override
			public void handleMessage(Message msg) {
				Toast.makeText(MainActivity.this,"lalala",0).show();
			}
		};
		handler.sendEmptyMessage(0);
	}
}).start();
複製程式碼

然後,我們加上了Looper.prepare();,又執行了一遍程式碼,同學們思考一下這次能否正常執行並且彈出 Toast。

333

22

1

好了,我來告訴大家執行結果,執行結果就是沒有任何結果,不報錯,也沒有任何響應,debug 發現 handleMessage方法並沒有被回撥。我們只好去看handler.sendEmptyMessage(0);是否有將訊息發出去。

通過閱讀 Handler 的原始碼,我們發現,Handler 不管是呼叫postDelayed、sendEmptyMessage、post 等各種方法,最終都會呼叫enqueueMessage方法,我們來看看這個方法。

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

其中MessageQueue 是構造方法的時候new 的,Message 是根據傳參建立的,uptimeMillis 則是一個訊息處理時間戳,用於判斷訊息是立即處理還是稍後處理。

看到這裡,還是沒看到為什麼 handler 發了訊息沒有回撥 handleMessage 方法。

那就接著看 queue.enqueueMessage 吧

enqueueMessage ,顧名思義,就是資訊入棧嘛,根據單一職能原則,這裡大概不會找到為什麼沒有回撥 handleMessage 的原因,但是我們還是來看一下吧。

boolean enqueueMessage(Message msg, long when) {
    if (msg.target == null) {
        throw new IllegalArgumentException("Message must have a target.");
    }
    if (msg.isInUse()) {
        throw new IllegalStateException(msg + " This message is already in use.");
    }

    synchronized (this) {
        if (mQuitting) {
            IllegalStateException e = new IllegalStateException(
                    msg.target + " sending message to a Handler on a dead thread");
            Log.w(TAG, e.getMessage(), e);
            msg.recycle();
            return false;
        }

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

這裡是 Message 的入棧操作,也就是把 Message 存到 MessageQueue 裡面,具體實現大家可以不用糾結細節,我給大家簡單講解一下:MessageQueue 裡面維護的是一個雙向連結串列,enqueueMessage 方法根據引數 when,決定 Message 查到連結串列的哪個位置。簡單的說MessageQueue 就是一個集合,維護 Handler 訊息的專屬集合(雖然沒有繼承集合介面,但是資料結構是連結串列呀)。

到了這裡,還是沒找到為什麼 handleMessage 方法沒被回撥的原因。
思考一下,很多同學肯定都知道 handler 是一個訊息輪詢機制,一條訊息只有被處理的時候才會呼叫 handleMessage,而訊息是儲存在 Message 裡面,Message 由MessageQueue 維護著,我們要處理訊息,必須從 MessageQueue 去取。剛剛我們找到了MessageQueue 的新增資訊的方法,那麼肯定有訊息被處理的時候需要出棧的操作,據此,我們在MessageQueue 裡面找到了 next()方法,用於訊息的出棧,那麼只需要找到 next 在哪被呼叫就知道了。

於是,又是一番尋找。在 Looper.loop()方法裡面找到了MessageQueue 的 next 方法呼叫。剛剛我們在建立 Looper 的時候,構造方法就new 了MessageQueue物件。

public static void loop() {
    final Looper me = myLooper();
    if (me == null) {
        throw new RuntimeException("No Looper; Looper.prepare() wasn`t called on this thread.");
    }
    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 slowDispatchThresholdMs = me.mSlowDispatchThresholdMs;

        final long traceTag = me.mTraceTag;
        if (traceTag != 0 && Trace.isTagEnabled(traceTag)) {
            Trace.traceBegin(traceTag, msg.target.getTraceName(msg));
        }
        final long start = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        final long end;
        try {
            msg.target.dispatchMessage(msg);
            end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis();
        } finally {
            if (traceTag != 0) {
                Trace.traceEnd(traceTag);
            }
        }
        if (slowDispatchThresholdMs > 0) {
            final long time = end - start;
            if (time > slowDispatchThresholdMs) {
                Slog.w(TAG, "Dispatch took " + time + "ms on "
                        + Thread.currentThread().getName() + ", h=" +
                        msg.target + " cb=" + msg.callback + " msg=" + msg.what);
            }
        }

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

        msg.recycleUnchecked();
    }
}
複製程式碼

這個方法比較長,我給大家簡單解釋一下。
首先這是一個靜態方法,通過靜態方法 myLooper()獲取當前執行緒的 Looper 物件,然後取出Looper 裡面的 MessageQueue,然後就走了死迴圈,不斷的取出 MessageQueue 裡面的 Message 進行消費。很多同學都知道 Android 的主執行緒就是一個死迴圈,這裡不扯遠了。

我們可以找到 msg.target.dispatchMessage(msg);這樣一行程式碼,我們看一下 handler 的這個方法:

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

這裡就很簡單了,根據狀態,決定呼叫那個方法處理訊息。我們可以輕易判斷出這裡呼叫的就是handleMessage。然後我們在思考一下,這個 handler 是誰,在那裡建立的。

前面我們在 handler 的enqueueMessage()方法裡面msg.target = this;把 handler 本身賦值給了 Message,所以msg.target.dispatchMessage(msg) 實際上呼叫的就是handler.sendEmptyMessage(0);這個 handler 本身,所以這裡也是沒毛病的。

到這裡,現在就只差 Looper.loop()方法沒被呼叫了,那麼我們手動呼叫一下試試?

然後有了如下程式碼:

new Thread(new Runnable() {
	@Override
	public void run() {
		Looper.prepare();
		Looper.loop();
		Handler handler = new Handler(){
			@Override
			public void handleMessage(Message msg) {
				Toast.makeText(MainActivity.this,"lalala",0).show();
			}
		};
		handler.sendEmptyMessage(0);
	}
}).start();
複製程式碼

然而,還是不行。同學們再思考一下原因?

333

22

1

好了,不逗大家了,Looper.loop();開啟了一個死迴圈,子執行緒執行到這行程式碼就在死迴圈,後面的程式碼就不會往下走了。把這行程式碼移到 handler.sendEmptyMessage(0)後面即可。

還沒講完

一直沒用過標題,怕你們看著累,我加個標題吧。
上面的這些講解,我們大概瞭解到了 handler 的工作機制。我給大家回顧一下。

1.呼叫 Looper.prepare();給當前執行緒建立一個 Looper,存在 Looper 的靜態變數ThreadLocal裡面。且這個方法在同一個執行緒只能呼叫一次,保證了一執行緒對應一個 Looper 。

2.Looper 的構造方法建立了MessageQueue 物件,所以Looper和 MessageQueue
也是一對一的關係。

3.Looper.loop()根據當前執行緒,獲取到 Looper 物件,然後死迴圈MessageQueue的訊息。

4.Handler 裡面有個mLooper物件,預設賦值是 Looper.myLooper();

5.Handler 發生訊息,只是將一個 Message 丟給 Handler 的成員變數mLooper裡面的 MessageQueue 裡面去。然後由Handler 裡面的 mLooper 消費掉(前期是mLooper已經呼叫了loop 方法 開啟死迴圈)。

大致就是醬紫吧。

再接著挖坑了,還是剛剛那個例子。

new Thread(new Runnable() {
	@Override
	public void run() {
		Looper.prepare();
		Looper.loop();
		Handler handler = new Handler(){
			@Override
			public void handleMessage(Message msg) {
				mBt.setText("asasA");
			}
		};
		handler.sendEmptyMessage(0);
	}
}).start();
複製程式碼

同學們思考一下,這次程式碼能否正常執行。

333
22
1

好了,不給大家看執行錯誤日誌了,看到這裡,相信每次都認真思考過的同學應該知道報錯原因了,沒想出來也沒關係,我們再來回顧一遍。

上面的分析中,主要牽涉到以下幾個類。

Message

一個訊息 bean。

MessageQueue

訊息佇列,可以當成是一個集合,但是資料結構是雙向連結串列,只給 Looper 用,和 Looper 是一對一的關係。

Looper

執行緒可以沒開啟 Looper,但是最多隻能開啟一個,Looper 在構造方法裡面建立一個 MessageQueue,在 loop()方法裡面開啟死迴圈不斷從 MessageQueue 取Message,Message 訊息在迴圈裡面由Message 持有的 handler$handleMessage 方法處理。

Handler

在構造方法裡面會繫結一個 Looper,預設繫結當前執行緒的 Looper,也可以指定一個 Looper 繫結。然後當 Handler 傳送一個訊息的時候,就把這個訊息創封裝成一個 Message,傳送到繫結 Looper 的 MessageQueue 裡面去,再被 Looper$loop 開啟的死迴圈消費掉。

好像講完了?

上面的報錯就是我們熟悉的“子執行緒不能修改 ui”的錯,是由 ViewRootImpl 檢測丟擲的異常,這個不屬於handler 的內容,所以我們在建立 Handler 的時候指定 handler 繫結主執行緒 Looper 即可。

好了,Handler 應該已經講清楚了吧,有點像生產者消費者模型,哈哈哈哈哈~~

來,思考一下,誰是生產者,誰是消費者。

哦,對了,漏了幾個知識點。

補充幾個知識點

主執行緒Looper 問題

為什麼在主執行緒建立的 handler,可以在子執行緒handleMessage 修改 ui,而子執行緒卻不可以呢?
這個問題在上面的分析過程已經講過了,handler 的建立預設是繫結當前執行緒的 Looper,你在子執行緒建立 handler 的時候指定 handler 繫結 主執行緒的 Looper 即可,程式碼是

然後主執行緒的Looper 是在哪裡建立的呢?

我們都知道 Activity 的啟動是從 ActivityThread 的 main 方法開始的(不知道別急,關注我,後面我會分析 Activity 的啟動過程的),在 main 方法的結尾有這麼幾行程式碼。

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

好了,我們的主執行緒要開啟訊息迴圈機制,也是需要呼叫 Looper.loop()的,在 ActivityThread 裡面幫我們做了而已。

為什麼主執行緒執行 looper 的死迴圈不會 ANR,而主執行緒做耗時操作,就會 ANR

先解釋一下 ANR(Application not Response)應用無響應,單詞應該沒拼錯。

剛剛我們已經知道主執行緒的迴圈是在不斷死迴圈去處理 MessageQueue 裡面的訊息,但是 MessageQueue 不僅僅是我們手動建立的 Handler 去往裡面生產訊息,更多的是各種系統的訊息,比如說?UI 的重新整理,應該也是在這裡面處理的(我猜測,後期研究 View 原始碼的時候再驗證哦,但是那傳說中的16ms 重新整理一次的螢幕,肯定跟這個有關係)。所以,我們在 Activity 主執行緒的某個方法裡面做了耗時操作,會影響 MessageQueue 裡面下一個 Message 的執行,如果下一個 Message 正好是重新整理View。其實 CPU 執行效率很高,一秒鐘能處理很多很多 message,比如說有100個,那麼耗時操作1秒鐘,就會導致後面100個message 的處理被滯後,這就造成了介面卡頓。

Activity 的 runOnUiThread 方法怎樣切換執行緒的

這個就簡單了,點進去看 Activity 對這個方法的實現。

final Handler mHandler = new Handler();
public final void runOnUiThread(Runnable action) {
    if (Thread.currentThread() != mUiThread) {
        mHandler.post(action);
    } else {
        action.run();
    }
}
複製程式碼

沒什麼好說的了,過!下一題

View 的 post 方法

public boolean postDelayed(Runnable action, long delayMillis) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null) {
        return attachInfo.mHandler.postDelayed(action, delayMillis);
    }

    // Postpone the runnable until we know on which thread it needs to run.
    // Assume that the runnable will be successfully placed after attach.
    getRunQueue().postDelayed(action, delayMillis);
    return true;
}
複製程式碼

咳咳,這個,我暫時也解釋不好,但是我們能看到,如果 View 已經被顯示到 Window 之後,會呼叫 handler 來處理這個 Runnable。沒辦法,我只好通過 debug 的方式,來跟你們證明這裡也是把訊息放到了主執行緒的 MessageQueue 裡面去了。attachInfo.mHandler.mLooper 是main looper,對應的是 main 執行緒。

好了,Handler 訊息機制的講解及原始碼分析就到這裡咯。

相關文章