首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Android Toast 源碼深度解析:從 show() 到系統隊列的完整鏈路

     編輯:Java 相關知識

前言

`Toast.makeText(context, msg, LENGTH_SHORT).show()` — 這行簡單代碼背後,隱藏著 **跨進程通訊 (IPC)、系統服務隊列管理、WindowManager 動態添加 View、Handler 線程切換** 等核心機制。本文基於 Android 源碼 (frameworks/base/core/java/android/widget/Toast.java 及 NotificationManagerService),完整還原 Toast 顯示全鏈路。

一、 入口:makeText() — 構建 View 與數據載體


// 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;
}
  • 佈局:系統內置 `transient_notification.xml`,垂直 LinearLayout 包裹 TextView,樣式 `TextAppearance.Toast`,背景 `?attr/toastFrameBackground`。
  • 關鍵字段:`mNextView` (待顯示 View)、`mDuration` (時長標識)。

二、 核心回調對象:TN (Transport Notification) — IPC Stub


// 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;
    }
}

關鍵點:

  1. IPC Stub:`TN extends ITransientNotification.Stub`,實現跨進程調用接口。
  2. Handler 線程綁定:`mHandler = new Handler()` **捕獲創建時線程的 Looper**。**Toast 必須在主線程 (UI 線程) 創建**,否則無 Looper 會拋 `RuntimeException: Can't create handler inside thread that has not called Looper.prepare()`。子線程需先 `Looper.prepare()/loop()`。
  3. WindowManager 添加 View:Toast 本質是**一個 TYPE_TOAST 類型的無焦點、不可觸摸窗口**,通過 `WindowManager.addView` 動態掛載到 WindowManagerService (WMS)。

三、 系統服務:NotificationManagerService (NMS) — 隊列調度中樞

1. 入隊:enqueueToast()


// 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();
        }
    }
}

2. 顯示調度: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();
    }
}

3. 系統強制時長限制:scheduleTimeoutLocked()


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 中硬編碼映射。

4. 超時處理:handleTimeout() → cancelToastLocked()


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 異常消失

六、 實戰避坑指南

  1. 子線程顯示 Toast 崩潰:`Looper.prepare(); Looper.loop();` 或用 `runOnUiThread` / `Handler(Looper.getMainLooper())` 切主線程。
  2. 自定義時長無效:只能用 `LENGTH_SHORT/LONG`。需自定義時長 → 用 `Snackbar`、自定義 `Dialog` 或 `WindowManager` 手動 `addView` + `Handler` 延遲移除。
  3. 連續彈 Toast 只顯示最後一個:NMS 隊列機制導致。需求「疊加顯示」→ 自定義 View + WindowManager 管理。
  4. Activity 銷毀後 Toast 仍顯示:正常行為 (TYPE_TOAST 窗口獨立)。需攔截 → `onDestroy` 記錄並 `toast.cancel()` (調用 `hide()`)。
  5. 非系統 App 彈 Toast 限制 (Android 7.0+):需 `android.permission.SYSTEM_ALERT_WINDOW` 權限 (用戶授權),否則 `enqueueToast` 直接返回。

總結

Toast 看似簡單,實則串聯了 **IPC、Binder、Handler、Looper、WindowManager、WMS、NMS 隊列調度、進程優先級管理** 等 Android 核心系統能力。理解其源碼,不僅能解決「為什麼時長不可配置」「為什麼子線程崩潰」「為什麼連續彈只顯最後一個」等疑難雜症,更能窺見 Android 系統服務架構設計的精妙:**跨進程回調 Stub + 主線程 Handler 分發 + 系統服務統一隊列調度 + 窗口管理器動態 View**,這是 Android 系統級 UI 組件的標準化實現範式。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved