天天看點

Toast的View界面是如何繪制出來的--Toast的Window(view)建立過程

前面我們已經講述了Activity的Window建立過程、Dialog的Window建立過程, 本文将繼續探索Window相關的知識:Toast的建立過程 及 其 View界面的展示。

#####代碼示例

Toast的一般使用非常簡單, 一行代碼就可以搞定:

Toast.makeText(this, "Toast測試", Toast.LENGTH_SHORT).show();
           

通過makeText建立一個Toast, 然後調用show方法,去真正的顯示出來這個Toast, 這一點和前文的Dialog很相似。

我們去看看這個makeText源碼:

public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
    Toast result = new Toast(context);

    LayoutInflater inflate = (LayoutInflater)
            context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
    TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
    tv.setText(text);
    
    result.mNextView = v;
    result.mDuration = duration;

    return result;
}
           

代碼也很簡單, 就是 new 一個Toast, 然後填充一個預設視圖,最後把這個建立的Toast對象傳回。從這裡也可以看出, 其實我們可以自己new一個Toast,然後填充我們的自定義布局, 就可以定義我們自己想要的Toast了。這裡Toast為我們提供了setView(View v)方法實作我們的自定義布局。

#####Toast的Window建立過程

Toast和前文講到的Dialog有所不同,Toast的工作過程要更複雜一些。Toast也是基于Window的, 這是毋庸置疑的,但是由于Toast有具有定時取消的特點,即我們通過設定Toast.LENGTH_SHORT或者Toast.LENGTH~LONG,Toast具有固定顯示實作, 分别是2.5s和3.5s,顯示完後就要消失, 是以這裡系統采用了handler的延時機制去完成。

在Toast内部, 有兩個不同的IPC過程, 第一個是Toast通路NotificationManagerService, 第二個是NotificationManagerService回調給Toast裡的TN接口(這裡的TN就是一個類,它的類名就是TN), 這裡說是一個回調其實是為了友善我們了解, 它的本質其實是NotificationManagerService通過IPC來通路我們的Toast,同時把資料傳過來。關于Binder通信機制可以自行搜尋一下, 或者參考我的部落格**Android的IPC機制–實作AIDL的最簡單例子(上)(下)**, AIDL本質就是一個Binder通信。

除了這兩個IPC過程, 最後Window的添加也是一個IPC過程, 是以Toast的IPC過程總共有三個,其中建立過程兩個, 添加過程一個。

Toast是一個系統級Window(這個後面會說明), show和cancel兩個方法用于顯示和隐藏Toast。

剛剛看過了makeText()方法,裡面很簡單,接着我們看看show方法:

public void show() {
    if (mNextView == null) {
        throw new RuntimeException("setView must have been called");
    }

    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;

    try {
        service.enqueueToast(pkg, tn, mDuration);
    } catch (RemoteException e) {
        // Empty
    }
}
           

注意這一行代碼:

INotificationManager service = getService();
           

這裡就是去擷取一個NotificationManagerService的本地代理 service對象, 然後通過service.enqueueToast(pkg, tn, mDuration) 向NotificationManagerService發送了消息:我要建立一個Toast啦。 這裡就是我們剛剛所提到的第一個IPC過程,其中enqueueToast的三個參數别是包名,TN對象以及 toast的時長。 我們去看一下這個getService()方法:

static private INotificationManager getService() {
    if (sService != null) {
        return sService;
    }
    sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
    return sService;
}
           

如果了解AIDL的通信, 是不是覺得這裡很眼熟, 這其實就是一個AIDL啊,還不了解AIDL的同學,推薦看完本文後,再移步看一下我的另兩篇部落格**Android的IPC機制–實作AIDL的最簡單例子(上)(下)**。跨程序通信不是本篇的重點, 這裡隻是提一下。

回到剛剛的位置, 我們通過service.enqueueToast()方法向NotificationManagerService發送消息後, 将會執行NotificationManagerService的enqueueToas方法, 繼續進去檢視源碼:

