您好,登錄后才能下訂單哦!
這篇文章主要講解了“Android的Toast問題有哪些”,文中的講解內容簡單清晰,易于學習與理解,下面請大家跟著小編的思路慢慢深入,一起來研究和學習“Android的Toast問題有哪些”吧!
當你在程序中調用了 Toast
的 API
,你可能會在后臺看到類似這樣的 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
的源碼。
首先,所有 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 } }
我們通過代碼可以看出,當 Toast
在 show
的時候,將這個請求放在 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
函數將調用 ToastRecord
的 callback
成員的 show
方法通知進程顯示,那么 callback
是什么呢?
final ITransientNotification callback;//TN的Binder代理對象
我們看到 callback
的聲明,可以知道它是一個 ITransientNotification
類型的對象,而這個對象實際上就是我們剛才所說的 TN
類型對象的代理對象:
private static class TN extends ITransientNotification.Stub { ... }
那么 callback
對象的show
方法中需要傳遞的參數 record.token
呢?實際上就是我們剛才所說的NotificationManager
服務所生成的窗口的 token
。
相信大家已經對 Android
的 Binder
機制已經熟門熟路了,當我們調用 TN
代理對象的 show
方法的時候,相當于 RPC
調用了 TN
的 show
方法。來看下 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
的時序控制。因此,我們需要穿梭一下時空,回到 NotificationManager
的 showNextToastLocked()
方法。大家可以看到:在調用 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
內部通過調用 Handler
的 sendMessageDelayed
函數來實現定時調用,而這個 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
函數將做以下兩件事:
遠程調用 ITransientNotification.hide
方法,通知客戶端隱藏窗口
將給 Toast
生成的窗口 Token
從 WMS
服務中刪除
上面我們就從源碼的角度分析了一個Toast的顯示和隱藏,我們不妨再來捋一下思路,Toast
的顯示和隱藏大致分成以下核心步驟:
Toast
調用 show
方法的時候 ,實際上是將自己納入到 NotificationManager
的 Toast
管理中去,期間傳遞了一個本地的 TN
類型或者是 ITransientNotification.Stub
的 Binder
對象
NotificationManager
收到 Toast
的顯示請求后,將生成一個 Binder
對象,將它作為一個窗口的 token
添加到 WMS
對象,并且類型是 TOAST
NotificationManager
將這個窗口 token
通過 ITransientNotification
的 show
方法傳遞給遠程的 TN
對象,并且拋出一個超時監聽消息 scheduleTimeoutLocked
TN
對象收到消息以后將往 Handler
對象中 post
顯示消息,然后調用顯示處理函數將 Toast
中的 View
添加到了 WMS
管理中, Toast
窗口顯示
NotificationManager
的 WorkerHandler
收到 MESSAGE_TIMEOUT
消息, NotificationManager
遠程調用進程隱藏 Toast
窗口,然后將窗口 token
從 WMS
中刪除
上面我們分析了 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 線程的某個消息阻塞。導致 TN
的 show
方法 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
有哪些原因引起的這個問題?
引起這個問題的也不一定是卡頓,當你的 TN
拋出消息的時候,前面有大量的 UI
線程消息等待執行,而每個 UI
線程消息雖然并不卡頓,但是總和如果超過了 NotificationManager
的超時時間,還是會出現問題
UI 線程執行了一條非常耗時的操作,比如加載圖片,大量浮點運算等等,比如我們上面用 sleep
模擬的就是這種情況
在某些情況下,進程退后臺或者息屏了,系統為了減少電量或者某種原因,分配給進程的 cpu
時間減少,導致進程內的指令并不能被及時執行,這樣一樣會導致進程看起來”卡頓”的現象
感謝各位的閱讀,以上就是“Android的Toast問題有哪些”的內容了,經過本文的學習后,相信大家對Android的Toast問題有哪些這一問題有了更深刻的體會,具體使用情況還需要大家實踐驗證。這里是億速云,小編將為大家推送更多相關知識點的文章,歡迎關注!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。