驚天秘密!從Thread開始,揭露Android線程通訊的詭計和主線程的陰謀
背景介紹
我們在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()的區別。
- notify()
調用notify()后,對象會從自己的線程池中(也就是對該對象調用了wait()函數的線程)隨機挑選一條線程去喚醒它。也就是一次只能喚醒一條線程。如果在多線程情況下,只調用一次notify(),那么只有一條線程能被喚醒,其它線程會一直在
- notifyAll()
調用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在設計過程中的精華思想。