第22章 物件共享,避免建立多物件——享元模式

detachment_w發表於2021-01-03

22.1 享元模式介紹

享元模式是物件池的一種實現,它的英文名稱叫做Flyweight,代表輕量級的意思。享元模式用來儘可能減少記憶體使用量,它適合用於可能存在大量重複物件的場景,來快取可共享的物件,達到物件共享、避免建立過多物件的效果,這樣一來就可以提升效能、避免記憶體移除等。

享元物件中的部分狀態是可以共享,可以共享的狀態成為內部狀態,內部狀態不會隨著環境變化;不可共享的狀態則稱為外部狀態,它們會隨著環境的改變而改變在享元模式中會建立一個物件容器,在經典的享元模式中該容器為一個Map,它的鍵是享元物件的內部狀態,它的值就是享元物件本身。客戶端程式通過這個內部狀態從享元工廠中獲取享元物件,如果有快取則使用快取物件,否則建立一個享元物件並且存入容器中,這樣一來就避免了建立過多物件的問題

22.2 享元模式的定義

使用共享物件可有效地支援大量的細粒度的物件。

22.3 享元模式的使用場景

  1. 系統中存在大量的相似物件。
  2. 細粒度的物件都具備較接近的外部狀態,而且內部狀態與環境無關,也就是說物件沒有特定身份。
  3. 需要緩衝池的場景。

22.4 享元模式的UML類圖

UML類圖如圖22-1所示。
在這裡插入圖片描述
角色介紹

  • Flyweight:享元物件抽象基類或者介面。
  • ConcreateFlyweight:具體的享元物件。
  • FlyweightFactory:享元工廠,負責管理享元物件池和建立享元物件。

22.5 享元模式的簡單示例

過年回家買火車票是一件很困難的事,無數人用刷票軟體向服務端發出請求,對於每一個請求伺服器都必須做出應答。在使用者設定好出發地和目的地之後,每次請求都返回一個查詢的車票結果。為了便於理解,我們假設每次返回的只有一躺列車的車票。那麼當數以萬計的人不間斷在請求資料時,如果每次都重新建立一個查詢的車票結果,那麼必然會造成大量重複物件的建立、銷燬,使得GC任務繁重、記憶體佔用率高居不下。而這類問題通過享元模式就能夠得到很好地改善,從城市A到城市B的車輛是有限的,車上的鋪位也就是軟臥、硬臥、坐票3種。我們將這些可以公用的物件快取起來,在使用者查詢時優先使用快取,如果沒有快取則重新建立。這樣就將成千上萬的物件變為了可選擇的有限數量。

首先我們建立一個Ticket介面,該介面定義展示車票資訊的函式,具體程式碼如下。
在這裡插入圖片描述
它的一個具體的實現類是TrainTicket類,具體程式碼如下。
在這裡插入圖片描述
在這裡插入圖片描述
資料庫中表示火車票的資訊有出發地、目的地、鋪位、價格等欄位,在購票使用者每次查詢時如果沒有用某種快取模式,那麼返回車票資料的介面實現如下。
在這裡插入圖片描述
在TicketFactory的getTicket函式中每次會new一個TrainTicket物件,也就是說如果在短時間內有10000萬使用者求購北京到青島的車票,那麼北京到青島的車票物件就會被建立10000次,當資料返回之後這些物件變得無用了又會被虛擬機器回收。此時就會造成大量的重複物件存在記憶體中,GC對這些物件的回收也會非常消耗資源。如果使用者的請求量很大可能導致系統變得極其緩慢,甚至可能導致OOM。

正如上文所說,享元模式通過訊息池的形式有效地減少了重複物件的存在。它通過內部狀態標識某個種類的物件,外部程式根據這個不會變化的內部狀態從訊息池中取出物件。使得同一類物件可以被複用,避免大量重複物件。

