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

Toast.show()

顯示一個Toast只須要調用它的show()方法,看一下源碼java

/** 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),這三個參數分別是 :ide

  • 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來完成切換線程oop

  • mDuration : 這個就是咱們建立Toast時傳入的 顯示時長。

接下來咱們會一層一層的深刻分析,貼一張圖來記錄進度:
源碼分析

在這裏插入圖片描述


INotificationManager.enqueueToast(pkg,tn,mDurtion)

接下來分析enqueueToast(pkg,tn,mDuration)裏面是作了什麼事情呢?咱們繼續點開看看佈局

NotificationManagerService.java #enqueueToast()post

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()分析完了,記錄一下this

在這裏插入圖片描述


INotificationManager.showNextToastLocked()

先貼上源碼spa

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藝術開發探索》-

(完~)

相關文章
相關標籤/搜索