Android 高階自定義Toast及原始碼解析
Toast概述
Toast的作用
不需要和使用者互動的提示框。 更多參見官網: https://developer.android.com/guide/topics/ui/notifiers/toasts.html
Toast的簡單使用

image
自定義Toast

image
佈局檔案中根元素為LinearLayout,垂直放入一個ImageView和一個TextView。程式碼就不貼了。
高階自定義Toast
產品狗的需求:點選一個Button,網路請求失敗的情況下使用Toast的方式提醒使用者。 程式猿:ok~大筆一揮。

image
測試:你這程式寫的有問題。每次點選就彈出了氣泡,連續點選20次,居然花了一分多鐘才顯示完。改! 程式猿:系統自帶的就這樣。愛要不要。 測試:那我用單元測試模擬點選50次之後,它就不顯示了,這個怎麼說。 程式猿:… 這個時候,高階自定義Toast就要出場了~ activity_main.xml—->上下兩個按鈕,略。 MainActivity.Java

image

image

image
SingleToast.java

image
那麼有的同學會問了:你這樣不就是加了個單例嗎,好像也沒有什麼區別。區別大了。僅僅一個單例,既實現了產品狗的需求,又不會有單元測試快速點選50次的之後不顯示的問題。為什麼?Read The Fucking Source Code。
Toast原始碼解析
這裡以Toast.makeText().show為例,一步步追尋這個過程中原始碼所做的工作。自定義Toast相當於自己做了makeText()方法的工作,道理是一樣一樣的,這裡就不再分別講述了~
原始碼位置:frameworks/base/core/java/Android/widght/Toast.java Toast#makeText()

image
這裡填充的佈局transient_notification.xml位於frameworks/base/core/res/res/layout/transient_notification.xml。加分項,對於XML佈局檔案解析不太瞭解的同學可以看下這篇部落格。

image
可以發現,裡面只有一個TextView,平日設定的文字內容就是在這裡展示。接下來只有一個show()方法,似乎我們的原始碼解析到這裡就快結束了。不,這只是個開始

image
這裡有三個問題。
- 通過getService()怎麼就獲得一個INotificationManager物件?
- TN類是個什麼鬼?
- 方法最後只有一個service.enqueueToast(),顯示和隱藏在哪裡?
Toast的精華就在這三個問題裡,接下來的內容全部圍繞上述三個問題,尤其是第三個。已經全部瞭解的同學可以去看別的部落格了~
1. 通過getService()怎麼就獲得一個INotificationManager物件?

image
對Binder機制瞭解的同學看見XXX.Stub.asInterface肯定會很熟悉,這不就是AIDL中獲取client嘛!確實是這樣。
tips: 本著追本溯源的精神,先看下ServiceManager.getService("notification")。在上上上上篇部落格SystemServer啟動流程原始碼解析中startOtherServices()涉及到NotificationManagerService的啟動,程式碼如下,這裡不再贅述。
<pre class="prism-token token language-javascript" style="box-sizing: border-box; list-style: inherit; margin: 24px 0px; font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; font-style: normal; font-variant: normal; font-weight: normal; font-stretch: normal; font-size: 14px; padding: 16px; overflow: auto; line-height: 1.45; background-color: rgb(247, 247, 247); border-radius: 3px; word-wrap: normal; text-align: left; white-space: pre; word-spacing: 0px; word-break: normal; tab-size: 2; hyphens: none; color: rgb(51, 51, 51); letter-spacing: normal; orphans: 2; text-indent: 0px; text-transform: none; widows: 2; -webkit-text-stroke-width: 0px;">mSystemServiceManager.startService(NotificationManagerService.class);</pre>
Toast中AIDL對應檔案的位置。
原始碼位置:frameworks/base/core/java/android/app/INotificationManager.aidl
Server端:NotificationManagerService.java 原始碼位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
篇幅有限,這裡不可能將AIDL檔案完整的敘述一遍,不瞭解的同學可以理解為:經過程序間通訊(AIDL方式),最後呼叫NotificationManagerService#enqueueToast()。具體可以看下這篇部落格。
2. TN類是個什麼鬼?
在Toast#makeText()中第一行就獲取了一個Toast物件

image
原始碼位置:frameworks/base/core/java/android/widght/Toast$TN.java

image
原始碼中的程序間通訊實在太多了,我不想說這方面的內容啊啊啊~。有時間專門再寫一片部落格。這裡提前劇透下TN類除了設定引數的作用之外,更大的作用是Toast顯示與隱藏的回撥。TN類在這裡作為Server端。NotificationManagerService$NotificationListeners類作為client端。這個暫且按下不提,下文會詳細講述。
3. show()方法最後只有一個service.enqueueToast(),顯示和隱藏在哪裡?
原始碼位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java

image

image
在Toast#show()最終會進入到這個方法。首先通過indexOfToastLocked()方法獲取應用程式對應的ToastRecord在mToastQueue中的位置,Toast消失後返回-1,否則返回對應的位置。mToastQueue明明是個ArratList物件,卻命名Queue,猜測後面會遵循“後進先出”的原則移除對應的ToastRecord物件~。這裡先以返回index=-1檢視,也就是進入到else分支。如果不是系統程式,也就是應用程式。那麼同一個應用程式瞬時在mToastQueue中存在的訊息不能超過50條(Toast物件不能超過50個)。否則直接return。這也是上文中為什麼快速點選50次之後無法繼續顯示的原因。既然瞬時Toast不能超過50個,那麼運用單例模式使用同一個Toast物件不就可以了嘛?答案是:可行。訊息用完了就移除,瞬時存在50個以上的Toast物件相信在正常的程式中也用不上。而且註釋中也說這樣做是為了放置DOS攻擊和防止洩露。其實從這裡也可以看出:為了防止記憶體洩露,建立Toast最好使用getApplicationContext,不建議使用Activity、Service等。
迴歸主題。接下來建立了一個ToastRecord物件並新增進mToastQueue。接下來呼叫showNextToastLocked()方法顯示一個Toast。
原始碼位置:frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java NotificationManagerService#showNextToastLocked()

image
這裡首先呼叫record.callback.show(),這裡的record.callback其實就是TN類。接下來呼叫scheduleTimeoutLocked()方法,我們知道Toast顯示一段時間後會自己消失,所以這個方法肯定是定時讓Toast消失。跟進。

image
果然如此。重點在於使用mHandler.sendMessageDelayed(m, delay)延遲傳送訊息。這裡的delay只有兩種值,要麼等於LENGTH_LONG,其餘統統的等於SHORT_DELAY,setDuration為其他值用正常手段是沒有用的(可以反射,不在重點範圍內)。 handler收到MESSAGE_TIMEOUT訊息後會呼叫handleTimeout((ToastRecord)msg.obj)。跟進。

image
啥也不說了,跟進吧~

image
延遲呼叫record.callback.hide()隱藏Toast,前文也提到過:record.callback就是TN物件。到這,第三個問題已經解決一半了,至少我們已經直到Toast的顯示和隱藏在哪裡被呼叫了,至於怎麼顯示怎麼隱藏的,客觀您接著往下看。
原始碼位置:frameworks/base/core/java/android/widght/ToastTN.javaToastTN#show()

image
注意下這裡直接使用new Handler獲取Handler物件,這也是為什麼在子執行緒中不用Looper彈出Toast會出錯的原因。跟進handleShow()。

image
原來addView到WindowManager。這樣就完成了Toast的顯示。至於隱藏就更簡單了。

image
直接remove掉。
喜歡的話請幫忙轉發一下能讓更多有需要的人看到吧,有些技術上的問題大家可以多探討一下。


以上Android資料以及更多Android相關資料及面試經驗可在QQ群裡獲取:936903570。有加群的朋友請記得備註上簡書,謝謝