Android 源碼分析 —— 從 Toast 出發

本系列文章在 github.com/mzlogin/rtfs 持續更新中,歡迎有興趣的童鞋們關注。

(圖 from Android Developers)

Toast 是 Android 開發里較常用的一個類了,有時候用它給用戶彈提示信息和界面反饋,有時候用它來作為輔助調試的手段。用得多了,自然想對其表層之下的運行機制有所了解,所以在此將它選為我的第一個 RTFSC Roots。

本篇採用的記錄方式是先對它有個整體的了解,然後提出一些問題,再通過閱讀源碼,對問題進行一一解讀而後得出答案。

本文使用的工具與源碼為:Chrome、插件 insight.io、GitHub 項目 aosp-mirror/platform_frameworks_base

目錄

  • Toast 印象
  • 提出問題
  • 解答問題
    • Toast 的超時時間
    • 能不能彈一個時間超長的 Toast?
    • Toast 能不能在非 UI 線程調用?
    • 應用在後台時能不能 Toast?
    • Toast 數量有沒有限制?
    • Toast.makeText(…).show() 具體都做了些什麼?
  • 總結
    • 補充後的 Toast 知識點列表
    • 遺留知識點
    • 本篇用到的源碼分析方法
  • 後話

Toast 印象

首先我們從 Toast 類的 官方文檔 和 API 指南 中可以得出它具備如下特性:

  1. Toast 不是 View,它用於幫助創建並展示包含一條小消息的 View;
  2. 它的設計理念是盡量不惹眼,但又能展示想讓用戶看到的信息;
  3. 被展示時,浮在應用界面之上;
  4. 永遠不會獲取到焦點;
  5. 大小取決於消息的長度;
  6. 超時後會自動消失;
  7. 可以自定義顯示在屏幕上的位置(默認左右居中顯示在靠近屏幕底部的位置);
  8. 可以使用自定義布局,也只有在自定義布局的時候才需要直接調用 Toast 的構造方法,其它時候都是使用 makeText 方法來創建 Toast;
  9. Toast 彈出後當前 Activity 會保持可見性和可交互性;
  10. 使用 cancel 方法可以立即將已顯示的 Toast 關閉,讓未顯示的 Toast 不再顯示;
  11. Toast 也算是一個「通知」,如果彈出狀態消息後期望得到用戶響應,應該使用 Notification。

不知道你看到這個列表,是否學到了新知識或者明確了以前不確定的東西,反正我在整理列表的時候是有的。

提出問題

根據以上特性,再結合平時對 Toast 的使用,提出如下問題來繼續本次源碼分析之旅(大致由易到難排列,後文用 小 demo 或者源碼分析來解答):

  1. Toast 的超時時間具體是多少?
  2. 能不能彈一個時間超長的 Toast?
  3. Toast 能不能在非 UI 線程調用?
  4. 應用在後台時能不能 Toast?
  5. Toast 數量有沒有限制?
  6. Toast.makeText(…).show() 具體都做了些什麼?

解答問題

Toast 的超時時間

用這樣的一個問題開始「Android 源碼分析」,真的好怕被打死……大部分人都會嗤之以鼻:Are you kidding me? So easy. 各位大佬們稍安勿躁,閱讀大型源碼不是個容易的活,讓我們從最簡單的開始,一點一點建立自信,將這項偉大的事業進行下去。

面對這個問題,我的第一反應是去查 Toast.LENGTH_LONGToast.LENGTH_SHORT 的值,畢竟平時都是用這兩個值來控制顯示長/短 Toast 的。

文件 platform_frameworks_base/core/java/android/widget/Toast.java 中能看到它們倆的定義是這樣的:

/** * Show the view or text notification for a short period of time. This time * could be user-definable. This is the default. * @see #setDuration */public static final int LENGTH_SHORT = 0;/** * Show the view or text notification for a long period of time. This time * could be user-definable. * @see #setDuration */public static final int LENGTH_LONG = 1;

啊哦~原來它們只是兩個 flag,並非確切的時間值。

既然是 flag,那自然就會有根據不同的 flag 來設置不同的具體值的地方,於是使用 insight.io 點擊 LENGTH_SHORT 的定義搜索一波 Toast.LENGTH_SHORT 的引用,在 aosp-mirror/platform_frameworks_base 里一共有 50 處引用,但都是調用 Toast.makeText(...) 時出現的。

繼續搜索 Toast.LENGTH_LONG 的引用,在 aosp-mirror/platform_frameworks_base 中共出現 42 次,其中有兩處長得像是我們想找的:

