您好,登錄后才能下訂單哦!
這篇文章主要講解了“Android的Toast問(wèn)題有哪些”,文中的講解內(nèi)容簡(jiǎn)單清晰,易于學(xué)習(xí)與理解,下面請(qǐng)大家跟著小編的思路慢慢深入,一起來(lái)研究和學(xué)習(xí)“Android的Toast問(wèn)題有哪些”吧!
當(dāng)你在程序中調(diào)用了 Toast
的 API
,你可能會(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
的源碼。
首先,所有 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) Toast
在 show
的時(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)用 ToastRecord
的 callback
成員的 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ì) Android
的 Binder
機(jī)制已經(jīng)熟門(mén)熟路了,當(dāng)我們調(diào)用 TN
代理對(duì)象的 show
方法的時(shí)候,相當(dāng)于 RPC
調(diào)用了 TN
的 show
方法。來(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í)空,回到 NotificationManager
的 showNextToastLocked()
方法。大家可以看到:在調(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)用 Handler
的 sendMessageDelayed
函數(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ù)將做以下兩件事:
遠(yuǎn)程調(diào)用 ITransientNotification.hide
方法,通知客戶端隱藏窗口
將給 Toast
生成的窗口 Token
從 WMS
服務(wù)中刪除
上面我們就從源碼的角度分析了一個(gè)Toast的顯示和隱藏,我們不妨再來(lái)捋一下思路,Toast
的顯示和隱藏大致分成以下核心步驟:
Toast
調(diào)用 show
方法的時(shí)候 ,實(shí)際上是將自己納入到 NotificationManager
的 Toast
管理中去,期間傳遞了一個(gè)本地的 TN
類(lèi)型或者是 ITransientNotification.Stub
的 Binder
對(duì)象
NotificationManager
收到 Toast
的顯示請(qǐng)求后,將生成一個(gè) Binder
對(duì)象,將它作為一個(gè)窗口的 token
添加到 WMS
對(duì)象,并且類(lèi)型是 TOAST
NotificationManager
將這個(gè)窗口 token
通過(guò) ITransientNotification
的 show
方法傳遞給遠(yuǎn)程的 TN
對(duì)象,并且拋出一個(gè)超時(shí)監(jiān)聽(tīng)消息 scheduleTimeoutLocked
TN
對(duì)象收到消息以后將往 Handler
對(duì)象中 post
顯示消息,然后調(diào)用顯示處理函數(shù)將 Toast
中的 View
添加到了 WMS
管理中, Toast
窗口顯示
NotificationManager
的 WorkerHandler
收到 MESSAGE_TIMEOUT
消息, NotificationManager
遠(yuǎn)程調(diào)用進(jìn)程隱藏 Toast
窗口,然后將窗口 token
從 WMS
中刪除
上面我們分析了 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)致 TN
的 show
方法 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)題?
引起這個(gè)問(wèn)題的也不一定是卡頓,當(dāng)你的 TN
拋出消息的時(shí)候,前面有大量的 UI
線程消息等待執(zhí)行,而每個(gè) UI
線程消息雖然并不卡頓,但是總和如果超過(guò)了 NotificationManager
的超時(shí)時(shí)間,還是會(huì)出現(xiàn)問(wèn)題
UI 線程執(zhí)行了一條非常耗時(shí)的操作,比如加載圖片,大量浮點(diǎn)運(yùn)算等等,比如我們上面用 sleep
模擬的就是這種情況
在某些情況下,進(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)注!
免責(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)容。