溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

Android的Toast問(wèn)題有哪些

發(fā)布時(shí)間:2022-01-11 17:18:33 來(lái)源:億速云 閱讀:173 作者:iii 欄目:開(kāi)發(fā)技術(shù)

這篇文章主要講解了“Android的Toast問(wèn)題有哪些”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“Android的Toast問(wèn)題有哪些”吧!

1. 異常和偶爾不顯示的問(wèn)題

當(dāng)你在程序中調(diào)用了 ToastAPI,你可能會(huì)在后臺(tái)看到類(lèi)似這樣的 Toast 執(zhí)行異常:

android.view.WindowManager$BadTokenException
    Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
    android.view.ViewRootImpl.setView(ViewRootImpl.java:826)
    android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:369)
    android.view.WindowManagerImpl.addView(WindowManagerImpl.java:94)
    android.widget.Toast$TN.handleShow(Toast.java:459)

另外,在某些系統(tǒng)上,你沒(méi)有看到什么異常,卻會(huì)出現(xiàn) Toast 無(wú)法正常展示的問(wèn)題。為了解釋上面這些問(wèn)題產(chǎn)生的原因,我們需要先讀一遍 Toast 的源碼。

2. Toast 的顯示和隱藏

首先,所有 Android 進(jìn)程的視圖顯示都需要依賴(lài)于一個(gè)窗口。而這個(gè)窗口對(duì)象,被記錄在了我們的 WindowManagerService(后面簡(jiǎn)稱(chēng) WMS) 核心服務(wù)中。WMS 是專(zhuān)門(mén)用來(lái)管理應(yīng)用窗口的核心服務(wù)。當(dāng) Android 進(jìn)程需要構(gòu)建一個(gè)窗口的時(shí)候,必須指定這個(gè)窗口的類(lèi)型。 Toast 的顯示也同樣要依賴(lài)于一個(gè)窗口, 而它被指定的類(lèi)型是:

public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//系統(tǒng)窗口

可以看出, Toast 是一個(gè)系統(tǒng)窗口,這就保證了 Toast 可以在 Activity 所在的窗口之上顯示,并可以在其他的應(yīng)用上層顯示。那么,這就有一個(gè)疑問(wèn):

“如果是系統(tǒng)窗口,那么,普通的應(yīng)用進(jìn)程為什么會(huì)有權(quán)限去生成這么一個(gè)窗口呢?”

實(shí)際上,Android 系統(tǒng)在這里使了一次 “偷天換日” 小計(jì)謀。我們先來(lái)看下 Toast 從顯示到隱藏的整個(gè)流程:

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

        INotificationManager service = getService();//調(diào)用系統(tǒng)的notification服務(wù)
        String pkg = mContext.getOpPackageName();
        TN tn = mTN;//本地binder
        tn.mNextView = mNextView;
        try {
            service.enqueueToast(pkg, tn, mDuration);
        } catch (RemoteException e) {
            // Empty
        }
    }

我們通過(guò)代碼可以看出,當(dāng) Toastshow 的時(shí)候,將這個(gè)請(qǐng)求放在 NotificationManager 所管理的隊(duì)列中,并且為了保證 NotificationManager 能跟進(jìn)程交互, 會(huì)傳遞一個(gè) TN 類(lèi)型的 Binder 對(duì)象給 NotificationManager 系統(tǒng)服務(wù)。而在 NotificationManager 系統(tǒng)服務(wù)中:

//code NotificationManagerService
public void enqueueToast(...) {
    ....
    synchronized (mToastQueue) {
                    ...
                    {
                        // 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) {
                                         //上限判斷
                                         return;
                                     }
                                 }
                            }
                        }

                        Binder token = new Binder();
                        mWindowManagerInternal.addWindowToken(token,
                                WindowManager.LayoutParams.TYPE_TOAST);//生成一個(gè)Toast窗口
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveIfNeededLocked(callingPid);
                    }
                    ....
                     if (index == 0) {
                        showNextToastLocked();//如果當(dāng)前沒(méi)有toast,顯示當(dāng)前toast
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
}

(不去深究其他代碼的細(xì)節(jié),有興趣可以自行研究,挑出我們所關(guān)心的Toast顯示相關(guān)的部分)

