详解Android中的Toast源码
Toast源码实现
Toast入口
我们在应用中使用Toast提示的时候,一般都是一行简单的代码调用,如下所示:
[java]viewplaincopyprint?在CODE上查看代码片派生到我的代码片
Toast.makeText(context,msg,Toast.LENGTH_SHORT).show();
makeText就是Toast的入口,我们从makeText的源码来深入理解Toast的实现。源码如下(frameworks/base/core/java/android/widget/Toast.java):
publicstaticToastmakeText(Contextcontext,CharSequencetext,intduration){ Toastresult=newToast(context); LayoutInflaterinflate=(LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); Viewv=inflate.inflate(com.android.internal.R.layout.transient_notification,null); TextViewtv=(TextView)v.findViewById(com.android.internal.R.id.message); tv.setText(text); result.mNextView=v; result.mDuration=duration; returnresult; }
从makeText的源码里,我们可以看出Toast的布局文件是transient_notification.xml,位于frameworks/base/core/res/res/layout/transient_notification.xml:
<?xmlversion="1.0"encoding="utf-8"?> <LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:background="?android:attr/toastFrameBackground"> <TextView android:id="@android:id/message" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:layout_gravity="center_horizontal" android:textAppearance="@style/TextAppearance.Toast" android:textColor="@color/bright_foreground_dark" android:shadowColor="#BB000000" android:shadowRadius="2.75" /> </LinearLayout>
系统Toast的布局文件非常简单,就是在垂直布局的LinearLayout里放置了一个TextView。接下来,我们继续跟到show()方法,研究一下布局形成之后的展示代码实现:
publicvoidshow(){ if(mNextView==null){ thrownewRuntimeException("setViewmusthavebeencalled"); } INotificationManagerservice=getService(); Stringpkg=mContext.getPackageName(); TNtn=mTN; tn.mNextView=mNextView; try{ service.enqueueToast(pkg,tn,mDuration); }catch(RemoteExceptione){ //Empty } }
show方法中有两点是需要我们注意的。(1)TN是什么东东?(2)INotificationManager服务的作用。带着这两个问题,继续我们Toast源码的探索。
TN源码
很多问题都能通过阅读源码找到答案,关键在与你是否有与之匹配的耐心和坚持。mTN的实现在Toast的构造函数中,源码如下:
publicToast(Contextcontext){ mContext=context; mTN=newTN(); 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); }
接下来,我们就从TN类的源码出发,探寻TN的作用。TN源码如下:
privatestaticclassTNextendsITransientNotification.Stub{ finalRunnablemShow=newRunnable(){ @Override publicvoidrun(){ handleShow(); } }; finalRunnablemHide=newRunnable(){ @Override publicvoidrun(){ handleHide(); //Don'tdothisinhandleHide()becauseitisalsoinvokedbyhandleShow() mNextView=null; } }; privatefinalWindowManager.LayoutParamsmParams=newWindowManager.LayoutParams(); finalHandlermHandler=newHandler(); intmGravity; intmX,mY; floatmHorizontalMargin; floatmVerticalMargin; ViewmView; ViewmNextView; WindowManagermWM; TN(){ //XXXThisshouldbechangedtouseaDialog,withaTheme.Toast //definedthatsetsupthelayoutparamsappropriately. finalWindowManager.LayoutParamsparams=mParams; params.height=WindowManager.LayoutParams.WRAP_CONTENT; params.width=WindowManager.LayoutParams.WRAP_CONTENT; params.format=PixelFormat.TRANSLUCENT; params.windowAnimations=com.android.internal.R.style.Animation_Toast; params.type=WindowManager.LayoutParams.TYPE_TOAST; params.setTitle("Toast"); params.flags=WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON |WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; ///M:[ALPS00517576]Supportmulti-user params.privateFlags=WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; } /** *schedulehandleShowintotherightthread */ @Override publicvoidshow(){ if(localLOGV)Log.v(TAG,"SHOW:"+this); mHandler.post(mShow); } /** *schedulehandleHideintotherightthread */ @Override publicvoidhide(){ if(localLOGV)Log.v(TAG,"HIDE:"+this); mHandler.post(mHide); } publicvoidhandleShow(){ if(localLOGV)Log.v(TAG,"HANDLESHOW:"+this+"mView="+mView +"mNextView="+mNextView); if(mView!=mNextView){ //removetheoldviewifnecessary handleHide(); mView=mNextView; Contextcontext=mView.getContext().getApplicationContext(); if(context==null){ context=mView.getContext(); } mWM=(WindowManager)context.getSystemService(Context.WINDOW_SERVICE); //WecanresolvetheGravityherebyusingtheLocaleforgetting //thelayoutdirection finalConfigurationconfig=mView.getContext().getResources().getConfiguration(); finalintgravity=Gravity.getAbsoluteGravity(mGravity,config.getLayoutDirection()); mParams.gravity=gravity; if((gravity&Gravity.HORIZONTAL_GRAVITY_MASK)==Gravity.FILL_HORIZONTAL){ mParams.horizontalWeight=1.0f; } if((gravity&Gravity.VERTICAL_GRAVITY_MASK)==Gravity.FILL_VERTICAL){ mParams.verticalWeight=1.0f; } mParams.x=mX; mParams.y=mY; mParams.verticalMargin=mVerticalMargin; mParams.horizontalMargin=mHorizontalMargin; if(mView.getParent()!=null){ if(localLOGV)Log.v(TAG,"REMOVE!"+mView+"in"+this); mWM.removeView(mView); } if(localLOGV)Log.v(TAG,"ADD!"+mView+"in"+this); mWM.addView(mView,mParams); trySendAccessibilityEvent(); } } privatevoidtrySendAccessibilityEvent(){ AccessibilityManageraccessibilityManager= AccessibilityManager.getInstance(mView.getContext()); if(!accessibilityManager.isEnabled()){ return; } //treattoastsasnotificationssincetheyareusedto //announceatransientpieceofinformationtotheuser AccessibilityEventevent=AccessibilityEvent.obtain( AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); event.setClassName(getClass().getName()); event.setPackageName(mView.getContext().getPackageName()); mView.dispatchPopulateAccessibilityEvent(event); accessibilityManager.sendAccessibilityEvent(event); } publicvoidhandleHide(){ if(localLOGV)Log.v(TAG,"HANDLEHIDE:"+this+"mView="+mView); if(mView!=null){ //note:checkingparent()justtomakesuretheviewhas //beenadded...ihaveseencaseswherewegetherewhen //theviewisn'tyetadded,solet'strynottocrash. if(mView.getParent()!=null){ if(localLOGV)Log.v(TAG,"REMOVE!"+mView+"in"+this); mWM.removeView(mView); } mView=null; } } }
通过源码,我们能很明显的看到继承关系,TN类继承自ITransientNotification.Stub,用于进程间通信。这里假设读者都有Android进程间通信的基础(不太熟的建议学习罗升阳关于Binder进程通信的一系列博客)。既然TN是用于进程间通信,那么我们很容易想到TN类的具体作用应该是Toast类的回调对象,其他进程通过调用TN类的具体对象来操作Toast的显示和消失。
TN类继承自ITransientNotification.Stub,ITransientNotification.aidl位于frameworks/base/core/java/android/app/ITransientNotification.aidl,源码如下:
packageandroid.app; /**@hide*/ onewayinterfaceITransientNotification{ voidshow(); voidhide(); }
ITransientNotification定义了两个方法show()和hide(),它们的具体实现就在TN类当中。TN类的实现为:
/** *schedulehandleShowintotherightthread */ @Override publicvoidshow(){ if(localLOGV)Log.v(TAG,"SHOW:"+this); mHandler.post(mShow); } /** *schedulehandleHideintotherightthread */ @Override publicvoidhide(){ if(localLOGV)Log.v(TAG,"HIDE:"+this); mHandler.post(mHide); }
这里我们就能知道,Toast的show和hide方法实现是基于Handler机制。而TN类中的Handler实现是:
finalHandlermHandler=newHandler();
而且,我们在TN类中没有发现任何Looper.perpare()和Looper.loop()方法。说明,mHandler调用的是当前所在线程的Looper对象。所以,当我们在主线程(也就是UI线程中)可以随意调用Toast.makeText方法,因为Android系统帮我们实现了主线程的Looper初始化。但是,如果你想在子线程中调用Toast.makeText方法,就必须先进行Looper初始化了,不然就会报出java.lang.RuntimeException:Can'tcreatehandlerinsidethreadthathasnotcalledLooper.prepare()。Handler机制的学习可以参考我之前写过的一篇博客:http://blog.csdn.net/wzy_1988/article/details/38346637。
接下来,继续跟一下mShow和mHide的实现,它俩的类型都是Runnable。
finalRunnablemShow=newRunnable(){ @Override publicvoidrun(){ handleShow(); } }; finalRunnablemHide=newRunnable(){ @Override publicvoidrun(){ handleHide(); //Don'tdothisinhandleHide()becauseitisalsoinvokedbyhandleShow() mNextView=null; } };
可以看到,show和hide的真正实现分别是调用了handleShow()和handleHide()方法。我们先来看handleShow()的具体实现:
publicvoidhandleShow(){ if(mView!=mNextView){ //removetheoldviewifnecessary handleHide(); mView=mNextView; Contextcontext=mView.getContext().getApplicationContext(); if(context==null){ context=mView.getContext(); } mWM=(WindowManager)context.getSystemService(Context.WINDOW_SERVICE); //WecanresolvetheGravityherebyusingtheLocaleforgetting //thelayoutdirection finalConfigurationconfig=mView.getContext().getResources().getConfiguration(); finalintgravity=Gravity.getAbsoluteGravity(mGravity,config.getLayoutDirection()); mParams.gravity=gravity; if((gravity&Gravity.HORIZONTAL_GRAVITY_MASK)==Gravity.FILL_HORIZONTAL){ mParams.horizontalWeight=1.0f; } if((gravity&Gravity.VERTICAL_GRAVITY_MASK)==Gravity.FILL_VERTICAL){ mParams.verticalWeight=1.0f; } mParams.x=mX; mParams.y=mY; mParams.verticalMargin=mVerticalMargin; mParams.horizontalMargin=mHorizontalMargin; if(mView.getParent()!=null){ mWM.removeView(mView); } mWM.addView(mView,mParams); trySendAccessibilityEvent(); } }
从源码中,我们知道Toast是通过WindowManager调用addView加载进来的。因此,hide方法自然是WindowManager调用removeView方法来将Toast视图移除。
总结一下,通过对TN类的源码分析,我们知道了TN类是回调对象,其他进程调用tn类的show和hide方法来控制这个Toast的显示和消失。
NotificationManagerService
回到Toast类的show方法中,我们可以看到,这里调用了getService得到INotificationManager服务,源码如下:
privatestaticINotificationManagersService; staticprivateINotificationManagergetService(){ if(sService!=null){ returnsService; } sService=INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); returnsService; }
得到INotificationManager服务后,调用了enqueueToast方法将当前的Toast放入到系统的Toast队列中。传的参数分别是pkg、tn和mDuration。也就是说,我们通过Toast.makeText(context,msg,Toast.LENGTH_SHOW).show()去呈现一个Toast,这个Toast并不是立刻显示在当前的window上,而是先进入系统的Toast队列中,然后系统调用回调对象tn的show和hide方法进行Toast的显示和隐藏。
这里INofiticationManager接口的具体实现类是NotificationManagerService类,位于frameworks/base/services/java/com/android/server/NotificationManagerService.java。
首先,我们来分析一下Toast入队的函数实现enqueueToast,源码如下:
publicvoidenqueueToast(Stringpkg,ITransientNotificationcallback,intduration) { //packageName为null或者tn类为null,直接返回,不进队列 if(pkg==null||callback==null){ return; } //(1)判断是否为系统Toast finalbooleanisSystemToast=isCallerSystem()||("android".equals(pkg)); //判断当前toast所属的pkg是否为系统不允许发生Toast的pkg.NotificationManagerService有一个HashSet数据结构,存储了不允许发生Toast的包名 if(ENABLE_BLOCKED_TOASTS&&!noteNotificationOp(pkg,Binder.getCallingUid())&&!areNotificationsEnabledForPackageInt(pkg)){ if(!isSystemToast){ return; } } synchronized(mToastQueue){ intcallingPid=Binder.getCallingPid(); longcallingId=Binder.clearCallingIdentity(); try{ ToastRecordrecord; //(2)查看该Toast是否已经在队列当中 intindex=indexOfToastLocked(pkg,callback); //如果Toast已经在队列中,我们只需要更新显示时间即可 if(index>=0){ record=mToastQueue.get(index); record.update(duration); }else{ //非系统Toast,每个pkg在当前mToastQueue中Toast有总数限制,不能超过MAX_PACKAGE_NOTIFICATIONS if(!isSystemToast){ intcount=0; finalintN=mToastQueue.size(); for(inti=0;i<N;i++){ finalToastRecordr=mToastQueue.get(i); if(r.pkg.equals(pkg)){ count++; if(count>=MAX_PACKAGE_NOTIFICATIONS){ Slog.e(TAG,"Packagehasalreadyposted"+count +"toasts.Notshowingmore.Package="+pkg); return; } } } } //将Toast封装成ToastRecord对象,放入mToastQueue中 record=newToastRecord(callingPid,pkg,callback,duration); mToastQueue.add(record); index=mToastQueue.size()-1; //(3)将当前Toast所在的进程设置为前台进程 keepProcessAliveLocked(callingPid); } //(4)如果index为0,说明当前入队的Toast在队头,需要调用showNextToastLocked方法直接显示 if(index==0){ showNextToastLocked(); } }finally{ Binder.restoreCallingIdentity(callingId); } } }
可以看到,我对上述代码做了简要的注释。代码相对简单,但是还有4点标注代码需要我们来进一步探讨。
(1)判断是否为系统Toast。如果当前Toast所属的进程的包名为“android”,则为系统Toast,否则还可以调用isCallerSystem()方法来判断。该方法的实现源码为:
booleanisUidSystem(intuid){ finalintappid=UserHandle.getAppId(uid); return(appid==Process.SYSTEM_UID||appid==Process.PHONE_UID||uid==0); } booleanisCallerSystem(){ returnisUidSystem(Binder.getCallingUid()); }
isCallerSystem的源码也比较简单,就是判断当前Toast所属进程的uid是否为SYSTEM_UID、0、PHONE_UID中的一个,如果是,则为系统Toast;如果不是,则不为系统Toast。
是否为系统Toast,通过下面的源码阅读可知,主要有两点优势:
系统Toast一定可以进入到系统Toast队列中,不会被黑名单阻止。
系统Toast在系统Toast队列中没有数量限制,而普通pkg所发送的Toast在系统Toast队列中有数量限制。
(2)查看将要入队的Toast是否已经在系统Toast队列中。这是通过比对pkg和callback来实现的,具体源码如下所示:
privateintindexOfToastLocked(Stringpkg,ITransientNotificationcallback) { IBindercbak=callback.asBinder(); ArrayList<ToastRecord>list=mToastQueue; intlen=list.size(); for(inti=0;i<len;i++){ ToastRecordr=list.get(i); if(r.pkg.equals(pkg)&&r.callback.asBinder()==cbak){ returni; } } return-1; }
通过上述代码,我们可以得出一个结论,只要Toast的pkg名称和tn对象是一致的,则系统把这些Toast认为是同一个Toast。
(3)将当前Toast所在进程设置为前台进程。源码如下所示:
privatevoidkeepProcessAliveLocked(intpid) { inttoastCount=0;//toastsfromthispid ArrayList<ToastRecord>list=mToastQueue; intN=list.size(); for(inti=0;i<N;i++){ ToastRecordr=list.get(i); if(r.pid==pid){ toastCount++; } } try{ mAm.setProcessForeground(mForegroundToken,pid,toastCount>0); }catch(RemoteExceptione){ //Shouldn'thappen. } }
这里的mAm=ActivityManagerNative.getDefault(),调用了setProcessForeground方法将当前pid的进程置为前台进程,保证不会系统杀死。这也就解释了为什么当我们finish当前Activity时,Toast还可以显示,因为当前进程还在执行。
(4)index为0时,对队列头的Toast进行显示。源码如下:
privatevoidshowNextToastLocked(){ //获取队列头的ToastRecord ToastRecordrecord=mToastQueue.get(0); while(record!=null){ try{ //调用Toast的回调对象中的show方法对Toast进行展示 record.callback.show(); scheduleTimeoutLocked(record); return; }catch(RemoteExceptione){ Slog.w(TAG,"Objectdiedtryingtoshownotification"+record.callback +"inpackage"+record.pkg); //removeitfromthelistandlettheprocessdie intindex=mToastQueue.indexOf(record); if(index>=0){ mToastQueue.remove(index); } keepProcessAliveLocked(record.pid); if(mToastQueue.size()>0){ record=mToastQueue.get(0); }else{ record=null; } } } }
这里Toast的回调对象callback就是tn对象。接下来,我们看一下,为什么系统Toast的显示时间只能是2s或者3.5s,关键在于scheduleTimeoutLocked方法的实现。原理是,调用tn的show方法展示完Toast之后,需要调用scheduleTimeoutLocked方法来将Toast消失。(如果大家有疑问:不是说tn对象的hide方法来将Toast消失,为什么要在这里调用scheduleTimeoutLocked方法将Toast消失呢?是因为tn类的hide方法一执行,Toast立刻就消失了,而平时我们所使用的Toast都会在当前Activity停留几秒。如何实现停留几秒呢?原理就是scheduleTimeoutLocked发送MESSAGE_TIMEOUT消息去调用tn对象的hide方法,但是这个消息会有一个delay延迟,这里也是用了Handler消息机制)。
privatestaticfinalintLONG_DELAY=3500;//3.5seconds privatestaticfinalintSHORT_DELAY=2000;//2seconds privatevoidscheduleTimeoutLocked(ToastRecordr) { mHandler.removeCallbacksAndMessages(r); Messagem=Message.obtain(mHandler,MESSAGE_TIMEOUT,r); longdelay=r.duration==Toast.LENGTH_LONG?LONG_DELAY:SHORT_DELAY; mHandler.sendMessageDelayed(m,delay); }
首先,我们看到这里并不是直接发送了MESSAGE_TIMEOUT消息,而是有个delay的延迟。而delay的时间从代码中“longdelay=r.duration==Toast.LENGTH_LONG?LONG_DELAY:SHORT_DELAY;”看出只能为2s或者3.5s,这也就解释了为什么系统Toast的呈现时间只能是2s或者3.5s。自己在Toast.makeText方法中随意传入一个duration是无作用的。
接下来,我们来看一下WorkerHandler中是如何处理MESSAGE_TIMEOUT消息的。mHandler对象的类型为WorkerHandler,源码如下:
privatefinalclassWorkerHandlerextendsHandler { @Override publicvoidhandleMessage(Messagemsg) { switch(msg.what) { caseMESSAGE_TIMEOUT: handleTimeout((ToastRecord)msg.obj); break; } } }
可以看到,WorkerHandler对MESSAGE_TIMEOUT类型的消息处理是调用了handlerTimeout方法,那我们继续跟踪handleTimeout源码:
privatevoidhandleTimeout(ToastRecordrecord) { synchronized(mToastQueue){ intindex=indexOfToastLocked(record.pkg,record.callback); if(index>=0){ cancelToastLocked(index); } } }
handleTimeout代码中,首先判断当前需要消失的Toast所属ToastRecord对象是否在队列中,如果在队列中,则调用cancelToastLocked(index)方法。真相就要浮现在我们眼前了,继续跟踪源码:
privatevoidcancelToastLocked(intindex){ ToastRecordrecord=mToastQueue.get(index); try{ record.callback.hide(); }catch(RemoteExceptione){ //don'tworryaboutthis,we'reabouttoremoveitfrom //thelistanyway } mToastQueue.remove(index); keepProcessAliveLocked(record.pid); if(mToastQueue.size()>0){ //Showthenextone.Ifthecallbackfails,thiswillremove //itfromthelist,sodon'tassumethatthelisthasn'tchanged //afterthispoint. showNextToastLocked(); } }
哈哈,看到这里,我们回调对象的hide方法也被调用了,同时也将该ToastRecord对象从mToastQueue中移除了。到这里,一个Toast的完整显示和消失就讲解结束了。