Android學習筆記14-從原始碼分析Toast的建立過程

拜仁fans發表於2019-05-12

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                }
複製程式碼

我只擷取了重要的一部分程式碼,有點長,我們來慢慢看:

  1. enqueueToast()方法首先把Toast的請求封裝到ToastRecord中。
record = new ToastRecord(callingPid, pkg, callback, duration);
複製程式碼
  1. 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_DELAYSHORT_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藝術開發探索》-

(完~)

Android學習筆記14-從原始碼分析Toast的建立過程

相關文章