使用享元模式很簡單,只需要簡單地改造一下TicketFactory,具體程式碼如下。
在這裡插入圖片描述
我們在TicketFactory中新增了一個map容器,並且以出發地+“-”+目的地為鍵、以車票物件作為值儲存車票物件。這個map的鍵就是我們說的內部狀態,在這裡就是出發地、橫槓、目的地拼接起來的字串,如果沒有快取則建立一個物件,並且將這個物件快取到map中,下次再有這類請求時則直接從快取中獲取。這樣即使有10000個請求北京到青島的車票資訊,那麼出發地是北京、目的地是青島的車票物件只有一個。這樣就從這個物件從10000減到了1個,避免了大量的記憶體佔用及頻繁的GC操作。簡單實現程式碼如下。
在這裡插入圖片描述
在這裡插入圖片描述
執行結果:
在這裡插入圖片描述
從輸出結果可以看到,只有第一次查詢車票時建立了一次物件,後續的查詢都使用的是訊息池中的物件。這其實就是相當於一個物件快取,避免了物件的重複建立與回收。在這個例子中,內部狀態就是出發地和目的地,內部狀態不會發生變化;外部狀態就是鋪位和價格,價格會隨著鋪位的變化而變化。

在JDK中String也是類似訊息池,我們知道在Java中String是存在於常量池中。也就是說一個String被定義之後它就被快取到了常量池中,當其他地方要使用同樣的字串時,則直接使用的是快取,而不會重複建立。例如下面這段程式碼。
在這裡插入圖片描述
輸出如下:
在這裡插入圖片描述
在前3個通過equals函式判定中,由於它們的字元值都相等,因此3個判等都為true,因此,String的equals 只根據字元值進行判斷。而在後4個判斷中則使用的是兩個等號判斷,兩個等號判斷代表的意思是判定這兩個物件是否相等,也就是兩個物件指向的記憶體地址是否相等。由於str1和str3都是通過new構建的,而str2則是通過字面值賦值的,因此這3個判定都為false,因為它們並不是同一個物件。而str2和str4都是通過字面值賦值的,也就是直接通過雙引號設定的字串值,因此,最後一個通過“=”判定的值為true,也就是說str2和str4是同一個字串物件。因為str4使用了快取在常量池中的str2物件。這就是享元模式在我們開發中的一個重要案例。

22.6 Android原始碼中的享元模式

在用Android開發了一段時間之後,很多讀者就應該知道了一個知識點:UI不能夠在子執行緒中更新。這原本就是一個偽命題,因為並不是UI不可以在子執行緒更新,而是UI不可以在不是它的建立執行緒裡進行更新。只是絕大多數情況下UI都是從UI執行緒中建立的,因此,在其他執行緒更新時會丟擲異常。在這種情況下,當我們在子執行緒完成了耗時操作之後,通常會通過一個Handler將結果傳遞給UI執行緒,然後在UI執行緒中更新相關的檢視。程式碼大致如下。
在這裡插入圖片描述
在MainActivity中首先建立了一個Handler物件,它的Looper就是UI執行緒的Looper。在子執行緒執行完耗時操作之後,則通過Handler向UI執行緒傳遞一個Runnable,即這個Runnable執行在UI執行緒中,然後在這個Runnable中更新UI即可。

那麼Handler、Looper的工作原理又是什麼呢?它們之間是如何協作的?

在講此之前我們還需要了解兩個概念,即Message和MessageQueue。其實Android應用是事件驅動的,每個事件都會轉化為一個系統訊息,即Message。訊息中包含了事件相關的資訊以及這個訊息的處理人 Handler。每個程式中都有一個預設的訊息佇列,也就是我們的MessageQueue,這個訊息佇列維護了一個待處理的訊息列表,有一個訊息迴圈不斷地從這個佇列中取出訊息、處理訊息,這樣就使得應用動態地運作起來。它們的運作原理就像工廠的生產線一樣,待加工的產品就是Message,"傳送帶”就是MessageQueue,工人們就對應處理事件的Handler。這麼一來Message就必然會產生很多物件,因為整個應用都是由事件,也就是Message來驅動的,系統需要不斷地產生Message、處理Message、銷燬Message,難道Android沒有iOS流暢就是這個原因嗎?答案顯然沒有那麼簡單,重複構建大量的Message也不是Android的實現方式。那麼我們先從Handler傳送訊息開始一步一步學習它的原理。

