详解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的完整显示和消失就讲解结束了。