背景介紹
我們在Android開發過程中,幾乎都離不開線程。但是你對線程的了解有多少呢?它完美運行的背後,究竟隱藏了多少不為人知的秘密呢?線程間互通暗語,傳遞信息究竟是如何做到的呢?Looper、Handler、MessageQueue究竟在這背後進行了怎樣的運作。本期,讓我們一起從Thread開始,逐步探尋這個完美的線程鏈背後的秘密。
注意,大部分分析在代碼中,所以請仔細關注代碼哦!
從Tread的創建流程開始
在這一個環節,我們將一起一步步的分析Thread的創建流程。
話不多說,直接代碼裡看。
線程創建的起始點init()
// 創建Thread的公有構造函數,都調用的都是這個私有的init()方法。我們看看到底幹什麼了。
/**
*
* @param 線程組
* @param 就是我們平時接觸最多的Runnable同學
* @param 指定線程的名稱
* @param 指定線程堆棧的大小
*/
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
Thread parent = currentThread(); //先獲取當前運行中的線程。這一個Native函數,暫時不用理會它怎麼做到的。黑盒思想,哈哈!
if (g == null) {
g = parent.getThreadGroup(); //如果沒有指定ThreadGroup,將獲取父線程的TreadGroup
}
g.addUnstarted(); //將ThreadGroup中的就緒線程計數器增加一。注意,此時線程還並沒有被真正加入到ThreadGroup中。
this.group = g; //將Thread實例的group賦值。從這裡開始線程就擁有ThreadGroup了。
this.target = target; //給Thread實例設置Runnable。以後start()的時候執行的就是它了。
this.priority = parent.getPriority(); //設置線程的優先權重為父線程的權重
this.daemon = parent.isDaemon(); //根據父線程是否是守護線程來確定Thread實例是否是守護線程。
setName(name); //設置線程的名稱
init2(parent); //納尼?又一個初始化,參數還是父線程。不急,稍後在看。
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize; //設置線程的堆棧大小
tid = nextThreadID(); //線程的id。這是個靜態變量,調用這個方法會自增,然後作為線程的id。
}
第二個init2()
至此,我們的Thread就初始化完成了,Thread的幾個重要成員變量都賦值了。
啟動線程,開車啦!
通常,我們這樣了啟動一條線程。
Thread threadDemo = new Thread(() -> {
});
threadDemo.start();
那麼start()背後究竟隱藏著什麼樣不可告人的秘密呢?是人性的扭曲?還是道德的淪喪?讓我們一起點進start()。探尋start()背後的秘密。
//如我們所見,這個方法是加了鎖的。原因是避免開發者在其它線程調用同一個Thread實例的這個方法,從而儘量避免拋出異常。
//這個方法之所以能夠執行我們傳入的Runnable裡的run()方法,是應為JVM調用了Thread實例的run()方法。
public synchronized void start() {
//檢查線程狀態是否為0,為0表示是一個新狀態,即還沒被start()過。不為0就拋出異常。
//就是說,我們一個Thread實例,我們只能調用一次start()方法。
if (threadStatus != 0)
throw new IllegalThreadStateException();
//從這裡開始才真正的線程加入到ThreadGroup組裡。再重複一次,前面只是把nUnstartedThreads這個計數器進行了增量,並沒有添加線程。
//同時,當線程啟動了之後,nUnstartedThreads計數器會-1。因為就緒狀態的線程少了一條啊!
group.add(this);
started = false;
try {
nativeCreate(this, stackSize, daemon); //又是個Native方法。這裡交由JVM處理,會調用Thread實例的run()方法。
started = true;
} finally {
try {
if (!started) {
group.threadStartFailed(this); //如果沒有被啟動成功,Thread將會被移除ThreadGroup,同時,nUnstartedThreads計數器又增量1了。
}
} catch (Throwable ignore) {
}
}
}
好吧,最精華的函數是native的,先當黑盒處理吧。只要知道它能夠調用到Thread實例的run()方法就行了。那我們再看看run()方法到底幹了什麼神奇的事呢?
黑實驗
上面的實驗表明了,我們完全可以用Thread來作為Runnable。
幾個常見的線程手段(操作)
Thread.sleep()那不可告人的秘密
我們平時使用Thread.sleep()的頻率也比較高,所以我們在一起研究研究Thread.sleep()被調用的時候發生了什麼。
在開始之前,先介紹一個概念——納秒。1納秒=十億分之一秒。可見用它計時將會非常的精準。但是由於設備限制,這個值有時候並不是那麼準確,但還是比毫秒的控制粒度小很多。
//平時我們調用的Thread.sleep(long)最後調用到這個方法來,後一個陌生一點的參數就是納秒。
//你可以在納秒級控制線程。
public static void sleep(long millis, int nanos)
throws InterruptedException {
//下面三個檢測毫秒和納秒的設置是否合法。
if (millis < 0) {
throw new IllegalArgumentException("millis < 0: " + millis);
}
if (nanos < 0) {
throw new IllegalArgumentException("nanos < 0: " + nanos);
}
if (nanos > 999999) {
throw new IllegalArgumentException("nanos > 999999: " + nanos);
}
if (millis == 0 && nanos == 0) {
if (Thread.interrupted()) { //當睡眠時間為0時,檢測線程是否中斷,並清除線程的中斷狀態標記。這是個Native的方法。
throw new InterruptedException(); //如果線程被設置了中斷狀態為true了(調用Thread.interrupt())。那麼他將拋出異常。如果在catch住這個異常之後return線程,那麼線程就停止了。
//需要注意,在調用了Thread.sleep()之後,再調用isInterrupted()得到的結果永遠是False。別忘了Thread.interrupted()在檢測的同時還會清除標記位置哦!
}
return;
}
long start = System.nanoTime(); //類似System.currentTimeMillis()。但是獲取的是納秒,可能不準。
long duration = (millis * NANOS_PER_MILLI) + nanos;
Object lock = currentThread().lock; //獲得當前線程的鎖。
synchronized (lock) { //對當前線程的鎖對象進行同步操作
while (true) {
sleep(lock, millis, nanos); //這裡又是一個Native的方法,並且也會拋出InterruptedException異常。
//據我估計,調用這個函數睡眠的時長是不確定的。
long now = System.nanoTime();
long elapsed = now - start; //計算線程睡了多久了
if (elapsed >= duration) { //如果當前睡眠時長,已經滿足我們的需求,就退出循環,睡眠結束。
break;
}
duration -= elapsed; //減去已經睡眠的時間,重新計算需要睡眠的時長。
start = now;
millis = duration / NANOS_PER_MILLI; //重新計算毫秒部分
nanos = (int) (duration % NANOS_PER_MILLI); //重新計算微秒部分
}
}
}
通過上面的分析可以知道,使線程休眠的核心方法就是一個Native函數sleep(lock, millis, nanos),並且它休眠的時常是不確定的。因此,Thread.sleep()方法使用了一個循環,每次檢查休眠時長是否滿足需求。
同時,需要注意一點,如果線程的interruted狀態在調用sleep()方法時被設置為true,那麼在開始休眠循環前會拋出InterruptedException異常。
Thread.yield()究竟隱藏了什麼?
這個方法是Native的。調用這個方法可以提示cpu,當前線程將放棄目前cpu的使用權,和其它線程重新一起爭奪新的cpu使用權限。當前線程可能再次獲得執行,也可能沒獲得。就醬。
無處不在的wait()究竟是什麼?
大家一定經常見到,不論是哪一個對象的實例,都會在最下面出現幾個名為wait()的方法。等待?它們究竟是怎樣的一種存在,讓我們一起點擊去看看。
哎喲我去,都是Native函數啊。
那就看看文檔它到底是什麼吧。
根據文檔的描述,wait()配合notify()和notifyAll()能夠實現線程間通訊,即同步。在線程中調用wait()必須在同步代碼塊中調用,否則會拋出IllegalMonitorStateException異常。因為wait()函數需要釋放相應對象的鎖。當線程執行到wait()時,對象會把當前線程放入自己的線程池中,並且釋放鎖,然後阻塞在這個地方。直到該對象調用了notify()或者notifyAll()後,該線程才能重新獲得,或者有可能獲得對象的鎖,然後繼續執行後面的語句。
呃。。。好吧,在說明一下notify()和notifyAll()的區別。
扒一扒Looper、Handler、MessageQueue之間的愛恨情仇
我們可能過去都寫過形如這樣的代碼:
很多同學知道,在線程中使用Handler時(除了Android主線程)必須把它放在Looper.prepare()和Looper.loop()之間。否則會拋出RuntimeException異常。但是為什麼要這麼做呢?下面我們一起來扒一扒這其中的內幕。
從Looper.prepare()開始
當Looper.prepare()被調用時,發生了什麼?
經過上面的分析,我們已經知道Looper.prepare()調用之後發生了什麼。
但是問題來了!sThreadLocal是個靜態的ThreadLocal 實例(在Android中ThreadLocal的範型固定為Looper)。就是說,當前進程中的所有線程都共享這一個ThreadLocal。那麼,Looper.prepare()既然是個靜態方法,Looper是如何確定現在應該和哪一個線程建立綁定關係的呢?我們接著往裡扒。
來看看ThreadLocal的get()、set()方法。
創建Handler
Handler可以用來實現線程間的通行。在Android中我們在子線程作完數據處理工作時,就常常需要通過Handler來通知主線程更新UI。平時我們都使用new Handler()來在一個線程中創建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());
}
}
mLooper = Looper.myLooper(); //重點啊!在這裡Handler和當前Thread的Looper綁定了。Looper.myLooper()就是從ThreadLocale中取出當前線程的Looper。
if (mLooper == null) {
//如果子線程中new Handler()之前沒有調用Looper.prepare(),那麼當前線程的Looper就還沒創建。就會拋出這個異常。
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
mQueue = mLooper.mQueue; //賦值Looper的MessageQueue給Handler。
mCallback = callback;
mAsynchronous = async;
}
Looper.loop()
我們都知道,在Handler創建之後,還需要調用一下Looper.loop(),不然發送消息到Handler沒有用!接下來,扒一扒Looper究竟有什麼樣的魔力,能夠把消息準確的送到Handler中處理。
public static void loop() {
final Looper me = myLooper(); //這個方法前面已經提到過了,就是獲取到當前線程中的Looper對象。
if (me == null) {
//沒有Looper.prepare()是要報錯的!
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
final MessageQueue queue = me.mQueue; //獲取到Looper的MessageQueue成員變量,這是在Looper創建的時候new的。
//這是個Native方法,作用就是檢測一下當前線程是否屬於當前進程。並且會持續跟蹤其真實的身份。
//在IPC機制中,這個方法用來清除IPCThreadState的pid和uid信息。並且返回一個身份,便於使用restoreCallingIdentity()來恢復。
Binder.clearCallingIdentity();
final long ident = Binder.clearCallingIdentity();
for (;;) { //重點(敲黑板)!這裡是個死循環,一直等待抽取消息、發送消息。
Message msg = queue.next(); // 從MessageQueue中抽取一條消息。至於怎麼取的,我們稍後再看。
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; //取得MessageQueue的跟蹤標記
if (traceTag != 0) {
Trace.traceBegin(traceTag, msg.target.getTraceName(msg)); //開始跟蹤本線程的MessageQueue中的當前消息,是Native的方法。
}
try {
msg.target.dispatchMessage(msg); //嘗試分派消息到和Message綁定的Handler中
} finally {
if (traceTag != 0) {
Trace.traceEnd(traceTag); //這個和Trace.traceBegin()配套使用。
}
}
if (logging != null) {
logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
}
final long newIdent = Binder.clearCallingIdentity(); //what?又調用這個Native方法了。這裡主要是為了再次驗證,線程所在的進程是否發生改變。
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(); //回收釋放消息。
}
}
從上面的分析可以知道,當調用了Looper.loop()之後,線程就就會被一個for(;;)死循環阻塞,每次等待MessageQueue的next()方法取出一條Message才開始往下繼續執行。然後通過Message獲取到相應的Handler (就是target成員變量),Handler再通過dispatchMessage()方法,把Message派發到handleMessage()中處理。
這裡需要注意,當線程loop起來是時,線程就一直在循環中。就是說Looper.loop()後面的代碼就不能被執行了。想要執行,需要先退出loop。
Looper myLooper = Looper.myLoop();
myLooper.quit(); //普通退出方式。
myLooper.quitSafely(); //安全的退出方式。
現在又產生一個疑問,MessageQueue的next()方法是如何阻塞住線程的呢?接下來,扒一扒這個幕後黑手MessageQueue。
幕後黑手MessageQueue
MessageQueue是一個用單鏈的數據結構來維護消息列表。
可以看到。MessageQueue在取消息(調用next())時,會進入一個死循環,直到取出一條Message返回。這就是為什麼Looper.loop()會在queue.next()處等待的原因。
那麼,一條Message是如何添加到MessageQueue中呢?要弄明白最後的真相,我們需要調查一下mHandler.post()這個方法。
Handler究竟對Message做了什麼?
Handler的post()系列方法,最終調用的都是下面這個方法:
接下來就看看MessageQueue的enqueueMessage()作了什麼。
至此,我們已經揭露了Looper、Handler、MessageQueue隱藏的秘密。
另一個疑問?
也許你已經注意到在主線程中可以直接使用Handler,而不需要Looper.prepare()和Looper.loop()。為什麼可以做到這樣呢?根據之前的分析可以知道,主線程中必然存在Looper.prepare()和Looper.loop()。既然如此,為什麼主線程沒有被loop()阻塞呢?看一下ActivityThread來弄清楚到底是怎麼回事。
注意ActivityThread並沒有繼承Thread,它的Handler是繼承Handler的私有內部類H.class。在H.class的handleMessage()中,它接受並執行主線程中的各種生命周期狀態消息。UI的16ms的繪製也是通過Handler來實現的。也就是說,主線程中的所有操作都是在Looper.prepareMainLooper()和Looper.loop()之間進行的。進一步說是在主Handler中進行的。
總結
Android中Thread在創建時進行初始化,會使用當前線程作為父線程,並繼承它的一些配置。
Thread初始化時會被添加到指定/父線程的ThreadGroup中進行管理。
Thread正真啟動是一個native函數完成的。
在Android的線程間通信中,需要先創建Looper,就是調用Looper.prepare()。這個過程中會自動依賴當前Thread,並且創建MessageQueue。經過上一步,就可以創建Handler了,默認情況下,Handler會自動依賴當前線程的Looper,從而依賴相應的MessageQueue,也就知道該把消息放在哪個地方了。MessageQueue通過Message.next實現了一個單鍊表結構來緩存Message。消息需要送達Handler處理,還必須調用Looper.loop()啟動線程的消息泵送循環。loop()內部是無限循環,阻塞在MessageQueue的next()方法上,因為next()方法內部也是一個無限循環,直到成功從鍊表中抽取一條消息返回為止。然後,在loop()方法中繼續進行處理,主要就是把消息派送到目標Handler中。接著進入下一次循環,等待下一條消息。由於這個機制,線程就相當於阻塞在loop()這了。
經過上面的揭露,我們已經對線程及其相互之間通訊的秘密有所了解。掌握了這些以後,相信在以後的開發過程中我們可以思路清晰的進行線程的使用,並且能夠吸收Android在設計過程中的精華思想。
感謝 CoorChice 同學投稿,Blog地址:
https://chenbingx.github.io/
本篇文章如有對您開發有幫助的話,歡迎在作者的 Github 給個Star 也可以分享給小夥伴哦; 小編每天都兢兢業業的為整理乾貨,支持小編在下方給鼓勵+1,需要投稿與及有疑問的小夥伴可以在下方留言,小編會第一時間與您聯繫!
基於 Material Design 的 Gank IO 客戶端(內含妹子圖)
玩轉仿探探卡片式滑動效果
程式設計師也要失業了?微軟發明了要取代他們的AI【Bus Weekly】43期
英語不好?是時候打造一款AndroidStudio中記單詞的翻譯插件了