Android9.0上針對(duì)Toast的特殊處理圖文詳解
前言
我們都清楚,Toast顯示時(shí)長(zhǎng)有兩個(gè)選擇,長(zhǎng)顯示是3.5秒,端顯示是2秒。那如果想要做到長(zhǎng)時(shí)間顯示,該怎么做呢?有個(gè)歷史遺留的app通過(guò)開(kāi)一個(gè)線程,不斷調(diào)用show方法進(jìn)行實(shí)現(xiàn),這些年也沒(méi)出過(guò)問(wèn)題,直到系統(tǒng)版本更新到了Android9.0。
實(shí)現(xiàn)方式大概如下:
mToast = new Toast(context); mToast.setDuration(Toast.LENGTH_LONG); mToast.setView(layout); ... mToast.show(); //在線程里不斷調(diào)用show方法,達(dá)到長(zhǎng)時(shí)間顯示的目的
在Android9.0上,Toast閃現(xiàn)了一下就不見(jiàn)了,并沒(méi)有如預(yù)期那樣,長(zhǎng)時(shí)間顯示。為什么呢?
概述
這里我們先來(lái)大概了解下Toast的顯示流程。
Toast使用
一般使用Toast的時(shí)候,比較簡(jiǎn)單的就是如下方式:
Toast.makeText(mContext, "hello world", duration).show();
這樣就可以顯示一個(gè)toast。還有一種是自定義view的:
mToast = new Toast(context); mToast.setDuration(Toast.LENGTH_LONG); mToast.setView(layout); mToast.show();
原理都一樣,先new 一個(gè)Toast,然后設(shè)置顯示時(shí)長(zhǎng),設(shè)置toast中要顯示的view(text也是view),然后就可以show出來(lái)。
Toast原理
Toast實(shí)現(xiàn)
先看看Toast的實(shí)現(xiàn):
//frameworks/base/core/java/android/widget/Toast.java
public Toast(@NonNull Context context, @Nullable Looper looper) {
mContext = context;
mTN = new TN(context.getPackageName(), looper);
mTN.mY = context.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.toast_y_offset);
mTN.mGravity = context.getResources().getInteger(
com.android.internal.R.integer.config_toastDefaultGravity);
}
Toast的構(gòu)造函數(shù)很簡(jiǎn)單,主要就是mTN這個(gè)成員,后續(xù)對(duì)Toast的操作都在這里進(jìn)行。緊接著就是設(shè)置Toast顯示時(shí)長(zhǎng)和顯示內(nèi)容:
public void setView(View view) {
mNextView = view;
}
public void setDuration(@Duration int duration) {
mDuration = duration;
mTN.mDuration = duration;
}

Toast顯示
public void show() {
if (mNextView == null) {
throw new RuntimeException("setView must have been called");
}
INotificationManager service = getService(); //這里是一個(gè)通知服務(wù)
String pkg = mContext.getOpPackageName();
TN tn = mTN;
tn.mNextView = mNextView;
try {
service.enqueueToast(pkg, tn, mDuration);
} catch (RemoteException e) {
// Empty
}
}

show方法簡(jiǎn)單,最終是調(diào)用了通知服務(wù)的enqueueToast方法:
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
public void enqueueToast(String pkg, ITransientNotification callback, int duration)
{
...
final boolean isSystemToast = isCallerSystemOrPhone() || ("android".equals(pkg));
...
synchronized (mToastQueue) {
int callingPid = Binder.getCallingPid();
long callingId = Binder.clearCallingIdentity();
try {
ToastRecord record;
int index;
// All packages aside from the android package can enqueue one toast at a time
if (!isSystemToast) {
index = indexOfToastPackageLocked(pkg);
} else {
index = indexOfToastLocked(pkg, callback);
}
// If the package already has a toast, we update its toast
// in the queue, we don't move it to the end of the queue.
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
try {
record.callback.hide();
} catch (RemoteException e) {
}
record.update(callback);
} else {
Binder token = new Binder();
mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, DEFAULT_DISPLAY);
record = new ToastRecord(callingPid, pkg, callback, duration, token);
mToastQueue.add(record);
index = mToastQueue.size() - 1;
}
keepProcessAliveIfNeededLocked(callingPid);
// If it's at index 0, it's the current toast. It doesn't matter if it's
// new or just been updated. Call back and tell it to show itself.
// If the callback fails, this will remove it from the list, so don't
// assume that it's valid after this.
if (index == 0) {
showNextToastLocked();
}
} finally {
Binder.restoreCallingIdentity(callingId);
}
}
}
Toast的管理是通過(guò)ToastRecord類型列表集中管理的,NotificationManagerService會(huì)將每一個(gè)Toast封裝為ToastRecord對(duì)象,并添加到mToastQueue中,mToastQueue的類型是ArrayList。在enqueueToast中,首先會(huì)判斷應(yīng)用是否為系統(tǒng)應(yīng)用,如果是系統(tǒng)應(yīng)用,則通過(guò)indexOfToastLocked來(lái)尋找是否有滿足條件的Toast存在:
int indexOfToastLocked(String pkg, ITransientNotification callback)
{
IBinder cbak = callback.asBinder();
ArrayList<ToastRecord> list = mToastQueue;
int len = list.size();
for (int i=0; i<len; i++) {
ToastRecord r = list.get(i);
if (r.pkg.equals(pkg) && r.callback.asBinder().equals(cbak)) {
return i;
}
}
return -1;
}
判斷的依據(jù)是包名和callback,這里的callback其實(shí)就是上文說(shuō)到的TN類,這是一個(gè)Binder類型,繼承自ITransientNotification.Stub。如果條件符合,則返回對(duì)應(yīng)索引,否則返回-1。首次show Toast的時(shí)候,肯定返回-1,則此時(shí)會(huì)new一個(gè)ToastRecord對(duì)象,并且加入到mToastQueue中,此時(shí)的index則為0:
record = new ToastRecord(callingPid, pkg, callback, duration, token); mToastQueue.add(record); index = mToastQueue.size() - 1;
那么就會(huì)走到如下分支了:
if (index == 0) {
showNextToastLocked(); //顯示Toast
}
void showNextToastLocked() {
ToastRecord record = mToastQueue.get(0);
while (record != null) {
if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback);
try {
record.callback.show(record.token); //調(diào)用TN類的show方法
scheduleDurationReachedLocked(record); //時(shí)間到就隱藏Toast
return;
} catch (RemoteException e) {
...
}
}
}
該方法也簡(jiǎn)單,就是回調(diào)TN類的show方法,上文提過(guò),TN類對(duì)外提供show,hide, cancel等方法,在這些方法中,再通過(guò)內(nèi)部handler進(jìn)行處理:
//frameworks/base/core/java/android/widget/Toast.java
public void show(IBinder windowToken) {
if (localLOGV) Log.v(TAG, "SHOW: " + this);
mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}
//貼出部分handleMessage方法
case SHOW: {
IBinder token = (IBinder) msg.obj;
handleShow(token);
break;
}
public void handleShow(IBinder windowToken) {
...
if (mView != mNextView) {
// remove the old view if necessary
handleHide();
mView = mNextView;
...
mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
try {
mWM.addView(mView, mParams); //交給WMS進(jìn)行下一步的操作,最終顯示出我們的view
trySendAccessibilityEvent();
} catch (WindowManager.BadTokenException e) {
/* ignore */
}
}
}
調(diào)用show方法,最終會(huì)調(diào)用到handleshow方法,在該方法中使用WMS服務(wù)將view顯示出來(lái)。
Toast隱藏
顯示說(shuō)完了,什么時(shí)候隱藏消失?在scheduleDurationReachedLocked方法中:
//frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
private void scheduleDurationReachedLocked(ToastRecord r)
{
mHandler.removeCallbacksAndMessages(r);
Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
mHandler.sendMessageDelayed(m, delay);
}
這里也是使用了一個(gè)handler來(lái)進(jìn)行處理,delay的時(shí)長(zhǎng)取決于我們之前設(shè)置的Toast顯示時(shí)長(zhǎng)。長(zhǎng)時(shí)間為3.5秒,短時(shí)間為2秒。
MESSAGE_DURATION_REACHED消息處理如下:
case MESSAGE_DURATION_REACHED:
handleDurationReached((ToastRecord)msg.obj);
break;
private void handleDurationReached(ToastRecord record)
{
if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback);
synchronized (mToastQueue) {
int index = indexOfToastLocked(record.pkg, record.callback);
if (index >= 0) {
cancelToastLocked(index);
}
}
}
void cancelToastLocked(int index) {
ToastRecord record = mToastQueue.get(index);
try {
record.callback.hide(); //隱藏掉該Toast
} catch (RemoteException e) {
...
}
ToastRecord lastToast = mToastQueue.remove(index); //已經(jīng)顯示完畢的Toast,從列表中移除掉
...
if (mToastQueue.size() > 0) { //如果還有待顯示Toast
// Show the next one. If the callback fails, this will remove
// it from the list, so don't assume that the list hasn't changed
// after this point.
showNextToastLocked();
}
}
該方法調(diào)用TN的hide方法隱藏掉Toast,然后再將Toast從列表中移除??纯措[藏的過(guò)程:
case HIDE: {
handleHide();
// Don't do this in handleHide() because it is also invoked by
// handleShow()
mNextView = null; //這里會(huì)把view清掉
break;
}
public void handleHide() {
if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
if (mView != null) {
...
mWM.removeViewImmediate(mView);
...
mView = null;
}
}
隱藏的過(guò)程,其實(shí)也簡(jiǎn)單,將view從窗口中移除,然后將mNextView和mView置Null。
到此Toast的顯示和隱藏已經(jīng)講完。下面說(shuō)說(shuō)多次show為什么會(huì)導(dǎo)致Toast消失。
Toast的消失
想象一個(gè)場(chǎng)景,如果一個(gè)全局Toast(此次出問(wèn)題的app中就是一個(gè)全局Toast),我們不斷的去調(diào)用Toast的show方法,那么就意味著上文說(shuō)的mToastQueue列表不為空,存在Toast,就會(huì)走到如下分支:
if (!isSystemToast) {
index = indexOfToastPackageLocked(pkg);
} else {
index = indexOfToastLocked(pkg, callback);
}
// If the package already has a toast, we update its toast
// in the queue, we don't move it to the end of the queue.
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
try {
record.callback.hide(); //如果存在已經(jīng)顯示的Toast,這里會(huì)先進(jìn)行hide
} catch (RemoteException e) {
}
record.update(callback);
}
}
hide的流程我們已經(jīng)清楚,會(huì)將資源釋放,將mNextView和mView置為Null。執(zhí)行到這里會(huì)導(dǎo)致第一個(gè)Toast消失,之后調(diào)用showNextToastLocked()方法顯示第二個(gè)Toast,最終調(diào)用到TN的handleShow方法:
public void handleShow(IBinder windowToken) {
// ...
if (mView != mNextView) {
// ...
mView = mNextView;
// ...
mWM = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
// ...
mWM.addView(mView, mParams);
// ...
}
}
由于所有的Toast都對(duì)應(yīng)一個(gè)TN對(duì)象,因此此時(shí)mView和mNextView均為null,不會(huì)執(zhí)行mWM.addView(),Toast也就不會(huì)顯示。
解決方法
在Android9.0中如果想要一直顯示某個(gè)Toast,怎么做?使用局部Toast,不要使用全局Toast。
但有一點(diǎn)比較奇怪的是,查看了Android10.0代碼,發(fā)現(xiàn)Android10.0將這個(gè)機(jī)制回滾了。即Android10.0上又可以一直顯示Toast:
//這里就不執(zhí)行hide的操作了
if (index >= 0) {
record = mToastQueue.get(index);
record.update(duration);
}
結(jié)語(yǔ)
Android多個(gè)系統(tǒng)版本中,唯獨(dú)Android9.0做了這個(gè)特殊處理,無(wú)非就是禁用應(yīng)用長(zhǎng)時(shí)間顯示Toast。但10.0版本又取消了這個(gè)處理,難道是發(fā)現(xiàn)這樣處理并不合適?
到此這篇關(guān)于Android9.0上針對(duì)Toast的特殊處理的文章就介紹到這了,更多相關(guān)Android9.0對(duì)Toast的特殊處理內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Android Toast的用法總結(jié)(五種用法)
- Android使用Toast顯示消息提示框
- Android中使用Toast.cancel()方法優(yōu)化toast內(nèi)容顯示的解決方法
- Android控件系列之Toast使用介紹
- android之自定義Toast使用方法
- Android 5.0以上Toast不顯示的解決方法
- Android開(kāi)發(fā)技巧之永不關(guān)閉的Toast信息框(長(zhǎng)時(shí)間顯示而非系統(tǒng)關(guān)閉)
- 超簡(jiǎn)單實(shí)現(xiàn)Android自定義Toast示例(附源碼)
- android自定義Toast設(shè)定顯示時(shí)間
- 如何解決android Toast重復(fù)顯示
相關(guān)文章
Flutter 使用fluro的轉(zhuǎn)場(chǎng)動(dòng)畫進(jìn)行頁(yè)面切換
在實(shí)際應(yīng)用中,我們常常會(huì)對(duì)不同的頁(yè)面采取不同的轉(zhuǎn)場(chǎng)動(dòng)畫,以提高頁(yè)面切換過(guò)程中的用戶體驗(yàn)。例如,微信的掃碼后在手機(jī)上確認(rèn)登錄頁(yè)面就是從底部彈出的,而大部分頁(yè)面的跳轉(zhuǎn)都是從右向左滑入。通過(guò)這種形式區(qū)分不同的轉(zhuǎn)場(chǎng)場(chǎng)景,從而給用戶更多的趣味性以提高用戶體驗(yàn)。2021-06-06
android開(kāi)發(fā)之listView組件用法實(shí)例簡(jiǎn)析
這篇文章主要介紹了android開(kāi)發(fā)之listView組件用法,結(jié)合實(shí)例形式簡(jiǎn)單分析了listView組件的相關(guān)屬性與使用技巧,需要的朋友可以參考下2016-01-01
android同時(shí)控制EditText輸入字符個(gè)數(shù)和禁止特殊字符輸入的方法
這篇文章主要介紹了android同時(shí)控制EditText輸入字符個(gè)數(shù)和禁止特殊字符輸入的方法,涉及Android操作EditText控制字符操作的技巧,需要的朋友可以參考下2015-04-04
Android?中TextureView和SurfaceView的屬性方法及示例說(shuō)明
這篇文章主要介紹了Android?中TextureView和SurfaceView的屬性方法及示例說(shuō)明,文章圍繞主題展開(kāi)詳細(xì)的內(nèi)容介紹,具有一定的參考價(jià)值,需要的小伙伴可以參考一下2022-06-06
Unity3D游戲引擎實(shí)現(xiàn)在Android中打開(kāi)WebView的實(shí)例
這篇文章主要介紹了Unity3D游戲引擎在Android中打開(kāi)WebView的實(shí)例,需要的朋友可以參考下2014-07-07
AndroidImageSlider實(shí)現(xiàn)炫酷輪播廣告效果
這篇文章主要為大家詳細(xì)介紹了AndroidImageSlider實(shí)現(xiàn)炫酷輪播廣告效果,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-08-08
使用SharedPreferences在Android存儲(chǔ)對(duì)象詳細(xì)代碼
這篇文章主要介紹了使用SharedPreferences在Android存儲(chǔ)對(duì)象并附上詳細(xì)代碼,下面文章內(nèi)容較少,大多以代碼的形式體現(xiàn),需要的小伙伴可以參考一下,希望對(duì)你有所幫助2021-11-11
Android點(diǎn)擊Button實(shí)現(xiàn)功能的幾種方法總結(jié)
當(dāng)Button有多個(gè)或者Button的使用次數(shù)很多時(shí),我們需要采用綁定監(jiān)聽(tīng)器的做法,其實(shí),綁定監(jiān)聽(tīng)器也有幾種方法,不過(guò),我在這里就不一一列舉了,畢竟那些方法在實(shí)際的應(yīng)用中也不常見(jiàn)2013-10-10

