歡迎你們前往雲+社區,獲取更多騰訊海量技術實踐乾貨哦~java
做者:QQ音樂技術團隊
Toast
做爲 Android
系統中最經常使用的類之一,因爲其方便的api設計和簡潔的交互體驗,被咱們所普遍採用。可是,伴隨着咱們開發的深刻,Toast
的問題也逐漸暴露出來。本文章就將解釋 Toast
這些問題產生的具體緣由。 本系列文章將分紅兩篇:android
Toast
所帶來的問題Toast
問題的解決方案(注:本文源碼基於Android 7.0)api
當你在程序中調用了 Toast
的 API
,你可能會在後臺看到相似這樣的 Toast
執行異常:bash
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
所在的窗口之上顯示,並能夠在其餘的應用上層顯示。那麼,這就有一個疑問:ide
「若是是系統窗口,那麼,普通的應用進程爲何會有權限去生成這麼一個窗口呢?」函數
實際上,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
系統服務中:post
//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
的超時時間,仍是會出現問題sleep
模擬的就是這種狀況cpu
時間減小,致使進程內的指令並不能被及時執行,這樣同樣會致使進程看起來」卡頓」的現象一種Android App在Native層動態加載so庫的方案
Android OpenGL開發實踐 - GLSurfaceView對攝像頭數據的再處理
經過JS庫Encog實現JavaScript機器學習和神經學網絡
此文已由做者受權騰訊雲+技術社區發佈,轉載請註明文章出處