亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

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

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Android的Toast問題有哪些

發布時間:2022-01-11 17:18:33 來源:億速云 閱讀:173 作者:iii 欄目:開發技術

這篇文章主要講解了“Android的Toast問題有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Android的Toast問題有哪些”吧!

1. 異常和偶爾不顯示的問題

當你在程序中調用了 ToastAPI,你可能會在后臺看到類似這樣的 Toast 執行異常:

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)

另外,在某些系統上,你沒有看到什么異常,卻會出現 Toast 無法正常展示的問題。為了解釋上面這些問題產生的原因,我們需要先讀一遍 Toast 的源碼。

2. Toast 的顯示和隱藏

首先,所有 Android 進程的視圖顯示都需要依賴于一個窗口。而這個窗口對象,被記錄在了我們的 WindowManagerService(后面簡稱 WMS) 核心服務中。WMS 是專門用來管理應用窗口的核心服務。當 Android 進程需要構建一個窗口的時候,必須指定這個窗口的類型。 Toast 的顯示也同樣要依賴于一個窗口, 而它被指定的類型是:

public static final int TYPE_TOAST = FIRST_SYSTEM_WINDOW+5;//系統窗口

可以看出, Toast 是一個系統窗口,這就保證了 Toast 可以在 Activity 所在的窗口之上顯示,并可以在其他的應用上層顯示。那么,這就有一個疑問:

“如果是系統窗口,那么,普通的應用進程為什么會有權限去生成這么一個窗口呢?”

實際上,Android 系統在這里使了一次 “偷天換日” 小計謀。我們先來看下 Toast 從顯示到隱藏的整個流程:

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

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

我們通過代碼可以看出,當 Toastshow 的時候,將這個請求放在 NotificationManager 所管理的隊列中,并且為了保證 NotificationManager 能跟進程交互, 會傳遞一個 TN 類型的 Binder 對象給 NotificationManager 系統服務。而在 NotificationManager 系統服務中:

//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);//生成一個Toast窗口
                        record = new ToastRecord(callingPid, pkg, callback, duration, token);
                        mToastQueue.add(record);
                        index = mToastQueue.size() - 1;
                        keepProcessAliveIfNeededLocked(callingPid);
                    }
                    ....
                     if (index == 0) {
                        showNextToastLocked();//如果當前沒有toast,顯示當前toast
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
}

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

我們會得到以下的流程(在 NotificationManager系統服務所在的進程中):

  • 判斷當前的進程所彈出的 Toast 數量是否已經超過上限 MAX_PACKAGE_NOTIFICATIONS ,如果超過,直接返回

  • 生成一個 TOAST 類型的系統窗口,并且添加到 WMS 管理

  • 將該 Toast 請求記錄成為一個 ToastRecord 對象

代碼到這里,我們已經看出 Toast 是如何偷天換日的。實際上,這個所需要的這個系統窗口 token ,是由我們的 NotificationManager 系統服務所生成,由于系統服務具有高權限,當然不會有權限問題。不過,我們又會有第二個問題:

既然已經生成了這個窗口的 Token 對象,又是如何傳遞給 Android進程并通知進程顯示界面的呢?

我們知道, Toast 不僅有窗口,也有時序。有了時序,我們就可以讓 Toast 按照我們調用的次序顯示出來。而這個時序的控制,自然而然也是落在我們的 NotificationManager 服務身上。我們通過上面的代碼可以看出,當系統并沒有 Toast 的時候,將通過調用 showNextToastLocked(); 函數來顯示下一個 Toast

void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            ...
            try {
                record.callback.show(record.token);//通知進程顯示
                scheduleTimeoutLocked(record);//超時監聽消息
                return;
            } catch (RemoteException e) {
                ...
            }
        }
    }

這里,showNextToastLocked 函數將調用 ToastRecordcallback 成員的 show 方法通知進程顯示,那么 callback 是什么呢?

final ITransientNotification callback;//TN的Binder代理對象

我們看到 callback 的聲明,可以知道它是一個 ITransientNotification 類型的對象,而這個對象實際上就是我們剛才所說的 TN 類型對象的代理對象:

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

那么 callback對象的show方法中需要傳遞的參數 record.token呢?實際上就是我們剛才所說的NotificationManager服務所生成的窗口的 token
相信大家已經對 AndroidBinder 機制已經熟門熟路了,當我們調用 TN 代理對象的 show 方法的時候,相當于 RPC 調用了 TNshow 方法。來看下 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();
        }

這時候 TN 收到了 show 方法通知,將通過 mHandler 對象去 post 出一條命令為 0 的消息。實際上,就是一條顯示窗口的消息。最終,將會調用 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);
                ...
            }
        }

而這個顯示窗口的方法非常簡單,就是將所傳遞過來的窗口 token 賦值給窗口屬性對象 mParams, 然后通過調用 WindowManager.addView 方法,將 Toast 中的 mView 對象納入 WMS 的管理。

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

record.callback.show(record.token);
//通知進程顯示 scheduleTimeoutLocked(record);//超時監聽消息

而這個方法就是用于管理 Toast 時序:

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 內部通過調用 HandlersendMessageDelayed 函數來實現定時調用,而這個 mHandler 對象的實現類,是一個叫做 WorkerHandler 的內部類:

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 消息會調用 handleTimeout(ToastRecord) 函數,而 handleTimeout(ToastRecord) 函數經過搜索后,將調用 cancelToastLocked 函數取消掉 Toast 的顯示:

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

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

cancelToastLocked 函數將做以下兩件事:

  1. 遠程調用 ITransientNotification.hide 方法,通知客戶端隱藏窗口

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

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

  1. Toast 調用 show 方法的時候 ,實際上是將自己納入到 NotificationManagerToast 管理中去,期間傳遞了一個本地的 TN 類型或者是 ITransientNotification.StubBinder 對象

  2. NotificationManager 收到 Toast 的顯示請求后,將生成一個  Binder 對象,將它作為一個窗口的 token 添加到 WMS 對象,并且類型是 TOAST

  3. NotificationManager 將這個窗口 token 通過 ITransientNotificationshow 方法傳遞給遠程的 TN 對象,并且拋出一個超時監聽消息 scheduleTimeoutLocked

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

  5. NotificationManagerWorkerHandler 收到 MESSAGE_TIMEOUT 消息, NotificationManager 遠程調用進程隱藏  Toast 窗口,然后將窗口 tokenWMS 中刪除

3. 異常產生的原因

上面我們分析了 Toast 的顯示和隱藏的源碼流程,那么為什么會出現顯示異常呢?我們先來看下這個異常是什么呢?

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)

首先,這個異常發生在 Toast 顯示的時候,原因是因為 token 失效。那么 token 為什么會失效呢?

通常情況下,按照正常的流程,是不會出現這種異常。但是由于在某些情況下, Android 進程某個 UI 線程的某個消息阻塞。導致 TNshow 方法 post 出來 0 (顯示) 消息位于該消息之后,遲遲沒有執行。這時候,NotificationManager 的超時檢測結束,刪除了 WMS 服務中的 token 記錄。也就是如圖所示,刪除 token 發生在 Android 進程 show 方法之前。這就導致了我們上面的異常。我們來寫一段代碼測試一下:

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

我們先調用 Toast.show 方法,然后在該 ui 線程消息中 sleep 10秒。當進程異常退出后我們截取他們的日志可以得到:

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)

果然如我們所料,我們復現了這個問題的堆棧。那么或許你會有下面幾個疑問:

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

當然沒用,按照我們的源碼分析,異常是發生在我們的下一個 UI 線程消息中,因此我們在上一個 ui 線程消息中加入 try-catch 是沒有意義的

為什么有些系統中沒有這個異常,但是有時候 toast不顯示?

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

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

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

有哪些原因引起的這個問題?

  1. 引起這個問題的也不一定是卡頓,當你的 TN 拋出消息的時候,前面有大量的 UI 線程消息等待執行,而每個 UI 線程消息雖然并不卡頓,但是總和如果超過了 NotificationManager 的超時時間,還是會出現問題

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

  3. 在某些情況下,進程退后臺或者息屏了,系統為了減少電量或者某種原因,分配給進程的 cpu 時間減少,導致進程內的指令并不能被及時執行,這樣一樣會導致進程看起來”卡頓”的現象

感謝各位的閱讀,以上就是“Android的Toast問題有哪些”的內容了,經過本文的學習后,相信大家對Android的Toast問題有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

鞍山市| 汾西县| 新营市| 吴川市| 玛曲县| 马公市| 叙永县| 竹溪县| 宣武区| 比如县| 莲花县| 石台县| 石嘴山市| 宁陕县| 南部县| 四平市| 浦县| 洛浦县| 九台市| 郑州市| 波密县| 通河县| 蒙阴县| 郴州市| 墨竹工卡县| 阿拉善右旗| 横峰县| 三门县| 青浦区| 淮安市| 江源县| 宜兴市| 佛学| 布尔津县| 双峰县| 钦州市| 衡水市| 抚松县| 绵竹市| 平潭县| 全州县|