就用上面的例子來說,我們通過Handler傳遞了一個Runnable給UI執行緒。實際上Runnable會被包裝到一個Message物件中,然後再投遞到UI執行緒的訊息佇列。我們看看Handler的post(Runnable run)函式。
在這裡插入圖片描述
在post函式中會呼叫sendMessageDelayed函式,但在此之前呼叫了getPostMessage將Runnable包裝到一個Message物件中。然後再將這個Message物件傳遞給sendMessageDelayed函式,具體程式碼如下。
在這裡插入圖片描述
sendMessageDelayed函式最終又呼叫了sendMessageAtTime函式,我們知道,post訊息時是可以延時釋出的,因此,有一個delay的時間引數。在sendMessageAtTime函式中會判斷當前Handler的訊息佇列是否為空,如果不為空那麼就會將該訊息追加到訊息佇列中。又因為我們的Handler在建立時就關聯了UI執行緒的Looper(如果不手動傳遞Looper那麼Handler持有的Looper就是當前執行緒的Looper,也就是說在哪個執行緒建立的Handler,就是哪個執行緒的Looper),Handler從這個Looper中獲取訊息佇列,這樣一來Runnable就會被放到UI執行緒的訊息佇列了,因此,我們的Runnable在後續的某個時刻就會被執行在UI執行緒中。

這裡我們不需要再深究Handler、Looper等角色的運作細節,我們這裡關注的是享元模式的運用。在上面的getPostMessage中會將Runnable包裝為一個Message,在前文沒有說過,系統並不會構建大量的Message物件,那麼它是如何處理的呢?

我們看到在getPostMessage中的Message物件是從一個Message.obtain()函式返回的,並不是使用new來實現,如果使用new那麼就是我們起初猜測的會構建大量的Message物件,當然到目前還不能下結論,我們看看Message.obtain()的實現。
在這裡插入圖片描述
實現很簡單,但是有一個很引人注意的關鍵詞——Pool,它的中文意思稱為池,難道是我們前文所說的共享物件池?目前我們依然不能確定,但是,此時已經看到了一些重要線索。現在就來看看obtain中的sPoolSync、sPool裡是些什麼程式。
在這裡插入圖片描述
首先Message文件第一段的意思就是介紹了一下這個Message類的欄位,以及說明Message物件是被髮送到Handler的,對於我們來說作用不大。第二段的意思是建議我們使用Message的obtain方法獲取Message物件,而不是通過Message的建構函式,因為obtain方法會從被回收的物件池中獲取Message物件。然後再看看關鍵的欄位,sPoolSync是一個普通的Object物件,它的作用就是用於在獲取Message物件時進行同步鎖。再看sPool居然是一個Message物件,居然不是我們上面說的訊息池之類的東西,既然它命名為sPool不可能是有名無實吧,我們再仔細看,發現了這個欄位。
在這裡插入圖片描述
這個欄位就在sPoolSync上面,“山重水複疑無路,柳暗花明又一村”。一看上面的註釋我們就明白了,原來Message訊息池沒有使用map這樣的容器,使用的是連結串列!這個next就是指向下一個Message的。Message的連結串列如圖22-2所示。
在這裡插入圖片描述
每個Message物件都有一個同型別的next欄位,這個next指向的就是下一個可用的Message,最後一個可用的Message的next則為空。這樣一來,所有可用的Message物件就通過next串連成一個可用的Message池。

那麼這些Message物件什麼時候會被放到連結串列中呢?我們在obtain函式中只看到了從連結串列中獲取,並且看到儲存。如果訊息池連結串列中沒有可用物件的時候,obtain中則是直接返回一個通過new建立的Message物件,而且並沒有儲存到連結串列中。此時,我們再次遇到了難點,暫時找不到相關線索了。

此時我們只好回過頭再看看Message類的說明,發現一個重要的句子。

"which will pull them from a pool of recycled objects.”,噢,原來在建立的時候不會把 Message物件放到池中,在回收(這裡的回收並不是指虛擬機器回收Message物件)該物件時才會將該物件新增到連結串列中。

我們搜尋一番之後果然發現了Message類中有類似Bitmap那樣的recycle函式。具體程式碼如下。
在這裡插入圖片描述
在這裡插入圖片描述
recycle函式會將一個Message物件回收到一個全域性的池中,這個池也就是我們上文說的連結串列。recycle函式首先判斷該訊息是否還在使用,如果還在使用則丟擲異常,否則呼叫recycleUnchecked函式處理該訊息recycleUnchecked函式中先清空該訊息的各欄位,並且將flags設定為FLAG_IN_USE,表明該訊息已被使用,這個flag在obtain函式中會被置為0,這樣根據這個flag就能夠追蹤該訊息的狀態然後判斷是否要將該訊息回收到訊息池中,如果池的大小小於MAX_POOL_SIZE時,將自身新增到連結串列的表頭。例如,當連結串列中還沒有元素時,將第一個Message物件新增到連結串列中,此時sPool為null,next指向了sPool,因此,next也為null然後sPool又指向了this,因此,sPool就指向了當前這個被回收的物件,並且sPoolSize加1。我們把這個被回收的Message物件命名為m1,此時結構圖如圖22-3所示。
在這裡插入圖片描述
此時如果再插入一個名稱為m2的Message物件,那麼m2將會被插到表頭中,此時sPool指向的就是m2,結構如圖22-4所示。

這個物件池的大小預設為50,因此,如果池大小在小於50的情況下,被回收的Message就會被插到連結串列頭部。

此時如果池中有元素,當我們呼叫obtain函式時,如果池中有元素就會從池中獲取,實際上獲取的也是表頭元素,也就是這裡的sPool。然後再將sPool這個指標後移到下一個元素。具體程式碼如下。
在這裡插入圖片描述
在obtain函式中,首先會宣告一個Message物件m,並且讓m指向sPool。sPool實際上指向了m2,因此,m實際上指向的也是m2,這裡相當於儲存了m2這個元素。下一步是sPool指向m2的下一個元素,也就是m1。sPool也完成後移之後此時把m.next置空,也就相當於m2.next變成了null。最後就是m指向了m2元素,m2的next為空,sPool從原來的表頭m2指向了下一個元素m1,最後將物件池的元素減1,這樣m2就順利地脫離了訊息池隊伍,返回給了呼叫obtain函式的客戶端程式。此時結構如圖22-5所示。

現在己經很明朗了,Message通過在內部構建一個連結串列來維護一個被回收的Message物件的物件池,當使用者呼叫obtain函式時會優先從池中取,如果池中沒有可以複用的物件則建立這個新的Message物件。這些新建立的Message物件在被使用完之後會被回收到這個物件池中,當下次再呼叫obtain函式時,它們就會被複用。這裡的Message相當於承擔了享元模式中3個元素的職責,即是Flyweight抽象,又是ConcreteFlyweight角色,同時又承擔了FlyweightFactory管理物件池的職責。因為Android應用是事件驅動的,因此,如果通過new建立Message就會建立大量重複的Message物件,導致記憶體佔用率高、頻繁GC等問題,通過享元模式建立一個大小為50的訊息池,避免了上述問題的產生,使得這些問題迎刃而解。當然,這裡的享元模式並不是經典的實現方式,它沒有內部、外部狀態,集各個職責於一身,甚至它更像是一個物件池,但這些都是很細節問題,我們關注的是靈活運用模式本身來解決問題。至於Message物件是否職責過多,既是實體類又是工廠類, 這些問題每個人見仁見智,也許你覺得增加一個MessagePool來管理Message物件的回收、獲取工作不會更好,這樣也滿足了單一職責原則;或者你覺得就這樣用就挺好,沒有必要增加管理類。這些我們不過多評論,原則上只是提供了一個可借鑑的規則,這個規則很多時候並不是一成不變的, 可以根據實際場景進行取捨。規則是使讀者避免走向軟體大泥潭,靈活運用才是最終的目的所在。

22.7 深度擴充

上文我們說到,Message、MessageQueue、Looper、Handler的工作原理像是工廠的生產線,Looper就是發動機,MessageQueue就是傳送帶,Handler就是工人,Message則是待處理的產品。它們的結構圖如圖22-6所示。
在這裡插入圖片描述
前面的章節中我們多次提到Android應用程式的入口實際上是ActivityThread.main方法,在該方法中首先會建立Application和預設啟動的Activity,並且將它們關聯在一起。而該應用的UI執行緒的訊息迴圈也是在這個方法中建立的,具體原始碼如下。
在這裡插入圖片描述
在這裡插入圖片描述
執行ActivityThread.main方法後,應用程式就啟動了,UI執行緒的訊息迴圈也在Looper.loop()函式中啟動。此後Looper會一直從訊息佇列中取訊息,然後處理訊息。使用者或者系統通過Handler不斷地往訊息佇列中新增訊息,這些訊息不斷地被取出、處理、回收,使得應用迅速地運轉起來

例如,我們在子執行緒中執行完耗時操作後通常需要更新UI,但我們都“知道”不能在子執行緒中更新UI。此時最常用的手段就是通過Handler將一個訊息post到UI執行緒中,然後再在Handler的handleMessage方法中進行處理。但是有一點要注意,如果用在不傳遞UI執行緒所屬的Looper的情況下,那麼該Handler必須在主執行緒中建立!正確地使用示例如下
在這裡插入圖片描述
為什麼必須要這麼做呢?

其實每個Handler都會關聯一個訊息佇列,訊息佇列被封裝在Lopper中,而每個Looper又是ThreadLocal的,也就是說每個訊息佇列只會屬於一個執行緒。因此,如果一個Looper線上程A中建立,那麼該Looper只能夠被執行緒A訪問。而Handler則是一個訊息投遞、處理器,它將訊息投遞給訊息佇列,然後這些訊息在訊息佇列中被取出,並且執行在關聯了該訊息佇列的執行緒中

預設情況下,訊息佇列只有一個,即主執行緒的訊息佇列,這個訊息佇列是在ActivityThread.main方法中建立的,也就呼叫了Lopper.prepareMainLooper方法,建立Looper之後,最後會執行Looper.loop()來啟動訊息迴圈

那麼Handler是如何關聯訊息佇列以及執行緒呢?我們還是深入原始碼來分析,首先看看Handler的建構函式。
在這裡插入圖片描述
Handler預設的建構函式中我們可以看到,Handler會在內部通過Looper.getLooper()來獲取Looper物件,並且與之關聯,最重要的就是獲取到Looper持有的訊息佇列mQueue。那麼Looper.getLooper()又是如何工作的呢?我們繼續往下看。
在這裡插入圖片描述
我們看到myLooper方法是通過sThreadLocal.get()來獲取的,關於ThreadLocal的資料請參考Java相關的書籍。那麼Looper物件又是什麼時候儲存在sThreadLocal中的呢?有些讀者可能看到 了,上面給出的程式碼中給出了一個熟悉的方法prepareMainLooper,在這個方法中呼叫了prepare()方法,在prepare()方法中建立了一個Looper物件,並且將該物件設定給了sThreadLocal。這樣,佇列就與執行緒關聯上了。

我們再回到Handler中來,Looper屬於某個執行緒,訊息佇列儲存在Looper中,因此,訊息佇列就通過Looper與特定的執行緒關聯上。而Handler又與Looper、訊息佇列關聯,因此,Handler最終就和執行緒、執行緒的訊息佇列關聯上了,通過該Handler傳送的訊息最終就會被執行在這個執行緒上。這就能解釋上面提到的問題了,“在不傳遞Looper引數給Handler建構函式的情況下,用更新UI的Handler為什麼必須在UI執行緒中建立?”。就是因為Handler要與主執行緒的訊息佇列關聯上,這樣handleMessage才會執行在UI執行緒中,更新UI才是被允許的

建立了Looper後,會呼叫Looper的loop函式,在這個函式中會不斷地從訊息佇列中取出、處理訊息,具體原始碼如下。
在這裡插入圖片描述
從上述程式可以看到,loop方法中實質上就是建立一個死迴圈,然後通過從訊息佇列中逐個取出訊息,最後就是處理訊息、回收訊息的過程

在註釋3處,呼叫了MessageQueue的next函式來獲取下一條要處理的訊息。這個MessageQueue在Looper的建構函式中構建,我們看看next函式的核心程式碼。
在這裡插入圖片描述
在這裡插入圖片描述
完整的next函式稍微有些複雜,但這裡只分析核心的程式。next函式的基本思路就是從訊息佇列中依次取出訊息,如果這個訊息到了執行時間,那麼就將這條訊息返回給Looper,並且將訊息佇列連結串列的指標後移。這個訊息佇列連結串列結構與Message中的訊息池結構一致,也是通過Message的next欄位將多個Message物件串連在一起。但是在從訊息佇列獲取訊息之前,還有一個nativePollOnce函式的呼叫,第一個引數為mPtr,第二個引數為超時時間

這與Native層有什麼關係?mPtr又是什麼?

其實這個mPtr可是大有來頭,它儲存了Native層的訊息佇列物件,也就是說Native層還有一個MessageQueue型別。mPtr的初始化是在MessageQueue的建構函式中,具體程式碼如下。
在這裡插入圖片描述
可以看到,mPtr的值是nativelnit函式返回的,該函式在android_os_MessageQueue.cpp類中,我們繼續跟蹤程式碼。
在這裡插入圖片描述
我們看到,在nativelnit函式中會構造一個NativeMessageQueue物件,然後將該物件轉為個整型值,並且返回給Java層中,而當Java層需要與Native層的MessageQueue通訊時只要把這個int值傳遞給Native層,然後Native通過reinterpret_cast將傳遞進來的int轉換為NativeMessageQueue指標即可得到這個NativeMessageQueue物件指標。首先看看NativeMessageQueue類的建構函式。
在這裡插入圖片描述
在這裡插入圖片描述
程式碼很簡單,就是建立了一個Native層的Looper,然後這個Looper設定給了當前執行緒。也就是說Java層的MessageQueue和Looper在Native層也都有,但是,它們功能並不是一一對應的。 那麼看看Looper究竟做了什麼,首先看看它的建構函式,程式碼在system/core/libutils/Looper.cpp檔案中
在這裡插入圖片描述
首先建立了一個管道(pipe),管道本質上就是一個檔案,一個管道中含有兩個檔案描述符,分別對應讀和寫。一般的使用方式是一個執行緒通過讀檔案描述符來讀管道的內容,當管道沒有內容時,這個執行緒就會進入等待狀態;而另外一個執行緒通過寫檔案描述符來向管道中寫入內容,寫入內容的時候,如果另一端正有執行緒正在等待管道中的內容,那麼這個執行緒就會被喚醒。這個等待和喚醒的操作是通過Linux系統的epoll機制。要使用Linux系統的epoll機制,首先要通過epoll create來建立一個epoll專用的檔案描述符,即註釋2的程式碼。最後通過epoll_ctl函式設定監聽的事件型別為EPOLLIN。此時Native層的MessageQueue和Looper就構建完畢了,在底層也通過管道和epoll建立了一套訊息機制。Native層構建完畢之後則會返回到Java層Looper的建構函式,因此,Java層的Looper和MessageQueue也構建完畢

這個過程有點繞,我們總結一下。

  1. 首先構造Java層的Looper物件,Looper物件又會在建構函式中建立Java層的MessageQueue物件
  2. Java層的MessageQueue的建構函式中呼叫nativelnit函式初始化Native層的NativeMessageQueue, NativeMessageQueue的建構函式又會建立Native層的Looper,並且通過管道和epoll建立一套訊息機制
  3. Native層構建完畢,將NativeMessageQueue物件轉換為一個整型儲存到Java層的MessageQueue的mPtr中
  4. 啟動Java層的訊息迴圈,不斷地讀取、處理訊息

這個初始化過程都是在ActivityThread的main函式中完成的,因此,main函式執行之後,UI執行緒訊息迴圈就啟動了,訊息迴圈不斷地從訊息佇列中讀取、處理訊息,使得系統運轉起來

我們繼續回到nativePollOnce函式本身,每次迴圈去讀訊息時都會呼叫這個函式,我們看看它到底做了什麼?程式碼在android_os_MessageQueue.cpp中。
在這裡插入圖片描述
首先將傳遞進來的整型轉換為NativeMessageQueue指標,這個整型就是在初始化時儲存到mPtr的數值。然後呼叫了NativeMessageQueue的polIOnce函式。具體程式碼如下。
在這裡插入圖片描述
這裡的程式碼很簡單,呼叫了Native層Looper的polIOnce函式。Native層Looper類的完整路徑是 system/core/libutils/Looper.cpp,polIOnce函式如下。
在這裡插入圖片描述
該函式的核心在於呼叫了polllnner,我們看看polllnner的相關實現。
在這裡插入圖片描述
在這裡插入圖片描述
從polllnner的核心程式碼中看,pollinner實際上就是從管道中讀取事件,並且處理這些事件。這樣一來就相當於在Native層存在一個獨立的訊息機制,這些事件儲存在管道中,而Java層的事件則儲存在訊息連結串列中。但這兩個層次的事件都在Java層的Looper訊息迴圈中進行不斷地獲取、處理等操作,從而實現程式的運轉。但需要注意的是,Native層的NativeMessageQueue實際上只是一個代理Native Looper的角色,它沒有做什麼實際工作,只是把操作轉發給Looper。而Native Looper則扮演了一個Java層的Handler角色,它能夠傳送訊息、取訊息、處理訊息

那麼Android為什麼要有兩套訊息機制呢?我們知道Android是支援純Native開發的,因此,在Native層實現一套訊息機制是必須的。另外,Android系統的核心元件也都是執行在Native世界,各元件之間也需要通訊,這樣一來Native層的訊息機制就變得很重要

在分析了訊息迴圈與訊息佇列的基本原理之後,最後看看訊息處理邏輯。我們看到在Java層的MessageQueue的next函式的第4步呼叫了msg.target.dispatchMessage(msg)來處理訊息。其中msg是Message型別,我們看看原始碼。
在這裡插入圖片描述
從原始碼中可以看到,target是Handler型別。實際上就是轉了一圈,通過Handler將訊息傳遞給訊息佇列,訊息佇列又將訊息分發給Handler來處理,其實這也是一個典型的命令模式,Message就是一條命令,Handler就是處理人,通過命令模式將操作和執行者解耦。我們繼續回到Handler程式碼中,訊息處理是呼叫了Handler的dispatchMessage方法,相關程式碼如下。
在這裡插入圖片描述
在這裡插入圖片描述
從上述程式中可以看到,dispatchMessage只是一個分發的方法,如果Runnable型別的callback為空則執行handlerMessage來處理訊息,該方法為空,我們會將更新UI的程式碼寫在該函式中;如果callback不為空,則執行handleCallback來處理,該方法會呼叫callback的run方法。其實這是Handler分發的兩種型別,比如我們post(Runnable callback)則callback就不為空,此時就會執行Runnable的run函式;當我們使用Handler來sendMessage時通常不會設定callback,因此,也就執行handlerMessage這個分支。下面我們看看通過Handler來post一個Runnable物件的實現程式碼。
在這裡插入圖片描述
從上述程式可以看到,在post(Runnable r)時,會將Runnable包裝成Message物件,並且將Runnable物件設定給Message物件的callback欄位,最後會將該Message物件插入訊息佇列。sendMessage也是類似實現。
在這裡插入圖片描述
因此不管是post一個Runnbale還是Message,都會呼叫sendMessageDelayed(msg,time)方法,然後該Message就會追加到訊息佇列中,當在訊息佇列中取出該訊息時就會呼叫callback的run方法或者Handler的handleMessage來執行相應的操作