我們會(huì)得到以下的流程(在 NotificationManager系統(tǒng)服務(wù)所在的進(jìn)程中):

  • 判斷當(dāng)前的進(jìn)程所彈出的 Toast 數(shù)量是否已經(jīng)超過(guò)上限 MAX_PACKAGE_NOTIFICATIONS ,如果超過(guò),直接返回

  • 生成一個(gè) TOAST 類(lèi)型的系統(tǒng)窗口,并且添加到 WMS 管理

  • 將該 Toast 請(qǐng)求記錄成為一個(gè) ToastRecord 對(duì)象

代碼到這里,我們已經(jīng)看出 Toast 是如何偷天換日的。實(shí)際上,這個(gè)所需要的這個(gè)系統(tǒng)窗口 token ,是由我們的 NotificationManager 系統(tǒng)服務(wù)所生成,由于系統(tǒng)服務(wù)具有高權(quán)限,當(dāng)然不會(huì)有權(quán)限問(wèn)題。不過(guò),我們又會(huì)有第二個(gè)問(wèn)題:

既然已經(jīng)生成了這個(gè)窗口的 Token 對(duì)象,又是如何傳遞給 Android進(jìn)程并通知進(jìn)程顯示界面的呢?

我們知道, Toast 不僅有窗口,也有時(shí)序。有了時(shí)序,我們就可以讓 Toast 按照我們調(diào)用的次序顯示出來(lái)。而這個(gè)時(shí)序的控制,自然而然也是落在我們的 NotificationManager 服務(wù)身上。我們通過(guò)上面的代碼可以看出,當(dāng)系統(tǒng)并沒(méi)有 Toast 的時(shí)候,將通過(guò)調(diào)用 showNextToastLocked(); 函數(shù)來(lái)顯示下一個(gè) Toast。

void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            ...
            try {
                record.callback.show(record.token);//通知進(jìn)程顯示
                scheduleTimeoutLocked(record);//超時(shí)監(jiān)聽(tīng)消息
                return;
            } catch (RemoteException e) {
                ...
            }
        }
    }

這里,showNextToastLocked 函數(shù)將調(diào)用 ToastRecordcallback 成員的 show 方法通知進(jìn)程顯示,那么 callback 是什么呢?

final ITransientNotification callback;//TN的Binder代理對(duì)象

我們看到 callback 的聲明,可以知道它是一個(gè) ITransientNotification 類(lèi)型的對(duì)象,而這個(gè)對(duì)象實(shí)際上就是我們剛才所說(shuō)的 TN 類(lèi)型對(duì)象的代理對(duì)象:

private static class TN extends ITransientNotification.Stub {     ... }

那么 callback對(duì)象的show方法中需要傳遞的參數(shù) record.token呢?實(shí)際上就是我們剛才所說(shuō)的NotificationManager服務(wù)所生成的窗口的 token。
相信大家已經(jīng)對(duì) AndroidBinder 機(jī)制已經(jīng)熟門(mén)熟路了,當(dāng)我們調(diào)用 TN 代理對(duì)象的 show 方法的時(shí)候,相當(dāng)于 RPC 調(diào)用了 TNshow 方法。來(lái)看下 TN 的代碼:

// code TN.java
final Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                IBinder token = (IBinder) msg.obj;
                handleShow(token);//處理界面顯示
            }
        };
@Override
        public void show(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.obtainMessage(0, windowToken).sendToTarget();
        }

這時(shí)候 TN 收到了 show 方法通知,將通過(guò) mHandler 對(duì)象去 post 出一條命令為 0 的消息。實(shí)際上,就是一條顯示窗口的消息。最終,將會(huì)調(diào)用 handleShow(Binder) 方法:

public void handleShow(IBinder windowToken) {
            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
                    + " mNextView=" + mNextView);
            if (mView != mNextView) {
                ...
                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
                ....
                mParams.token = windowToken;
                ...
                mWM.addView(mView, mParams);
                ...
            }
        }

而這個(gè)顯示窗口的方法非常簡(jiǎn)單,就是將所傳遞過(guò)來(lái)的窗口 token 賦值給窗口屬性對(duì)象 mParams, 然后通過(guò)調(diào)用 WindowManager.addView 方法,將 Toast 中的 mView 對(duì)象納入 WMS 的管理。

上面我們解釋了 NotificationManager 服務(wù)是如何將窗口 token 傳遞給 Android 進(jìn)程,并且 Android 進(jìn)程是如何顯示的。我們剛才也說(shuō)到, NotificationManager 不僅掌管著 Toast 的生成,也管理著 Toast 的時(shí)序控制。因此,我們需要穿梭一下時(shí)空,回到 NotificationManagershowNextToastLocked() 方法。大家可以看到:在調(diào)用 callback.show 方法之后又調(diào)用了個(gè) scheduleTimeoutLocked 方法:

record.callback.show(record.token);
//通知進(jìn)程顯示 scheduleTimeoutLocked(record);//超時(shí)監(jiān)聽(tīng)消息

而這個(gè)方法就是用于管理 Toast 時(shí)序:

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

scheduleTimeoutLocked 內(nèi)部通過(guò)調(diào)用 HandlersendMessageDelayed 函數(shù)來(lái)實(shí)現(xiàn)定時(shí)調(diào)用,而這個(gè) mHandler 對(duì)象的實(shí)現(xiàn)類(lèi),是一個(gè)叫做 WorkerHandler 的內(nèi)部類(lèi):

private final class WorkerHandler extends Handler
    {
        @Override
        public void handleMessage(Message msg)
        {
            switch (msg.what)
            {
                case MESSAGE_TIMEOUT:
                    handleTimeout((ToastRecord)msg.obj);
                    break;
                ....
            }
    } 
    private void handleTimeout(ToastRecord record)
    {
        synchronized (mToastQueue) {
            int index = indexOfToastLocked(record.pkg, record.callback);
            if (index >= 0) {
                cancelToastLocked(index);
            }
        }
    }

WorkerHandler 處理 MESSAGE_TIMEOUT 消息會(huì)調(diào)用 handleTimeout(ToastRecord) 函數(shù),而 handleTimeout(ToastRecord) 函數(shù)經(jīng)過(guò)搜索后,將調(diào)用 cancelToastLocked 函數(shù)取消掉 Toast 的顯示:

void cancelToastLocked(int index) {
        ToastRecord record = mToastQueue.get(index);
            ....
            record.callback.hide();//遠(yuǎn)程調(diào)用hide,通知客戶端隱藏窗口
            ....

        ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, true);
        //將給 Toast 生成的窗口 Token 從 WMS 服務(wù)中刪除
        ...

cancelToastLocked 函數(shù)將做以下兩件事:

  1. 遠(yuǎn)程調(diào)用 ITransientNotification.hide 方法,通知客戶端隱藏窗口

  2. 將給 Toast 生成的窗口 TokenWMS 服務(wù)中刪除

上面我們就從源碼的角度分析了一個(gè)Toast的顯示和隱藏,我們不妨再來(lái)捋一下思路,Toast 的顯示和隱藏大致分成以下核心步驟:

  1. Toast 調(diào)用 show 方法的時(shí)候 ,實(shí)際上是將自己納入到 NotificationManagerToast 管理中去,期間傳遞了一個(gè)本地的 TN 類(lèi)型或者是 ITransientNotification.StubBinder 對(duì)象

  2. NotificationManager 收到 Toast 的顯示請(qǐng)求后,將生成一個(gè)  Binder 對(duì)象,將它作為一個(gè)窗口的 token 添加到 WMS 對(duì)象,并且類(lèi)型是 TOAST

  3. NotificationManager 將這個(gè)窗口 token 通過(guò) ITransientNotificationshow 方法傳遞給遠(yuǎn)程的 TN 對(duì)象,并且拋出一個(gè)超時(shí)監(jiān)聽(tīng)消息 scheduleTimeoutLocked

  4. TN 對(duì)象收到消息以后將往 Handler 對(duì)象中 post 顯示消息,然后調(diào)用顯示處理函數(shù)將 Toast 中的 View 添加到了 WMS 管理中, Toast 窗口顯示

  5. NotificationManagerWorkerHandler 收到 MESSAGE_TIMEOUT 消息, NotificationManager 遠(yuǎn)程調(diào)用進(jìn)程隱藏  Toast 窗口,然后將窗口 tokenWMS 中刪除

3. 異常產(chǎn)生的原因

上面我們分析了 Toast 的顯示和隱藏的源碼流程,那么為什么會(huì)出現(xiàn)顯示異常呢?我們先來(lái)看下這個(gè)異常是什么呢?

Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
    android.view.ViewRootImpl.setView(ViewRootImpl.java:826)
    android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:369)

首先,這個(gè)異常發(fā)生在 Toast 顯示的時(shí)候,原因是因?yàn)?token 失效。那么 token 為什么會(huì)失效呢?

通常情況下,按照正常的流程,是不會(huì)出現(xiàn)這種異常。但是由于在某些情況下, Android 進(jìn)程某個(gè) UI 線程的某個(gè)消息阻塞。導(dǎo)致 TNshow 方法 post 出來(lái) 0 (顯示) 消息位于該消息之后,遲遲沒(méi)有執(zhí)行。這時(shí)候,NotificationManager 的超時(shí)檢測(cè)結(jié)束,刪除了 WMS 服務(wù)中的 token 記錄。也就是如圖所示,刪除 token 發(fā)生在 Android 進(jìn)程 show 方法之前。這就導(dǎo)致了我們上面的異常。我們來(lái)寫(xiě)一段代碼測(cè)試一下:

public void click(View view) {
        Toast.makeText(this,"test",Toast.LENGTH_SHORT).show();
        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
}

我們先調(diào)用 Toast.show 方法,然后在該 ui 線程消息中 sleep 10秒。當(dāng)進(jìn)程異常退出后我們截取他們的日志可以得到:

12-28 11:10:30.086 24599 24599 E AndroidRuntime: android.view.WindowManager$BadTokenException: Unable to add window -- token android.os.BinderProxy@2e5da2c is not valid; is your activity running?
12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.view.ViewRootImpl.setView(ViewRootImpl.java:679)
12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.view.WindowManagerGlobal.addView(WindowManagerGlobal.java:342)
12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.view.WindowManagerImpl.addView(WindowManagerImpl.java:93)
12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.widget.Toast$TN.handleShow(Toast.java:434)
12-28 11:10:30.086 24599 24599 E AndroidRuntime:     at android.widget.Toast$TN$2.handleMessage(Toast.java:345)

果然如我們所料,我們復(fù)現(xiàn)了這個(gè)問(wèn)題的堆棧。那么或許你會(huì)有下面幾個(gè)疑問(wèn):

Toast.show 方法外增加 try-catch 有用么?

當(dāng)然沒(méi)用,按照我們的源碼分析,異常是發(fā)生在我們的下一個(gè) UI 線程消息中,因此我們?cè)谏弦粋€(gè) ui 線程消息中加入 try-catch 是沒(méi)有意義的

為什么有些系統(tǒng)中沒(méi)有這個(gè)異常,但是有時(shí)候 toast不顯示?

我們上面分析的是7.0的代碼,而在8.0的代碼中,Toast 中的 handleShow發(fā)生了變化:

//code handleShow() android 8.0
                try {
                    mWM.addView(mView, mParams);
                    trySendAccessibilityEvent();
                } catch (WindowManager.BadTokenException e) {
                    /* ignore */
                }

8.0 的代碼中,對(duì) mWM.addView 進(jìn)行了 try-catch 包裝,因此并不會(huì)拋出異常,但由于執(zhí)行失敗,因此不會(huì)顯示 Toast

有哪些原因引起的這個(gè)問(wèn)題?

  1. 引起這個(gè)問(wèn)題的也不一定是卡頓,當(dāng)你的 TN 拋出消息的時(shí)候,前面有大量的 UI 線程消息等待執(zhí)行,而每個(gè) UI 線程消息雖然并不卡頓,但是總和如果超過(guò)了 NotificationManager 的超時(shí)時(shí)間,還是會(huì)出現(xiàn)問(wèn)題

  2. UI 線程執(zhí)行了一條非常耗時(shí)的操作,比如加載圖片,大量浮點(diǎn)運(yùn)算等等,比如我們上面用 sleep 模擬的就是這種情況

  3. 在某些情況下,進(jìn)程退后臺(tái)或者息屏了,系統(tǒng)為了減少電量或者某種原因,分配給進(jìn)程的 cpu 時(shí)間減少,導(dǎo)致進(jìn)程內(nèi)的指令并不能被及時(shí)執(zhí)行,這樣一樣會(huì)導(dǎo)致進(jìn)程看起來(lái)”卡頓”的現(xiàn)象

感謝各位的閱讀,以上就是“Android的Toast問(wèn)題有哪些”的內(nèi)容了,經(jīng)過(guò)本文的學(xué)習(xí)后,相信大家對(duì)Android的Toast問(wèn)題有哪些這一問(wèn)題有了更深刻的體會(huì),具體使用情況還需要大家實(shí)踐驗(yàn)證。這里是億速云,小編將為大家推送更多相關(guān)知識(shí)點(diǎn)的文章,歡迎關(guān)注!

向AI問(wèn)一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI