Toast.show()
顯示一個Toast只需要呼叫它的show()
方法,看一下原始碼
/**
109 * Show the view for the specified duration.
110 */
111 public void show() {
112 if (mNextView == null) {
113 throw new RuntimeException("setView must have been called");
114 }
115
116 INotificationManager service = getService(); //獲得NotificationManagerService
117 String pkg = mContext.getOpPackageName(); // 包名
118 TN tn = mTN;
119 tn.mNextView = mNextView;
120
121 try {
122 service.enqueueToast(pkg, tn, mDuration); // 呼叫NMS
123 } catch (RemoteException e) {
124 // Empty
125 }
126 }
複製程式碼
可以看到顯示由 getService()
獲得了 NMS,NMS主要是Android系統用來管理 通知服務的,而且Toast也屬於系統通知的一種。
NMS呼叫了enqueueToast(pkg,tn,mDuration)
,這三個引數分別是 :
- pkg : 應用包名
- tn : 是Toast的一個靜態內部類Tn,用於被回撥,內部含有兩個主要方法用來顯示,隱藏Toast , 並且這兩個方法是等著被回撥,不會主動呼叫
private static class TN extends ITransientNotification.Stub {
@Override
public void hide() {
if (localLOGV) Log.v(TAG, "HIDE: " + this);
mHandler.obtainMessage(HIDE).sendToTarget();
}
public void cancel() {
if (localLOGV) Log.v(TAG, "CANCEL: " + this);
mHandler.obtainMessage(CANCEL).sendToTarget();
}
}
複製程式碼
我們發現,在show()
和hide()
方法中,都是呼叫了Handler
來處理,這是因為 NMS是執行在系統的程式中,Toast和NMS之間是一個IPC過程,NMS只能通過遠端呼叫的方式來顯示和隱藏Toast, 而 TN
這個類是一個Binder
類,它裡面的show()
'hide()'方法會在Toast和NMS進行IPC時回撥。
這時,TN
是執行在Binder的執行緒池中的,而我們的Toast需要在當前UI執行緒中顯示,所以需要通過Handler
,配合著Looper
來完成切換執行緒
- mDuration : 這個就是我們建立Toast時傳入的 顯示時長。
接下來我們會一層一層的深入分析,貼一張圖來記錄進度:
INotificationManager.enqueueToast(pkg,tn,mDurtion)
接下來分析enqueueToast(pkg,tn,mDuration)
裡面是做了什麼事情呢?我們繼續點開看看
NotificationManagerService.java #enqueueToast()
1087 synchronized (mToastQueue) {
1089 ...
1090 try {
//將Toast請求封裝為ToastRecord 見 1117行
1091 ToastRecord record;
1092 int index = indexOfToastLocked(pkg, callback);
1093 //如果Toast已經在列表中,則更新它的資訊
1095 if (index >= 0) {
1096 record = mToastQueue.get(index);
1097 record.update(duration);
1098 } else {
1099 // 限制Toast的個數, MAX_PACKAGE_NOTIFICATIONS = 50
1101 if (!isSystemToast) {
1102 int count = 0;
1103 final int N = mToastQueue.size();
1104 for (int i=0; i<N; i++) {
1105 final ToastRecord r = mToastQueue.get(i);
1106 if (r.pkg.equals(pkg)) {
1107 count++;
1108 if (count >= MAX_PACKAGE_NOTIFICATIONS) {
1109 Slog.e(TAG, "Package has already posted " + count
1110 + " toasts. Not showing more. Package=" + pkg);
1111 return;
1112 }
1113 }
1114 }
1115 }
1116
1117 record = new ToastRecord(callingPid, pkg, callback, duration);
1118 mToastQueue.add(record);
...
1121 }
1122 // 如果index==0,代表就是當前的Toast
1126 if (index == 0) {
1127 showNextToastLocked();
1128 }
1129 } finally {
1130 Binder.restoreCallingIdentity(callingId);
1131 }
複製程式碼
我只擷取了重要的一部分程式碼,有點長,我們來慢慢看:
enqueueToast()
方法首先把Toast的請求封裝到ToastRecord
中。
record = new ToastRecord(callingPid, pkg, callback, duration);
複製程式碼
- 將
ToastRecord
新增到一個儲存到到mToastQueue
中,這是一個ArrayList的儲存結構,對於非系統應用,最多能存下50個Toast,
mToastQueue.add(record);
複製程式碼
1106 if (r.pkg.equals(pkg)) {
1107 count++;
1108 if (count >= MAX_PACKAGE_NOTIFICATIONS) { //MAX_PACKAGE_NOTIFICATIONS = 50
1109 Slog.e(TAG, "Package has already posted " + count
1110 + " toasts. Not showing more. Package=" + pkg);
1111 return;
1112 }
1113}
複製程式碼
- 接下來NMS通過
showNestToastLocked()
來顯示當前的Toast ,index = 0,就代表佇列中只剩下一個Toast,就是當前的Toast
1119 index = mToastQueue.size() - 1;
...
1126 if (index == 0) {
1127 showNextToastLocked();
1128 }
複製程式碼
enqueueToast()分析完了,記錄一下
INotificationManager.showNextToastLocked()
先貼上原始碼
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
...
try {
record.callback.show(record.token); //回撥 callback中的show方法
scheduleTimeoutLocked(record); //傳送延時訊息,取決於Toast的時長
return;
} catch (RemoteException e) {
...//省略部分程式碼
}
}
}
複製程式碼
這裡 record.callback
就是 我們前面提到的TN
,在這裡回撥它的show()
方法Toast,並通過scheduleTimeoutLocked(record)
根據指定的Toast顯示時長髮送一個延時訊息。
當前記錄:
下面來看一下延時訊息是如何實現的
scheduleTimeoutLocked(record)
private void scheduleTimeoutLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
複製程式碼
在上面程式碼中,LONG_DELAY
和SHORT_DELAY
分別是 3.5s和 2s. 在經過這麼長的延時後,傳送message
來看一下對應此Message的處理:
@Override
public void handleMessage(Message msg)
{
switch (msg.what)
{
case MESSAGE_TIMEOUT:
handleTimeout((ToastRecord)msg.obj);
break;
...
}
}
複製程式碼
好,接下來又要進 handleTimeout()
方法中看一下,碼住:
handleTimeout(ToastRecord record)
private void handleTimeout(ToastRecord record)
{
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
複製程式碼
在經過一定的延時時間後,就該去除當前這個Toast了,跟 index 判斷 當前這個Toast是否還在佇列中,如果還在,NMS就會通過cancelToastLocked()
方法來隱藏Toast,並將其從佇列中移除。 如果佇列中還有其他的Toast,繼續呼叫showNextToastLocked();
將其顯示.
cancelToastLocked(int index)
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide();
} catch (RemoteException e) {
...
}
ToastRecord lastToast = mToastQueue.remove(index); //從佇列中移除
mWindowManagerInternal.removeWindowToken(lastToast.token, true, DEFAULT_DISPLAY); //移除window
keepProcessAliveIfNeededLocked(record.pid);
if (mToastQueue.size() > 0) { //如果還有其他的Toast,繼續顯示
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextToastLocked();
}
}
複製程式碼
到這裡,一個Toast的顯示到隱藏就結束了。剛剛我們說過,Toast的顯示和隱藏都是回撥 TN
中的方法 :
現在來看一下TN
中具體顯示Toast的方法 : 可以看一下注釋
public void handleShow(IBinder windowToken) {
...
//如果此時handler又傳送 隱藏 或者 取消的訊息,則返回,也就是不顯示了。
if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
return;
}
if (mView != mNextView) {
// 如果正在顯示的Toast不是當前的Toast.(是之前顯示的還沒隱藏掉),那就隱藏它
handleHide();
mView = mNextView;
Context context = mView.getContext().getApplicationContext();
String packageName = mView.getContext().getOpPackageName();
if (context == null) {
context = mView.getContext();
}
//獲得WindowMangaer
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
... //省略了顯示Toast的一些佈局引數的設定程式碼
try {
mWM.addView(mView, mParams); //將Toast新增到Window中
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
...
}
}
}
複製程式碼
handleShow()
主要做的就是將Toast新增到Window中。相反,handleHide()
會把Toast的View從Window中移除:
public void handleHide() {
if (mView != null) {
if (mView.getParent() != null) {
if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
mWM.removeViewImmediate(mView);
}
mView = null;
}
}
複製程式碼
mView.getParent()
用來判斷此View是否已經被新增到Window,如果!= null,說明 有Window包含這個Toast的View,那就移除它。
Toast的流程圖:
ps: 這是我第一次寫關於原始碼分析的部落格,還有很多可能寫的不清楚的地方,大家可以指出來互相學習O(∩_∩)O。我覺得通過看原始碼能夠讓你對系統的理解層次清晰,不會及停留在表面。看原始碼的時候注意不要被各個類之間的呼叫關係搞混,可以隨手畫出來記錄一下。。。
Reference: 《Android藝術開發探索》-
(完~)