private final IBinder mService = new INotificationManager.Stub() {
   
    @Override
    public void enqueueToast(String pkg, ITransientNotification callback, int duration)
    {
       ...
        synchronized (mToastQueue) {
           ...
            try {
                ToastRecord record;
                int index = indexOfToastLocked(pkg, callback);
				// 1. 如果這個toast已經存在于queue了,則隻是更新它,但是并不會把它移動到隊尾
                if (index >= 0) {
                    record = mToastQueue.get(index);
                    record.update(duration);
                } else {
                    if (!isSystemToast) {
                        int count = 0;
                        final int N = mToastQueue.size();
                        for (int i=0; i<N; i++) {
                             final ToastRecord r = mToastQueue.get(i);
							// 2. 判斷如果是同一個包, 則最多隻能存在50個toast,
							//否則不再允許添加進隊列
                             if (r.pkg.equals(pkg)) {
                                 count++;
                                 if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                     return;
                                 }
                             }
                        }
                    }
                    record = new ToastRecord(callingPid, pkg, callback, duration);
					//把傳過來的toast加入進隊列, 等待顯示
                    mToastQueue.add(record);
                   ...
                }
                // 3. 如果是第一個toast, 則直接顯示
                if (index == 0) {
                    showNextToastLocked();
                }
	...
           

上面這一段源碼,主要有三個重要代碼:

    1. 判斷這個Toast是否已存在,如果這個toast已經存在于queue了,則隻是更新它,但是并不會把它移動到隊列的隊尾, 這裡的mToastQueue其實是一個ArrayList;
    1. 判斷如果是同一個包, 則最多隻能存在50個toast,否則不再允許添加進隊列, 這一點非常重要,試想一下, 如果我們通過循環去大量彈出toast, 那麼這個toast隊列裡面的toast就會無窮無盡, 這個時候去打開其他APP, 它們都沒機會彈出toast了。正常情況下, 一個應用的toast也打不到50個,完全夠用了;
    1. 如果是第一個toast, 則直接調用showNextToastLocked方法彈出toast。

要注意這個enqueueToast的三個參數, 我們傳遞過來的分别是包名、 TN對象、 顯示時間, 這裡把TN對象指派給了callback, TN實際上實作了ITransientNotification接口的。

#####Toast的Window顯示過程

接着,我們去看看showNextToastLocked是如何顯示toast的:

void showNextToastLocked() {
    ToastRecord record = mToastQueue.get(0);
    while (record != null) {
       
        try {
			//關鍵代碼 1:回調show方法
            record.callback.show();
			// 關鍵代碼2:執行toast的逾時後的移除處理
            scheduleTimeoutLocked(record);
            return;
        } catch (RemoteException e) {
			//這裡處理由于某些意外導緻 跨程序通信失敗,即這個調用Toast的顯示失敗
			//這個時候就從隊列把這個Toast移除掉, 執行下一個Toast的顯示
            int index = mToastQueue.indexOf(record);
            if (index >= 0) {
                mToastQueue.remove(index);
            }
            keepProcessAliveLocked(record.pid);
            if (mToastQueue.size() > 0) {
                record = mToastQueue.get(0);
            } else {
                record = null;
            }
        }
    }
}
           

這裡的邏輯一樣很簡單, 主要看try代碼塊就行了, 二tyr代碼塊就兩行代碼,都是關鍵代碼:

  • 關鍵代碼1: 回調是record.callback.show(): 這個callback對象其實就是我們的TN對象, 是以這裡就回調給我們的toast中的TN了, 這裡就是第二次跨程序通信;
  • 關鍵代碼2 調用scheduleTimeoutLocked:從這個方法名上也可看出一二, 這是用來處理逾時的方法, 主要是當顯示超過我們的設定的Toast的顯示時間後(即SHORT或者LONG), 就移除掉這個toast,然後繼續調用下一個Toast。具體細節感興趣的可以去跟一下

我們繼續按照主路徑走, 去看我們的關鍵代碼, 這裡回調給了TN的show方法, TN是Toast的一個内部類:

private static class TN extends ITransientNotification.Stub {
    final Runnable mShow = new Runnable() {
        @Override
        public void run() {
            handleShow();
        }
    };

	...

    @Override
    public void show() {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.post(mShow);
    }

	...
}
           

我們看到 TN extends ITransientNotification.Stub, 了解AIDL的同學一看就知道這是一個跨程序通信的類, 它繼承了ITransientNotification.Stub, 也就實作了ITransientNotification接口。 我們繼續跟進show方法, 裡面調用了 mHandler.post(mShow), 具體的執行在mShow裡面,接着看, mShow裡面又調用了handleShow(), 看一下這個方法:

public void handleShow() {
      
        if (mView != mNextView) {
            // 移除掉前一個Toast的view 
            handleHide();
			//把我們的Toast的View傳進來
            mView = mNextView;
           ...
			//關鍵代碼, 擷取WindowManagerb本地代理對象
            mWM = (WindowManager)context.getSystemService
           ...
            //關鍵代碼, 向WindowManager添加Window, 把我們的view傳過去
            mWM.addView(mView, mParams);
        }
    }
           

這個方法邏輯也比較清楚, 就是把上一個toast的view已移除掉,然後把要顯示的Toast添加進入WindowManger, 這同樣是一個IPC過程, Window的添加過程可以參考**從Window的添加過程了解Window和WindowManager**, 文中配了一張圖檔, 很清晰的展示了Window的添加過程。 這就是我們前面提到的到的Toast的 第三次IPC過程。

然後, Toast就真正的顯示在我們的界面上啦~~

到這裡, 整個Toast的view添加, Window添加,以及Toast的逾時後自動消失的 整個流程就講完了~~

#####Toast的一些不為人知的細節

######1、為什麼我們通常隻能在UI主線程使用Toast?

上面一段代碼中, 我們看到,當NotificationManagerService回調給TN, 通知它可以顯示Toast的時候, 回調給了TN的show方法, 我們再次看一些這個show方法:

@Override
    public void show() {
        if (localLOGV) Log.v(TAG, "SHOW: " + this);
        mHandler.post(mShow);
    }
           

注意到沒有, 這裡使用mHandler, 去送出的一個任務, 看一下這個mHandler的定義:

final Handler mHandler = new Handler();   
           

就是一個普通的Handler, 而我們知道, 隻有在主線程, 才能直接這麼使用handler, 否則都會報錯,因為主線程在初始化時, 就已經調用了Looper.prepare()方法。如果需要在子線程使用Handler的話, 必須調用Looper.prepare()方法。是以, 一般情況下, 我們在主線程使用Toast, 但是如果想要在子線程使用Toast, 也是可以的, 就在子線程的run方法最後一行, 調用Looper.prepare()即可。為什麼是在最後一行調用呢, 因為這個Looper.prepare()内部是一個死循環, 在這個方法後面的代碼就無法執行了。

######2、為什麼說Toast是一個系統級的Window?

Window分為系統級Window, 應用級Window, 子Window, 其中子Window必須附屬在其他兩類Window上,這個在**從Window的添加過程了解Window和WindowManager** 一文中有所介紹, 那為什麼說Toast是一個系統級Window呢, 我們隻需要去看看它的層級即可。

我們再去看看Toast的Window的添加過程:

//關鍵代碼, 向WindowManager添加Window, 把我們的view傳過去
 mWM.addView(mView, mParams);
           

層級是在WindowManager.LayoutParams.type中設定的, 我們從這裡的mParams去看看, 找一下這個mParams在哪裡指派了, 找了一圈, 除了剛剛的handleShow方法, mparams在TN的構造方法裡也有一些初始化:

TN() {
     
        final WindowManager.LayoutParams params = mParams;
        params.height = WindowManager.LayoutParams.WRAP_CONTENT;
        params.width = WindowManager.LayoutParams.WRAP_CONTENT;
        params.format = PixelFormat.TRANSLUCENT;
        params.windowAnimations = com.android.internal.R.style.Animation_Toast;
		//關鍵代碼,給Window層級指派
        params.type = WindowManager.LayoutParams.TYPE_TOAST;
        params.setTitle("Toast");
        params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
                | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
                | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
    }
           

我們看到, type的值為WindowManager.LayoutParams.TYPE_TOAST, 去看看這個值:

public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;
           

可以看到它的值是FIRST_SYSTEM_WINDOW+5, 這個FIRST_SYSTEM_WINDOW表示系統Window的第一個層級, 比它大的都是系統Window,FIRST_SYSTEM_WINDOW的值為2000。系統Window的層級為 2000~2999.

到這裡,Toast的Window的建立到消失的整個流程就講完了。

喜歡的朋友麻煩點一個贊吧~~

繼續閱讀