Android9.0上针对Toast的特殊处理图文详解
前言
我们都清楚,Toast显示时长有两个选择,长显示是3.5秒,端显示是2秒。那如果想要做到长时间显示,该怎么做呢?有个历史遗留的app通过开一个线程,不断调用show方法进行实现,这些年也没出过问题,直到系统版本更新到了Android9.0。
实现方式大概如下:
mToast=newToast(context); mToast.setDuration(Toast.LENGTH_LONG); mToast.setView(layout); ... mToast.show();//在线程里不断调用show方法,达到长时间显示的目的
在Android9.0上,Toast闪现了一下就不见了,并没有如预期那样,长时间显示。为什么呢?
概述
这里我们先来大概了解下Toast的显示流程。
Toast使用
一般使用Toast的时候,比较简单的就是如下方式:
Toast.makeText(mContext,"helloworld",duration).show();
这样就可以显示一个toast。还有一种是自定义view的:
mToast=newToast(context); mToast.setDuration(Toast.LENGTH_LONG); mToast.setView(layout); mToast.show();
原理都一样,先new一个Toast,然后设置显示时长,设置toast中要显示的view(text也是view),然后就可以show出来。
Toast原理
Toast实现
先看看Toast的实现:
//frameworks/base/core/java/android/widget/Toast.java
publicToast(@NonNullContextcontext,@NullableLooperlooper){
mContext=context;
mTN=newTN(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的构造函数很简单,主要就是mTN这个成员,后续对Toast的操作都在这里进行。紧接着就是设置Toast显示时长和显示内容:
publicvoidsetView(Viewview){
mNextView=view;
}
publicvoidsetDuration(@Durationintduration){
mDuration=duration;
mTN.mDuration=duration;
}
Toast显示
publicvoidshow(){
if(mNextView==null){
thrownewRuntimeException("setViewmusthavebeencalled");
}
INotificationManagerservice=getService();//这里是一个通知服务
Stringpkg=mContext.getOpPackageName();
TNtn=mTN;
tn.mNextView=mNextView;
try{
service.enqueueToast(pkg,tn,mDuration);
}catch(RemoteExceptione){
//Empty
}
}
show方法简单,最终是调用了通知服务的enqueueToast方法:
frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
publicvoidenqueueToast(Stringpkg,ITransientNotificationcallback,intduration)
{
...
finalbooleanisSystemToast=isCallerSystemOrPhone()||("android".equals(pkg));
...
synchronized(mToastQueue){
intcallingPid=Binder.getCallingPid();
longcallingId=Binder.clearCallingIdentity();
try{
ToastRecordrecord;
intindex;
//Allpackagesasidefromtheandroidpackagecanenqueueonetoastatatime
if(!isSystemToast){
index=indexOfToastPackageLocked(pkg);
}else{
index=indexOfToastLocked(pkg,callback);
}
//Ifthepackagealreadyhasatoast,weupdateitstoast
//inthequeue,wedon'tmoveittotheendofthequeue.
if(index>=0){
record=mToastQueue.get(index);
record.update(duration);
try{
record.callback.hide();
}catch(RemoteExceptione){
}
record.update(callback);
}else{
Bindertoken=newBinder();
mWindowManagerInternal.addWindowToken(token,TYPE_TOAST,DEFAULT_DISPLAY);
record=newToastRecord(callingPid,pkg,callback,duration,token);
mToastQueue.add(record);
index=mToastQueue.size()-1;
}
keepProcessAliveIfNeededLocked(callingPid);
//Ifit'satindex0,it'sthecurrenttoast.Itdoesn'tmatterifit's
//neworjustbeenupdated.Callbackandtellittoshowitself.
//Ifthecallbackfails,thiswillremoveitfromthelist,sodon't
//assumethatit'svalidafterthis.
if(index==0){
showNextToastLocked();
}
}finally{
Binder.restoreCallingIdentity(callingId);
}
}
}
Toast的管理是通过ToastRecord类型列表集中管理的,NotificationManagerService会将每一个Toast封装为ToastRecord对象,并添加到mToastQueue中,mToastQueue的类型是ArrayList。在enqueueToast中,首先会判断应用是否为系统应用,如果是系统应用,则通过indexOfToastLocked来寻找是否有满足条件的Toast存在:
intindexOfToastLocked(Stringpkg,ITransientNotificationcallback)
{
IBindercbak=callback.asBinder();
ArrayListlist=mToastQueue;
intlen=list.size();
for(inti=0;i
判断的依据是包名和callback,这里的callback其实就是上文说到的TN类,这是一个Binder类型,继承自ITransientNotification.Stub。如果条件符合,则返回对应索引,否则返回-1。首次showToast的时候,肯定返回-1,则此时会new一个ToastRecord对象,并且加入到mToastQueue中,此时的index则为0:
record=newToastRecord(callingPid,pkg,callback,duration,token);
mToastQueue.add(record);
index=mToastQueue.size()-1;
那么就会走到如下分支了:
if(index==0){
showNextToastLocked();//显示Toast
}
voidshowNextToastLocked(){
ToastRecordrecord=mToastQueue.get(0);
while(record!=null){
if(DBG)Slog.d(TAG,"Showpkg="+record.pkg+"callback="+record.callback);
try{
record.callback.show(record.token);//调用TN类的show方法
scheduleDurationReachedLocked(record);//时间到就隐藏Toast
return;
}catch(RemoteExceptione){
...
}
}
}
该方法也简单,就是回调TN类的show方法,上文提过,TN类对外提供show,hide,cancel等方法,在这些方法中,再通过内部handler进行处理:
//frameworks/base/core/java/android/widget/Toast.java
publicvoidshow(IBinderwindowToken){
if(localLOGV)Log.v(TAG,"SHOW:"+this);
mHandler.obtainMessage(SHOW,windowToken).sendToTarget();
}
//贴出部分handleMessage方法
caseSHOW:{
IBindertoken=(IBinder)msg.obj;
handleShow(token);
break;
}
publicvoidhandleShow(IBinderwindowToken){
...
if(mView!=mNextView){
//removetheoldviewifnecessary
handleHide();
mView=mNextView;
...
mWM=(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
...
try{
mWM.addView(mView,mParams);//交给WMS进行下一步的操作,最终显示出我们的view
trySendAccessibilityEvent();
}catch(WindowManager.BadTokenExceptione){
/*ignore*/
}
}
}
调用show方法,最终会调用到handleshow方法,在该方法中使用WMS服务将view显示出来。
Toast隐藏
显示说完了,什么时候隐藏消失?在scheduleDurationReachedLocked方法中:
//frameworks/base/services/core/java/com/android/server/notification/NotificationManagerService.java
privatevoidscheduleDurationReachedLocked(ToastRecordr)
{
mHandler.removeCallbacksAndMessages(r);
Messagem=Message.obtain(mHandler,MESSAGE_DURATION_REACHED,r);
longdelay=r.duration==Toast.LENGTH_LONG?LONG_DELAY:SHORT_DELAY;
mHandler.sendMessageDelayed(m,delay);
}
这里也是使用了一个handler来进行处理,delay的时长取决于我们之前设置的Toast显示时长。长时间为3.5秒,短时间为2秒。
MESSAGE_DURATION_REACHED消息处理如下:
caseMESSAGE_DURATION_REACHED:
handleDurationReached((ToastRecord)msg.obj);
break;
privatevoidhandleDurationReached(ToastRecordrecord)
{
if(DBG)Slog.d(TAG,"Timeoutpkg="+record.pkg+"callback="+record.callback);
synchronized(mToastQueue){
intindex=indexOfToastLocked(record.pkg,record.callback);
if(index>=0){
cancelToastLocked(index);
}
}
}
voidcancelToastLocked(intindex){
ToastRecordrecord=mToastQueue.get(index);
try{
record.callback.hide();//隐藏掉该Toast
}catch(RemoteExceptione){
...
}
ToastRecordlastToast=mToastQueue.remove(index);//已经显示完毕的Toast,从列表中移除掉
...
if(mToastQueue.size()>0){//如果还有待显示Toast
//Showthenextone.Ifthecallbackfails,thiswillremove
//itfromthelist,sodon'tassumethatthelisthasn'tchanged
//afterthispoint.
showNextToastLocked();
}
}
该方法调用TN的hide方法隐藏掉Toast,然后再将Toast从列表中移除。看看隐藏的过程:
caseHIDE:{
handleHide();
//Don'tdothisinhandleHide()becauseitisalsoinvokedby
//handleShow()
mNextView=null;//这里会把view清掉
break;
}
publicvoidhandleHide(){
if(localLOGV)Log.v(TAG,"HANDLEHIDE:"+this+"mView="+mView);
if(mView!=null){
...
mWM.removeViewImmediate(mView);
...
mView=null;
}
}
隐藏的过程,其实也简单,将view从窗口中移除,然后将mNextView和mView置Null。
到此Toast的显示和隐藏已经讲完。下面说说多次show为什么会导致Toast消失。
Toast的消失
想象一个场景,如果一个全局Toast(此次出问题的app中就是一个全局Toast),我们不断的去调用Toast的show方法,那么就意味着上文说的mToastQueue列表不为空,存在Toast,就会走到如下分支:
if(!isSystemToast){
index=indexOfToastPackageLocked(pkg);
}else{
index=indexOfToastLocked(pkg,callback);
}
//Ifthepackagealreadyhasatoast,weupdateitstoast
//inthequeue,wedon'tmoveittotheendofthequeue.
if(index>=0){
record=mToastQueue.get(index);
record.update(duration);
try{
record.callback.hide();//如果存在已经显示的Toast,这里会先进行hide
}catch(RemoteExceptione){
}
record.update(callback);
}
}
hide的流程我们已经清楚,会将资源释放,将mNextView和mView置为Null。执行到这里会导致第一个Toast消失,之后调用showNextToastLocked()方法显示第二个Toast,最终调用到TN的handleShow方法:
publicvoidhandleShow(IBinderwindowToken){
//...
if(mView!=mNextView){
//...
mView=mNextView;
//...
mWM=(WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
//...
mWM.addView(mView,mParams);
//...
}
}
由于所有的Toast都对应一个TN对象,因此此时mView和mNextView均为null,不会执行mWM.addView(),Toast也就不会显示。
解决方法
在Android9.0中如果想要一直显示某个Toast,怎么做?使用局部Toast,不要使用全局Toast。
但有一点比较奇怪的是,查看了Android10.0代码,发现Android10.0将这个机制回滚了。即Android10.0上又可以一直显示Toast:
//这里就不执行hide的操作了
if(index>=0){
record=mToastQueue.get(index);
record.update(duration);
}
结语
Android多个系统版本中,唯独Android9.0做了这个特殊处理,无非就是禁用应用长时间显示Toast。但10.0版本又取消了这个处理,难道是发现这样处理并不合适?
到此这篇关于Android9.0上针对Toast的特殊处理的文章就介绍到这了,更多相关Android9.0对Toast的特殊处理内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。