最後總結一下就是訊息通過Handler投遞到訊息佇列,這個訊息佇列在Handler關聯的Looper中,訊息迴圈啟動之後會不斷地從佇列中獲取訊息,其中訊息的處理分為Native層和Java層,兩個層次都有自己的訊息機制,Native層基於管道和epoll,而Java層則是一個普通的連結串列。獲取訊息之後會呼叫訊息的callback或者分發給對應Handler的handleMessage函式進行處理,這樣就將訊息、訊息的分發、處理隔離開來,降低各個角色之間的耦合。訊息被處理之後會被收回到訊息池中便於下次利用,這樣整個應用通過不斷地執行這個流程就運轉起來了

22.7.2 子執行緒中建立Handler為何會丟擲異常

先給一段程式。
在這裡插入圖片描述
上面的程式碼有問題嗎?

如果你能夠發現並且解釋上述程式碼的問題,那麼應該說您對Handler、Looper、Thread這幾個概念已經很瞭解了。如果您還不太清楚,那麼我們一起往下學習。

前面說過,Looper物件是ThreadLocal的,即每個執行緒都有自己的Looper,這個Looper可以為空。但是,當你要在子執行緒中建立Handler物件時,如果Looper為空,那麼就會丟擲"Can’t create handler inside thread that has not called Looper.prepare()"異常,為什麼會這樣呢?我們一起看原始碼。
在這裡插入圖片描述
從上述程式中我們可以看到,當mLooper物件為空時,丟擲了該異常。這是因為該執行緒中的Looper物件還沒有建立,因此,sThreadLocai.get()會返回null。我們知道Looper是使用ThreadLocal儲存的,也就是說它是和執行緒關聯的,在子執行緒中沒有手動呼叫Looper.prepare之前該執行緒的Looper就為空。因此,解決方法就是在構造Handler之前為當前執行緒設定Looper物件,解決方法如下。
在這裡插入圖片描述
在程式碼中我們增加了2處,第一是通過Looper.prepare()來建立Looper,第二是通過Looper.loop()來啟動訊息迴圈。這樣該執行緒就有了自己的Looper,也就是有了自己的訊息佇列。如果只建立Looper,而不啟動訊息迴圈,雖然不會丟擲異常,但是你通過handler來post或者sendMessage也不會有效,因為雖然訊息被追加到訊息佇列了,但是並沒有啟動訊息迴圈,也就不會從訊息佇列中獲取訊息並且執行

在應用啟動時,會開啟一個主執行緒(UI執行緒),並且啟動訊息迴圈,應用不停地從該訊息佇列中取出、處理訊息達到程式執行的效果Looper物件封裝了訊息佇列,Looper物件被封裝在ThreadLocal中,這使得不同執行緒之間的Looper不能被共享。而Handler通過與Looper物件繫結來實現與執行執行緒的繫結,handler會把Runnable(包裝成Message)或者Message物件追加到與執行緒關聯的訊息佇列中,然後在訊息迴圈中逐個取出訊息,並且處理訊息。當Handler繫結的Looper是主執行緒的Looper,則該Handler可以在handleMessage中更新UI,否則更新UI則會丟擲異常。

22.8 小結

享元模式實現比較簡單,但是它的作用在某些場景確實極其重要的。它可以大大減少應用程式建立的物件,降低程式記憶體的佔用,增強程式的效能,但它同時也提高了系統的複雜性,需要分離出外部狀態和內部狀態,而且外部狀態具有固化特性,不應該隨內部狀態改變而改變,否則導致系統的邏輯混亂

享元模式的優點在於它大幅度地降低記憶體中物件的數量。但是,它做到這一點所付出的代價也是很高的。

  • 享元模式使得系統更加複雜。為了使物件可以共享,需要將一些狀態外部化,這使得程式的邏輯複雜化
  • 享元模式將享元物件的狀態外部化,而讀取外部狀態使得執行時間稍微變長

相關文章