Android 源碼分析(二)handler 機制

深刻淺出 Handler

此次我本身不折不扣弄懂 handler 機制了,真的,不信我講給你聽。java

從哪裏講起呢,我特地去翻了一下 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 我相信你們開發中確定都用過。沒用過的出門左拐~~less

通常咱們用 Handler 都是用來作線程切換,可能說到這裏,有同窗會想起一句話「子線程不能修改 ui,主線程不能作耗時操做」,沒錯,handler 的使用場景大多都是在異步任務中須要修改 ui。然並卵,這個咱們都知道,可是並不能讓我完全理解 handler 的機制。異步

好了,不扯犢子了,耽誤你們的時間。async

先來看一個錯誤的示範。ide

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

根據你們的經驗求解,以上代碼可否運行經過?爲何oop

思考一分鐘再看答案。源碼分析

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

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 消息機制的講解及源碼分析就到這裏咯。

相關文章
相關標籤/搜索