第一處,文件 platform_frameworks_base/core/java/android/widget/Toast.java

private static class TN extends ITransientNotification.Stub { ... static final long SHORT_DURATION_TIMEOUT = 4000; static final long LONG_DURATION_TIMEOUT = 7000; ... public void handleShow(IBinder windowToken) { ... mParams.hideTimeoutMilliseconds = mDuration == Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT; ... } ...}

這個 hideTimeoutMilliseconds 是幹嘛的呢?

文件 platform_frameworks_base/core/java/android/view/WindowManager.java 里能看到這個

/** * ... * ... . Therefore, we do hide * such windows to prevent them from overlaying other apps. * * @hide */public long hideTimeoutMilliseconds = -1;

在 GitHub 用 blame 查看到改動這一行的最近一次提交 aa07653d,它的 commit message 能表明它的用途:

Prevent apps to overlay other apps via toast windowsIt was possible for apps to put toast type windowsthat overlay other apps which toast winodws arentremoved after a timeout.Now for apps targeting SDK greater than N MR1 to add atoast window one needs to have a special token. The tokenis added by the notificatoion manager service only forthe lifetime of the shown toast and is then removedincluding all windows associated with this token. Thisprevents apps to add arbitrary toast windows.Since legacy apps may rely on the ability to directlyadd toasts we mitigate by allowing these apps to stilladd such windows for unlimited duration if this app isthe currently focused one, i.e. the user interacts withit then it can overlay itself, otherwise we make surethese toast windows are removed after a timeout likea toast would be.We dont allow more that one toast window per UID beingadded at a time which prevents 1) legacy apps to put thesame toast after a timeout to go around our new policyof hiding toasts after a while; 2) modern apps to reusethe passed token to add more than one window; Note thatthe notification manager shows toasts one at a time.

它並不是用來控制 Toast 的顯示時間的,只是為了防止有些應用的 toast 類型的窗口長期覆蓋在別的應用上面,而超時自動隱藏這些窗口的時間,可以看作是一種防護措施。

第二處,文件 platform_frameworks_base/services/core/java/com/android/server/notification/NotificationManagerService.java 里

long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;

在同一文件里能找到 LONG_DELAYSHORT_DELAY 的定義:

static final int LONG_DELAY = PhoneWindowManager.TOAST_WINDOW_TIMEOUT;static final int SHORT_DELAY = 2000; // 2 seconds

點擊查看 PhoneWindowManager.TOAST_WINDOW_TIMEOUT 的定義:

文件 platform_frameworks_base/services/core/java/com/android/server/policy/PhoneWindowManager.java

/** Amount of time (in milliseconds) a toast window can be shown. */public static final int TOAST_WINDOW_TIMEOUT = 3500; // 3.5 seconds

至此,我們可以得出 結論:Toast 的長/短超時時間分別為 3.5 秒和 2 秒。

Tips: 也可以通過分析代碼里的邏輯,一層一層追蹤用到 LENGTH_SHORT LENGTH_LONG 的地方,最終得出結論,而這裡是根據一些合理推斷來簡化追蹤過程,更快達到目標,這在一些場景下是可取和必要的。

能不能彈一個時間超長的 Toast?

註:這裡探討的是能否直接通過 Toast 提供的公開 API 做到,網路上能搜索到的使用 Timer、反射、自定義等方式達到彈出一個超長時間 Toast 目的的方法不在討論範圍內。

我們在 Toast 類的源碼里看一下跟設置時長相關的代碼:

文件 platform_frameworks_base/core/java/android/widget/Toast.java

... /** @hide */ @IntDef({LENGTH_SHORT, LENGTH_LONG}) @Retention(RetentionPolicy.SOURCE) public @interface Duration {}... /** * Set how long to show the view for. * @see #LENGTH_SHORT * @see #LENGTH_LONG */ public void setDuration(@Duration int duration) { mDuration = duration; mTN.mDuration = duration; }... /** * Make a standard toast that just contains a text view. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * */ public static Toast makeText(Context context, CharSequence text, @Duration int duration) { return makeText(context, null, text, duration); }...

其實從上面 setDurationmakeText 的注釋已經可以看出,duration 只能取值 LENGTH_SHORTLENGTH_LONG,除了注釋之外,還使用了 @Duration 註解來保證此事。Duration 自身使用了 @IntDef 註解,它用於限制可以取的值。

文件 platform_frameworks_base/core/java/android/annotation/IntDef.java

