`Toast.makeText(context, msg, LENGTH_SHORT).show()` — 這行簡單代碼背後,隱藏著 **跨進程通訊 (IPC)、系統服務隊列管理、WindowManager 動態添加 View、Handler 線程切換** 等核心機制。本文基於 Android 源碼 (frameworks/base/core/java/android/widget/Toast.java 及 NotificationManagerService),完整還原 Toast 顯示全鏈路。
// Toast.java
public static Toast makeText(Context context, CharSequence text, int duration) {
Toast result = new Toast(context);
// 1. 加載佈局:transient_notification.xml (LinearLayout + TextView)
LayoutInflater inflate = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
// 2. 設置文本
TextView tv = v.findViewById(com.android.internal.R.id.message);
tv.setText(text);
// 3. 保存 View 與時長
result.mNextView = v;
result.mDuration = duration; // LENGTH_SHORT (0) / LENGTH_LONG (1)
return result;
}
// Toast.java 內部類
private static class TN extends ITransientNotification.Stub {
final Handler mHandler = new Handler(); // 關鍵:綁定創建線程的 Looper (通常是主線程)
// Binder 回調:系統服務調用此方法顯示 Toast
@Override public void show() {
mHandler.post(mShow); // 切換到創建線程 (主線程) 執行
}
@Override public void hide() {
mHandler.post(mHide);
}
// 實際顯示邏輯 (運行在主線程)
void handleShow() {
if (mView != mNextView) { // View 變更
handleHide(); // 先移除舊 View
mView = mNextView;
// 獲取 WindowManager
Context context = mView.getContext().getApplicationContext();
mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// 配置 WindowManager.LayoutParams
mParams.gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
mParams.x = mX; mParams.y = mY;
// 關鍵屬性:TYPE_TOAST、非焦點、非觸摸、保持屏幕亮
mParams.type = WindowManager.LayoutParams.TYPE_TOAST;
mParams.flags = FLAG_KEEP_SCREEN_ON | FLAG_NOT_FOCUSABLE | FLAG_NOT_TOUCHABLE;
// 添加 View 到窗口
mWM.addView(mView, mParams);
}
}
void handleHide() {
if (mView != null && mView.getParent() != null) {
mWM.removeView(mView); // 從窗口移除
}
mView = null;
}
}
關鍵點:
// NotificationManagerService.java
public void enqueueToast(String pkg, ITransientNotification callback, int duration) {
// 1. 校驗包名/回調
// 2. 判斷是否系統 Toast (系統 UID 或 pkg="android")
boolean isSystemToast = isCallerSystem() || "android".equals(pkg);
// 3. 非系統 Toast 黑名單/數量限制 (MAX_PACKAGE_NOTIFICATIONS = 50)
if (!isSystemToast && !checkPermission(pkg)) return;
synchronized (mToastQueue) {
// 4. 去重:同一 pkg + 同一 callback (TN) 視為同一 Toast,只更新時長
int index = indexOfToastLocked(pkg, callback);
ToastRecord record;
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
} else {
// 5. 非系統 Toast 數量限制
if (!isSystemToast && countPackageToasts(pkg) >= MAX_PACKAGE_NOTIFICATIONS) return;
// 6. 創建記錄入隊
record = new ToastRecord(callingPid, pkg, callback, duration);
mToastQueue.add(record);
}
// 7. 保持進程存活 (設置前台進程,防止被 LMK 殺死)
keepProcessAliveLocked(callingPid);
// 8. 隊頭顯示觸發
if (mToastQueue.indexOf(record) == 0) {
showNextToastLocked();
}
}
}
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0); // 隊頭
try {
// 核心:跨進程調用 TN.show() -> 觸發 handleShow() -> WMS addView
record.callback.show();
// 9. 設置超時消息 (關鍵:系統強制限制時長)
scheduleTimeoutLocked(record);
} catch (RemoteException e) {
// 進程死亡,移除記錄,顯示下一個
mToastQueue.remove(0);
showNextToastLocked();
}
}
private static final int LONG_DELAY = 3500; // 3.5 秒
private static final int SHORT_DELAY = 2000; // 2 秒
void scheduleTimeoutLocked(ToastRecord r) {
mHandler.removeCallbacksAndMessages(r); // 清除舊消息
Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);
// 關鍵:無論傳入什麼 duration,**系統只認 LENGTH_LONG/SHORT 對應 3.5s/2s**
long delay = (r.duration == Toast.LENGTH_LONG) ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
揭秘:`Toast.makeText(..., duration)` 中的 `duration` **只能是 `LENGTH_SHORT` (0) 或 `LENGTH_LONG` (1)**,對應**固定 2 秒 / 3.5 秒**。**傳入自定義毫秒值無效**!系統在 NMS 中硬編碼映射。
void handleTimeout(ToastRecord record) {
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) cancelToastLocked(index);
}
}
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide(); // IPC 調用 TN.hide() -> handleHide() -> WMS removeView
} catch (RemoteException ignored) {}
mToastQueue.remove(index);
keepProcessAliveLocked(record.pid); // 更新前台進程狀態
if (!mToastQueue.isEmpty()) showNextToastLocked(); // 顯示下一個
}
App 進程 系統進程 (system_server)
| |
|-- Toast.makeText() --> 構建 View |
|-- toast.show() ------------------->| (IPC)
| |-- NMS.enqueueToast()
| | |-- 入隊 mToastQueue
| | |-- 隊頭? -> showNextToastLocked()
| | |-- callback.show() (IPC 回調)
| | | |-- TN.show() -> mHandler.post(mShow)
| (主線程) | | |
|<-- TN.handleShow() ----------------| | |
| |-- WMS.addView(TYPE_TOAST) ----------------->|
| | | |
| | |-- scheduleTimeoutLocked(2s/3.5s)
| | | (延遲發送 MESSAGE_TIMEOUT)
| | | |
| | | (時間到) mHandler.handleMessage
| | | |
| | |-- handleTimeout() -> cancelToastLocked()
| | |-- callback.hide() (IPC 回調)
| | | |-- TN.hide() -> mHandler.post(mHide)
| (主線程) | | |
|<-- TN.handleHide() ----------------| | |
| |-- WMS.removeView() ------------------------->| |
| | |-- 移除隊列、顯示下一個
| 設計 | 作用 |
|---|---|
| TN 繼承 ITransientNotification.Stub | 實現 App 進程與 System Server 進程雙向 IPC (show/hide 回調) |
| Handler 綁定創建線程 Looper | 確保 View 操作 (addView/removeView) 在主線程執行,符合 Android UI 線程模型 |
| TYPE_TOAST 窗口類型 | 繞過普通應用窗口層級,顯示在系統級別,不受 Activity 生命週期影響 |
| NMS 隊列串行化 | 避免多 Toast 疊加閃爍,保證順序顯示,單包數量限制防刷屏 |
| 系統強制時長 (2s/3.5s) | 統一用戶體驗,防止長時間佔用屏幕,節省電量/資源 |
| keepProcessAlive (前台進程) | Toast 顯示期間提升進程優先級,防止 LMK 因內存不足殺死進程導致 Toast 異常消失 |
Toast 看似簡單,實則串聯了 **IPC、Binder、Handler、Looper、WindowManager、WMS、NMS 隊列調度、進程優先級管理** 等 Android 核心系統能力。理解其源碼,不僅能解決「為什麼時長不可配置」「為什麼子線程崩潰」「為什麼連續彈只顯最後一個」等疑難雜症,更能窺見 Android 系統服務架構設計的精妙:**跨進程回調 Stub + 主線程 Handler 分發 + 系統服務統一隊列調度 + 窗口管理器動態 View**,這是 Android 系統級 UI 組件的標準化實現範式。