/** * Denotes that the annotated element of integer type, represents * a logical type and that its value should be one of the explicitly * named constants. If the {@link #flag()} attribute is set to true, * multiple constants can be combined. * ... */

不信邪的我們可以快速在一個 demo Android 工程里寫一句這樣的代碼試試:

Toast.makeText(this, "Hello", 2);

Android Studio 首先就不會同意,警告你 Must be one of: Toast.LENGTH_SHORT, Toast.LENGTH_LONG,但實際這段代碼是可以通過編譯的,因為 Duration 註解的 RetentionRetentionPolicy.SOURCE,我的理解是該註解主要能用於 IDE 的智能提示警告,編譯期就被丟掉了。

但即使 duration 能傳入 LENGTH_SHORTLENGTH_LONG 以外的值,也並沒有什麼卵用,別忘了這裡設置的只是一個 flag,真正計算的時候是 long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;,即 duration 為 LENGTH_LONG 時時長為 3.5 秒,其它情況都是 2 秒。

所以我們可以得出 結論:無法通過 Toast 提供的公開 API 直接彈出超長時間的 Toast。(如節首所述,可以通過一些其它方式實現類似的效果)

Toast 能不能在非 UI 線程調用?

這個問題適合用一個 demo 來解答。

我們創建一個最簡單的 App 工程,然後在啟動 Activity 的 onCreate 方法里添加這樣一段代碼:

new Thread(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "Call toast on non-UI thread", Toast.LENGTH_SHORT) .show(); }}).start();

啊哦~很遺憾程序直接掛掉了。

11-07 13:35:33.980 2020-2035/org.mazhuang.androiduidemos E/AndroidRuntime: FATAL EXCEPTION: Thread-77 java.lang.RuntimeException: Cant create handler inside thread that has not called Looper.prepare() at android.widget.Toast$TN.<init>(Toast.java:390) at android.widget.Toast.<init>(Toast.java:114) at android.widget.Toast.makeText(Toast.java:277) at android.widget.Toast.makeText(Toast.java:267) at org.mazhuang.androiduidemos.MainActivity$1.run(MainActivity.java:27) at java.lang.Thread.run(Thread.java:856)

順著堆棧里顯示的方法調用從下往上一路看過去,

文件 platform_frameworks_base/core/java/android/widget/Toast.java

首先是兩級 makeText 方法:

// 我們的代碼里調用的 makeText 方法public static Toast makeText(Context context, CharSequence text, @Duration int duration) { return makeText(context, null, text, duration);}// 隱藏的 makeText 方法,不能手動調用public static Toast makeText(@NonNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration) { Toast result = new Toast(context, looper); // 這裡的 looper 為 null ...

然後到了 Toast 的構造方法:

public Toast(@NonNull Context context, @Nullable Looper looper) { mContext = context; mTN = new TN(context.getPackageName(), looper); // looper 為 null ...}

到 Toast$TN 的構造方法:

// looper = nullTN(String packageName, @Nullable Looper looper) { ... if (looper == null) { // Use Looper.myLooper() if looper is not specified. looper = Looper.myLooper(); if (looper == null) { throw new RuntimeException( "Cant toast on a thread that has not called Looper.prepare()"); } } ...}

至此,我們已經追蹤到了我們的崩潰的 RuntimeException,即要避免進入拋出異常的邏輯,要麼調用的時候傳遞一個 Looper 進來(無法直接實現,能傳遞 Looper 參數的構造方法與 makeText 方法是 hide 的),要麼 Looper.myLooper() 返回不為 null,提示信息 Cant create handler inside thread that has not called Looper.prepare() 里給出了方法,那我們在 toast 前面加一句 Looper.prepare() 試試?這次不崩潰了,但依然不彈出 Toast,畢竟,這個線程在調用完 show() 方法後就直接結束了,沒有調用 Looper.loop(),至於為什麼調用 Toast 的線程結束與否會對 Toast 的顯示隱藏等起影響,在本文的後面的章節里會進行分析。

從崩潰提示來看,Android 並沒有限制在非 UI 線程里使用 Toast,只是線程得是一個有 Looper 的線程。於是我們嘗試構造如下代碼,發現可以成功從非 UI 線程彈出 toast 了:

new Thread(new Runnable() { @Override public void run() { final int MSG_TOAST = 101; final int MSG_QUIT = 102; Looper.prepare(); final Handler handler = new Handler() { @Override public void handleMessage(Message msg) { switch (msg.what) { case MSG_TOAST: Toast.makeText(MainActivity.this, "Call toast on non-UI thread", Toast.LENGTH_SHORT) .show(); sendEmptyMessageDelayed(MSG_QUIT, 4000); return; case MSG_QUIT: Looper.myLooper().quit(); return; } super.handleMessage(msg); } }; handler.sendEmptyMessage(MSG_TOAST); Looper.loop(); }}).start();

至於為什麼 sendEmptyMesageDelayed(MSG_QUIT, 4000) 里的 delayMillis 我設成了 4000,這裡賣個關子,感興趣的同學可以把這個值調成 0、1000 等等看一下效果,會有一些意想不到的情況發生。

到此,我們可以得出 結論:可以在非 UI 線程里調用 Toast,但是得是一個有 Looper 的線程。

ps. 上面這一段演示代碼讓人感覺為了彈出一個 Toast 好麻煩,也可以採用 Activity.runOnUiThread、View.post 等方法從非 UI 線程將邏輯切換到 UI 線程里執行,直接從 UI 線程里彈出,UI 線程是有 Looper 的。

知識點:這裡如果對 Looper、Handler 和 MessageQueue 有所了解,就容易理解多了,預計下一篇對這三劍客進行講解。

應用在後台時能不能 Toast?

這個問題也比較適合用一個簡單的 demo 來嘗試回答。

在 MainActivity 的 onCreate 里加上這樣一段代碼:

view.postDelayed(new Runnable() { @Override public void run() { Toast.makeText(MainActivity.this, "background toast", Toast.LENGTH_SHORT).show(); }}, 5000);

然後待應用啟動後按 HOME 鍵,等幾秒看是否能彈出該 Toast 即可。

結論是:應用在後台時可以彈出 Toast。

Toast 數量有沒有限制?

這個問題將在下一節中一併解答。

Toast.makeText(…).show() 具體都做了些什麼?

首先看一下 makeText 方法。

文件 platform_frameworks_base/core/java/android/widget/Toast.java

/** * Make a standard toast to display using the specified looper. * If looper is null, Looper.myLooper() is used. * @hide */public static Toast makeText(@NonNull Context context, @Nullable Looper looper, @NonNull CharSequence text, @Duration int duration) { Toast result = new Toast(context, looper); 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;}

這個方法里就是構造了一個 Toast 對象,將需要展示的 View 準備好,設置好超時時長標記,我們可以看一下 com.android.internal.R.layout.transient_notification 這個布局的內容:

文件 platform_frameworks_base/core/res/res/layout/transient_notification.xml

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width_="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="?android:attr/toastFrameBackground"> <TextView android:id="@android:id/message" android:layout_width_="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:layout_marginHorizontal="24dp" android:layout_marginVertical="15dp" android:layout_gravity="center_horizontal" android:textAppearance="@style/TextAppearance.Toast" android:textColor="@color/primary_text_default_material_light" /></LinearLayout>

我們最常見的 Toast 就是從這個布局文件渲染出來的了。

我們繼續看一下 makeText 里調用的 Toast 的構造方法里做了哪些事情:

/** * Constructs an empty Toast object. If looper is null, Looper.myLooper() is used. * @hide */public Toast(@NonNull Context context, @Nullable Looper looper) { mContext = context; mTN = new TN(context.getPackageName(), looper); mTN.mY = context.getResources().getDimensionPixelSize( com.android.internal.R.dimen.toast_y_offset); mTN.mGravity = context.getResources().getInteger( com.android.internal.R.integer.config_toastDefaultGravity);}

主要就是構造了一個 TN 對象,計算了位置。

TN 的構造方法:

TN(String packageName, @Nullable Looper looper) { // XXX This should be changed to use a Dialog, with a Theme.Toast // defined that sets up the layout params appropriately. 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; 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; mPackageName = packageName; if (looper == null) { // Use Looper.myLooper() if looper is not specified. looper = Looper.myLooper(); if (looper == null) { throw new RuntimeException( "Cant toast on a thread that has not called Looper.prepare()"); } } mHandler = new Handler(looper, null) { ... };}

設置了 LayoutParams 的初始值,在後面 show 的時候會用到,設置了包名和 Looper、Handler。

TN 是 App 中用於與 Notification Service 交互的對象,這裡涉及到 Binder 和跨進程通信的知識,這塊會在後面開新篇來講解,這裡可以簡單地理解一下:Notification Service 是系統為了管理各種 App 的 Notification(包括 Toast)的服務,比如 Toast,由這個服務來統一維護一個待展示 Toast 隊列,各 App 需要彈 Toast 的時候就將相關信息發送給這個服務,服務會將其加入隊列,然後根據隊列的情況,依次通知各 App 展示和隱藏 Toast。

接下來看看 show 方法:

/** * Show the view for the specified duration. */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 的 enqueueToast 方法,INotificationManager 是一個介面,其實現類在 NotificationManagerService 里,我們來看 enqueueToast 方法的實現:

文件 platform_frameworks_base/services/core/java/com/android/server/notification/NotificationManagerService.java

@Overridepublic void enqueueToast(String pkg, ITransientNotification callback, int duration){ ... synchronized (mToastQueue) { ... try { ToastRecord record; int index = indexOfToastLocked(pkg, callback); // If its already in the queue, we update it in place, we dont // move it to the end of the queue. if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { // Limit the number of toasts that any given package except the android // package can enqueue. Prevents DOS attacks and deals with leaks. if (!isSystemToast) { int count = 0; final int N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } } } Binder token = new Binder(); mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY); record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); index = mToastQueue.size() - 1; keepProcessAliveIfNeededLocked(callingPid); } // If its at index 0, its the current toast. It doesnt matter if its // new or just been updated. Call back and tell it to show itself. // If the callback fails, this will remove it from the list, so dont // assume that its valid after this. if (index == 0) { showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } }}

主要就是使用調用方傳來的包名、callback 和 duration 構造一個 ToastRecord,然後添加到 mToastQueue 中。如果在 mToastQueue 中已經存在該包名和 callback 的 Toast,則只更新其 duration。

這段代碼里有一段可以回答我們的上一個問題 Toast 數量有沒有限制 了:

// Limit the number of toasts that any given package except the android// package can enqueue. Prevents DOS attacks and deals with leaks.if (!isSystemToast) { int count = 0; final int N = mToastQueue.size(); for (int i=0; i<N; i++) { final ToastRecord r = mToastQueue.get(i); if (r.pkg.equals(pkg)) { count++; if (count >= MAX_PACKAGE_NOTIFICATIONS) { Slog.e(TAG, "Package has already posted " + count + " toasts. Not showing more. Package=" + pkg); return; } } }}

即會計算 mToastQueue 里該包名的 Toast 數量,如果超過 50,則將當前申請加入隊列的 Toast 拋棄掉。所以上一個問題的 結論是:Toast 隊列里允許每個應用存在不超過 50 個 Toast。

那麼構造 ToastRecord 並加入 mToastQueue 之後是如何調度,控制顯示和隱藏的呢?enqueueToast 方法里有個邏輯是如果當前列表裡只有一個 ToastRecord,則調用 showNextToastLocked,看一下與該方法相關的代碼:

@GuardedBy("mToastQueue")void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { ... try { record.callback.show(record.token); scheduleTimeoutLocked(record); return; } catch (RemoteException e) { ... if (index >= 0) { mToastQueue.remove(index); } ... } }}...@GuardedBy("mToastQueue")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);}private void handleTimeout(ToastRecord record){ if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback); synchronized (mToastQueue) { int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } }}...@GuardedBy("mToastQueue")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); keepProcessAliveIfNeededLocked(record.pid); if (mToastQueue.size() > 0) { // Show the next one. If the callback fails, this will remove // it from the list, so dont assume that the list hasnt changed // after this point. showNextToastLocked(); // 繼續顯示隊列里的下一個 Toast }}...private final class WorkerHandler extends Handler{ ... @Override public void handleMessage(Message msg) { switch (msg.what) { case MESSAGE_TIMEOUT: handleTimeout((ToastRecord)msg.obj); break; ... } }}

即首先調用 record.callback.show(record.token),通知 App 展示該 Toast,然後根據 duration,延時發送一條超時消息 MESSAGE_TIMEOUT,WorkHandler 收到該消息後,調用 cancelToastLocked 通知應用隱藏該 Toast,並繼續調用 showNextToastLocked 顯示隊列里的下一個 Toast。這樣一個機制就保證了只要隊列里有 ToastRecord,就能依次顯示出來。

機制弄清楚了,再詳細看一下應用接到通知 show 和 hide 一個 Toast 後是怎麼做的:

文件 platform_frameworks_base/core/java/android/widget/Toast.java

private static class TN extends ITransientNotification.Stub { ... TN(String packageName, @Nullable Looper looper) { ... mHandler = new Handler(looper, null) { @Override public void handleMessage(Message msg) { switch (msg.what) { case SHOW: { IBinder token = (IBinder) msg.obj; handleShow(token); break; } case HIDE: { handleHide(); ... break; } ... } } }; } /** * schedule handleShow into the right thread */ @Override public void show(IBinder windowToken) { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.obtainMessage(SHOW, windowToken).sendToTarget(); } /** * schedule handleHide into the right thread */ @Override public void hide() { if (localLOGV) Log.v(TAG, "HIDE: " + this); mHandler.obtainMessage(HIDE).sendToTarget(); } ... public void handleShow(IBinder windowToken) { ... mWM.addView(mView, mParams); ... } ... public void handleHide() { ... mWM.removeViewImmediate(mView); ... }}

顯示過程:show 方法被遠程調用後,先是發送了一個 SHOW 消息,接收到該消息後調用了 handleShow 方法,然後 mWM.addView 將該 View 添加到窗口。

隱藏過程:hide 方法被遠程調用後,先是發送了一個 HIDE 消息,接收到該消息後調用了 handleHide 方法,然後 mWM.removeViewImmediate 將該 View 從窗口移除。

這裡插播一條結論,就是前文留下的為什麼調用 Toast 的線程線束之後沒彈出的 Toast 就無法彈出了的問題,因為 Notification Service 通知應用進程顯示或隱藏 Toast 時,使用的是 mHandler.obtainMessage(SHOW).sendToTarget() mHandler.obtainMessage(HIDE).sendToTarget(),這個消息發出去後,Handler 對應線程沒有在 Looper.loop() 過程里的話,就沒有辦法進入到 Handler 的 handleMessage 方法里去,自然也就無法調用顯示和隱藏 View 的流程了。Looper.loop() 相關的知識點將在下篇講解。

總結

補充後的 Toast 知識點列表

  1. Toast 不是 View,它用於幫助創建並展示包含一條小消息的 View;
  2. 它的設計理念是盡量不惹眼,但又能展示想讓用戶看到的信息;
  3. 被展示時,浮在應用界面之上;
  4. 永遠不會獲取到焦點;
  5. 大小取決於消息的長度;
  6. 超時後會自動消失;
  7. 可以自定義顯示在屏幕上的位置(默認左右居中顯示在靠近屏幕底部的位置);
  8. 可以使用自定義布局,也只有在自定義布局的時候才需要直接調用 Toast 的構造方法,其它時候都是使用 makeText 方法來創建 Toast;
  9. Toast 彈出後當前 Activity 會保持可見性和可交互性;
  10. 使用 cancel 方法可以立即將已顯示的 Toast 關閉,讓未顯示的 Toast 不再顯示;
  11. Toast 也算是一個「通知」,如果彈出狀態消息後期望得到用戶響應,應該使用 Notification;
  12. Toast 的超時時間為 LENGTH_SHORT 對應 2 秒,LENGTH_LONG 對應 3.5 秒;
  13. 不能通過 Toast 類的公開方法直接彈一個時間超長的 Toast;
  14. 應用在後台時可以調用 Toast 並正常彈出;
  15. Toast 隊列里允許單個應用往裡添加 50 個 Toast,超出的將被丟棄。

遺留知識點

本篇涉及到了一些需要進一步了解的知識點,在後續的篇章中會依次解讀:

  1. Handler、Looper 和 MessageQueue
  2. WindowManager
  3. Binder 與跨進程通信

本篇用到的源碼分析方法

  1. 查找關鍵變數被引用的地方;
  2. 按方法調用堆棧一層層邏輯跟蹤與分析;
  3. 使用 git blame 查看關鍵代碼行的變更日誌;

後話

到此,上面提到的幾個問題都已經解答完畢,對 Toast 源碼的分析也告一段落。

寫這篇文章花費的時間比較長,所以並不能按照預計的節奏更新,這裡表示抱歉。另外,各位如果有耐心讀到這裡,覺得本文的思路是否清晰,是否能跟隨文章的節奏理解一些東西?因為我也在摸索寫這類文章的組織形式,所以也希望能收到反饋和建議,以作改進,先行謝過。


最後,照例要安利一下我的微信公眾號「悶騷的程序員」,掃碼關注,接收 rtfsc-android 的最近更新。

原始鏈接:mzlogin/rtfsc-android

推薦閱讀:

Android源碼的Binder許可權是如何控制?
Android中為什麼主線程不會因為Looper.loop()里的死循環卡死?
拿到 Android 項目源碼後,如何才能以最高效的速度看懂?

TAG:Android | Android系统源码 |