最新消息:欢迎访问Android开发中文站!商务联系微信:loading_in

Android Toast源码详解(全)

热点资讯 loading 111浏览 0评论

Toast源码解析

Android版本: 基于API源码29,Android版本10.0。

Toast是一种弱提示浮窗,实质上是一个视图,它为用户提供一条简短的消息。当Toast视图显示给用户时,是以浮动视图的形式展示在应用程序之上。它永远不会受到关注。用户可能正在输入其他东西。其思想是尽可能不引人注目,同时仍然向用户显示您希望他们看到的信息。例如音量控制,以及提示设置已保存的简短信息。

Toast有以下特点:

  • 系统Window
  • 不会获取焦点。其视图View接收不到用户输入事件。
  • 展示和隐藏由NotificationManagerService管控。

Toast的使用很简单:

Toast.makeText(getApplicationContext(), "Toast", Toast.LENGTH_SHORT).show();

Toast源码分析:

Toast对象的创建可以通过makeText方法,也可以直接通过new 关键字直接创建:

//Toast.java
@hide
public static Toast makeText(Context context,Looper looper,CharSequence text,int duration) {
    //创建Toast对象。
    Toast result = new Toast(context, looper);
    //加载系统布局。
    LayoutInflater inflate = (LayoutInflater)
                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
    View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
    //获取文本View。    
    TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
    //设置文本。
    tv.setText(text);
    //赋值。
    result.mNextView = v;
    result.mDuration = duration;
    return result;
}

ToastmakeToast方法有三个重载方法,其中带Looper参数的方法是外界不能直接调用的,它使用了@hiden注解注释。在方法中,获取了系统内置的LayoutInflater对象,来加载系统布局:

//transient_notification
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:paddingHorizontal="@dimen/screen_percentage_05"
    android:orientation="vertical">
    <TextView
        android:id="@android:id/message"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_weight="1"
        android:minHeight="48dp"
        android:paddingHorizontal="16dp"
        android:paddingVertical="8dp"
        android:gravity="center"
        android:textAppearance="@style/TextAppearance.Toast"
        android:background="?android:attr/toastFrameBackground"
        />
</LinearLayout>

Toast显示的文本就是布局中idmessageTextView展示的。默认情况下Toast的背景色就是当前应用设置的主题色,但在不同的Android版本中背景色的取值又有所不同,这边不再深入分析。当然,除了使用系统布局之外,Toast也支持自定义布局,但要使用new直接创建Toast对象。分析下Toast构造方法的源码:

//Toast.java
public Toast(@NonNull Context context, @Nullable Looper looper) {
    mContext = context;
    //创建TN对象。
    mTN = new TN(context.getPackageName(), looper);
    //Y轴偏移值。
    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对象:

private static class TN extends ITransientNotification.Stub {
}

它是Toast私有的静态内部类,继承自ITransientNotification.Stub类。ITransientNotification是系统定义的AIDL文件,这必定是用来进行跨进程通讯的。

/frameworks/java/android/android/app/ITransientNotification.aidl
package android.app;
/** @hide */
oneway interface ITransientNotification {
   void show(IBinder windowToken);
   void hide();
}

ITransientNotification.aidl中只定义的两个关键的方法,在TN对象中会有对应的实现。了解过Toast的源码之后,会明白Toast的展示跟隐藏,与系统服务NotificationManagerService简称NMS相关。且该服务运行在系统进程中,与NMS交互就必然要涉及到IPC通讯。结合NMS的源码分析,TN就是NMS服务与Toast通讯的桥梁。就像ViewRootImpl类中定义的W类,是WindowManagerServiceViewRootImpl之间通讯的桥梁一样。这也暗示着Toast的展示跟隐藏并不是自身管理的,而是交给系统服务统一管理的。

既然TN类负责与NMS交互,那Toast干脆将Window相关的事情,都交给TN。在TN的构造方法中会初始化WindowManager.LayoutParams的基本属性,包括窗口的两个重要属性typeflag

//Toast$TN.java
TN(String packageName, @Nullable Looper looper) {
            //mParams是预创建好的对象。
            final WindowManager.LayoutParams params = mParams;
            //设置宽高。  
            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
            params.format = PixelFormat.TRANSLUCENT;
            //Window动画。
            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;

            mPackageName = packageName;
            //获取主进程的Looper,如果在子线程中展示Toast的话,会出现异常。
            if (looper == null) {
                looper = Looper.myLooper();
                if (looper == null) {
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            //创建主线程Handler。
            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                        case SHOW: {
                            IBinder token = (IBinder) msg.obj;
                            handleShow(token);
                            break;
                        }
                        case HIDE: {
                            handleHide();
                            mNextView = null;
                            break;
                        }
                        case CANCEL: {
                            handleHide();
                            mNextView = null;
                            try {
                                getService().cancelToast(mPackageName, TN.this);
                            } catch (RemoteException e) {
                            }
                            break;
                        }
                    }
                }
            };
        }

TN的构造方法中,初始化了Window的一些基本属性。ToastWindow类型为TYPE_TOAST,属于系统窗口,在添加到WMS中时,不需要检查应用是否开启悬浮窗权限,但需要验证window token。另外重要的是设置给window的几个flag属性,第一个为FLAG_KEEP_SCREEN_ON,表示只要这个窗口对用户可见,保持设备的屏幕打开和明亮。第二个为FLAG_NOT_FOCUSABLE,表示这个窗口永远不会得到键输入焦点,所以用户不能发送键或其他按钮事件给它,事件将会转发给它背后的window。第三个为FLAG_NOT_TOUCHABLE,表示这个窗口接收不到Touch事件。总结一下就是,Toast虽然是系统Window,但无法获取输入焦点和响应Touch事件。也就是Toast中的View压根接收不到任何输入事件,所以别指望给Toast中的View添加点击事件。Toast的作用仅仅是一种弱提示浮窗,系统设计的目的就是不会让Toast去过多的吸引用户的注意跟用户的操作。如果执意要让Toast响应点击事件的,只有通过反射来动态修改windowflag属性。

除此之外,构造方法中创建了主线程Handler,用来从Binder线程切换到主线程,然后执行Toast的相关操作。

那么Toast的重点肯定就在show()hide()方法中:

//Toast.java
public void show() {
    //Toast的根布局。
    if (mNextView == null) {
       throw new RuntimeException("setView must have been called");
    }
    //获取系统服务
    INotificationManager service = getService();
    String pkg = mContext.getOpPackageName();
    TN tn = mTN;
    tn.mNextView = mNextView;
    final int displayId = mContext.getDisplayId();
    //交给系统服务处理Toast。
    try {
       service.enqueueToast(pkg, tn, mDuration, displayId);
    } catch (RemoteException e) {
            // Empty
    }
}

show方法是Toast类中定义的方法。方法中并没有明显的添加window的逻辑,而是通过getService获取INotificationManager对象,然后执行了enqueueToast方法。先来分析下getService方法:

//Toast.java
static private INotificationManager getService() {
   if (sService != null) {
        return sService;
   }
   sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
   return sService;
}

ServiceManager类被@hide注解标记,对于应用层是不可见的,只能下载Android源码查看。熟悉Binder机制应该对ServiceManager不陌生。在Binder机制中所有的服务端,都需要在Servicemanager服务中注册,但注册的并不是服务端创建的真实的IBinder对象,而是Binder驱动为之创建的代理IBinder对象。然后将服务端的名称跟代理IBinder对象一并发送给ServicemanagerServiceManager接收到数据之后,将数据保存在Map集合中。服务端的名称为key,代理IBinder对象为value保存下来。并提供了getService静态方法,来通过服务端的名称,比如说notification,来获取服务端的Binder代理对象。

回到源码中,ServiceManager.getService("notification")方法,是获取注册在ServiceManager中的名字为notification的服务端代理对象IBinder,通过asInterface()方法获取服务端的代理对象。在系统服务NotificationManagerService类中,创建了属性名为mServiceINotificationManager.Stub对象。在服务启动之后会执行onStart方法,方法中会通过SystemServicepublishBinderService方法,将mService表示的本地代理对象注册到ServiceManager中。但要区分一下的是,通过SM获取的代理对象,跟mService表示的对象并非同一个对象。在注册为了Binder服务端的过程中,Binder驱动保存了真实的INotificationManager.Stub对象,然后创建一个代理对象,将代理对象跟注册服务的名称一并交给ServiceManager

所以Toast#getService()方法最终获取的是NotificationManagerService类中创建的INotificationManager.Stub对象,enqueueToast方法真正的实现也在该代理对象中:

//NotificationManagerService.java
final IBinder mService = new INotificationManager.Stub() {
        @Override
  public void enqueueToast(String pkg, ITransientNotification callback, int duration,
                                                                        int displayId){
      //判断展示Toast的进程是否是系统进程。     
      final boolean isSystemToast = isCallerSystemOrPhone()
                    || PackageManagerService.PLATFORM_PACKAGE_NAME.equals(pkg);
      //省略代码   
      try {
           //判断是否需要展示Toast。
           if (ENABLE_BLOCKED_TOASTS && !isSystemToast && ((notificationsDisabledForPackage && !appIsForeground) || isPackageSuspended)) {
               return;
            }
           }
         synchronized (mToastQueue) {
            int callingPid = Binder.getCallingPid();
            long callingId = Binder.clearCallingIdentity();
            try {
                 ToastRecord record;
                 int index = indexOfToastLocked(pkg, callback);
                 //如果当前要展示的Toast存在队列中,则更新Toast的展示时间。
                 if (index >= 0) {
                     record = mToastQueue.get(index);
                     record.update(duration);
                 } else {
                     //判断是否是系统Toast。一般应用创建的Toast都不是系统Toast。
                     if (!isSystemToast) {
                         int count = 0;
                         final int N = mToastQueue.size();
                         for (int i=0; i<N; i++) {
                              final ToastRecord r = mToastQueue.get(i);
                              //判断同一个应用展示Toast的个数,超过25个
                              if (r.pkg.equals(pkg)) {
                                  count++;
                                  if (count >= MAX_PACKAGE_NOTIFICATIONS) {
                                      return;
                                  }
                              }
                         }
                     }
                     //创建Binder对象。
                     Binder token = new Binder();
                     //想WindowManager添加Token。
                     mWindowManagerInternal.addWindowToken(token, TYPE_TOAST, displayId);       
                     //创建ToastRecord对象,包含了该Toast的信息。
                     record = new ToastRecord(callingPid, pkg, callback, duration, token,displayId);
                     //添加到队列中。
                     mToastQueue.add(record);
                     //获取当前添加的Toast的小标。
                     index = mToastQueue.size() - 1;
                     keepProcessAliveIfNeededLocked(callingPid);
                    }
                    //如果当前添加的Toast就是第一个Toast,则显示该Toast。
                    if (index == 0) {
                        showNextToastLocked();
                    }
                } finally {
                    Binder.restoreCallingIdentity(callingId);
                }
            }
        }

    int indexOfToastLocked(String pkg, ITransientNotification callback){
        //获取`Toast`在NMS服务中的代理对象IBinder。
        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() == cbak) {
                return i;
            }
        }
        return -1;
}

enqueueToast()方法决定了当前创建的Toast是否需要展示和是否需要马上展示。在try语句一开始的if语句中,判断了什么情况下不展示Toast,比如说创建Toast的进程被挂起时,不展示Toast窗口。在Toast符合需要展示的条件之后,接着判断当前Toast是否在等待队列中。这种情况还是比较常见的,当应用频繁的触发同一个Toast对象的show()方法。另外系统判断两个Toast相同的条件,就是创建Toast的包名和TN对象相等。包名相等说明是同一个应用展示的,TN对象相等说明是同一个Toast对象,因为Toast对象跟TN对象是一一对应的。当在mToastQueue集合中找到相同的Toast的时候,其返回的index大于等于0,说明此时已有一个Toast窗口正在屏幕中展示,然后又使用同一个Toast对象执行show方法。虽然Toast允许这样的操作,但不同的Android版本中,对这种情况的处理逻辑会有所不同,这将影响Toast的展示效果。比如说,单例Toast在小于展示时间之内,多次调用其show方法时,在Android 10.0版本和在Android 9.0版本下会有完全不同的展示效果:

  1. Android版本10.0及其除了9.0之外的版本(效果一样,源码可能会有一点的差异):
    //enqueueToast方法。
    if (index >= 0) {
        record = mToastQueue.get(index);
        record.update(duration);
    }
    

    当前要展示的Toast在队列中存在的时候,仅更新Toast的显示时间。这样的效果就是,在LENGTH_LONG或者LENGTH_SHORT时间范围内,多次调用Toast#show()方法时,Toast在设置的时间范围内仅展示一次,继续点击将无任何反应 。需等到该次ToastWindow移除之后,再点击才有效果,该源码稍后会在分析。

  2. Android版本9.0
    //enqueueToast方法。
    if (index >= 0) {
       record = mToastQueue.get(index);
       record.update(duration);
       try {
             record.callback.hide();
           } catch (RemoteException e) {}
             record.update(callback);
    }
    

    当前要展示的Toast在队列中存在的时候,先更新其展示时间,再执行hide()方法将Toast隐藏起来,在更新callback回调。这样的效果就是,在LENGTH_LONG或者LENGTH_SHORT时间范围内,**多次调用Toast#show()方法时,已经展示的Toast会马上消失掉,但并不会重新展示新的Toast,继续点击将无任何反应。**需等到该次ToastWindow移除之后,再点击才有效果。

这也提示开发者,在封装Toast工具类时,应根据Toast的使用场景来选择是否采用单例模式。尽大可能的避免在需要多次展示Toast的场景下,Toast就是不展示的问题。

回到源码中,接着分析else之后的逻辑。这里在展示Toast之前,如果当前的Toast并不是系统进程展示的Toast,一般应用创建的都不是系统Toast。所以会先判断当前队列中缓存的同包名下的Toast,是否已经超过了系统设置的上限。在Android 10.0中默认上限是25。如果超过了上限,则不会处理此时Toast的展示请求。NotificationManagerService是系统界别的服务,手机中的所有应用在展示Toast的时候,都需要它来管理是否展示跟消失。如果对应用没有任何限制的话,那么手机屏幕中的Toast就会无规则的展示,可能你当前看到的提示内容,并不是来自你当前交互的应用。这也是服务为什么要将Toast加入到队列中的一个原因。

接着分析,当符合以上的条件之后,就需要保存当前Toast的数据,并准备好window tokenToast属于系统Window,在添加到WMS中时,WMS会检查其window token的正确性。通过验证token是否已经在WMS中注册过,只有注册过TokenWindow才能展示View。所以在Toast展示之前会先创建一个Binder对象,作为当前Toastwindow token。接着执行addWindowToken()方法,将当前的Binder对象注册在WMS中。注册之后再将创建的Binder对象,以及当前Toast的信息,保存在ToastRecord对象中。ToastRecordNMS服务中就代表了一个Toast,就跟ActivityRecord在系统服务中代表Activity一样。ToastRecord对象保存了Toast的基本信息,包括当前Toast在服务端中的代理对象ITransientNotification,也就是Toast中创建的TN代理对象 。毕竟此时代码的运行环境还在系统进程中,NMS需要借助ToastRecord对象中的TN对象,来给创建Toast的进程通讯。接着执行showNextToastLocked()方法,来通知客户端应用展示Toast View

//NotificationManagerService.java   
void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            //执行`TN`对象的show(Binder windowToken)方法。
            try {
                record.callback.show(record.token);
                scheduleDurationReachedLocked(record);
                return;
            } catch (RemoteException e) {
               //如果IPC通讯中断的话,就移除掉当前的Toast。
                int index = mToastQueue.indexOf(record);
                if (index >= 0) {
                    mToastQueue.remove(index);
                }
                keepProcessAliveIfNeededLocked(record.pid);
                //继续找下一个Toast显示。
                if (mToastQueue.size() > 0) {
                    record = mToastQueue.get(0);
                } else {
                    record = null;
                }
            }
        }
    }

mToastQueue队列中获取第一个要展示的Toast,然后调用callback.show()方法。callback表示的ITransientNotification对象,之前已经解释过了,就是Toast中创建的TNNMS中的代理对象,之后调用其show方法来通知创建Toast的客户端,展示Toast。这属于IPC通讯,如果代码捕获到RemoteException异常,表示此次跨进程通讯失败,则放弃该Toast的显示,从队列中取出下一个ToastRecord继续展示。以上代码逻辑比较简单,接着回到Toast#TB中分析下show(Binder)方法:

//Toast$TN.java
public void show(IBinder windowToken) {
   mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
}

IPC通讯时,服务端跟客户端通讯的代码逻辑都执行在Binder线程池中。所以当前TN#show()代码的执行环境是在Binder线程中,而ViewRootImpl绘制View的线程要求是在UI线程中,在其requestLayout()方法中会检查线程。所以,这里就需要使用主线程的Handler,将代码执行的环境切换到主线程中,之后再执行handleShow逻辑:

//Toast$TN.java
public void handleShow(IBinder windowToken) {
      if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
          return;
      }
      f (mView != mNextView) {
         //如果Toast更换了`View`需要先将之前的View隐藏掉。
         handleHide();
         //赋值。mView一开始为null。 
         mView = mNextView;
         //获取ApplicationCOntext。 
         Context context = mView.getContext().getApplicationContext();
         //获取应用包名。 
         String packageName = mView.getContext().getOpPackageName();
         if (context == null) {
             context = mView.getContext();
         }
         //获取系统WindowmanagerImpl。注意是获取预置在系统中的。 
         mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
         //省略代码。。。
         //设置包名。 
         mParams.packageName = packageName;
         //Toast消失的时间。 
         mParams.hideTimeoutMilliseconds = mDuration ==
                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
         //token赋值。 
         mParams.token = windowToken;
         //如果当前View已经跟ViewRootImpl建立了联系的话,就先移除掉。
         if (mView.getParent() != null) {
             mWM.removeView(mView);
          }
          //由于通知管理器服务在通知我们取消toast之后立即取消了令牌,因此存在一个固有的竞争,我们可以尝试在令牌无效之后添加一个窗口。让我们来对冲一下。
          try {
              //添加Window。
               mWM.addView(mView, mParams);
               trySendAccessibilityEvent();
          } catch (WindowManager.BadTokenException e) {
            /* ignore */
          }
      }
}

handleShow方法的主要逻辑就是添加window窗口。不过源码中有一个小细节,在获取WindowManager对象的时候采用的是ApplicationContext,并没有直接使用在创建Toast对象时传进来的mContext对象。这样做的目的是确保通过Context对象获取到的WindowManager对象是系统预置的。我们知道在创建Toast的时候传入的Context对象一般会使用Activity对象,这样的话直接使用content.getSystemService的话,会导致获取的WindowManager对象,是Activity自身创建的带有TokenWindowManager对象。而Toast作为系统窗口,根本不需要应用token,所以为了避免发生不必要的问题,这里统一使用了ApplicationContext

注意一下handleShow方法的参数是一个IBinder对象,该对象在NMS#enqueueToast()方法中创建的。之前已经讲过,Toast在展示之前,NMS会创建一个Binder对象,作为Toast窗口在WMS中的令牌。调用 mWM.addView()方法之后,WMS会检查window token的正确性,但这只是在Android 8.0以及之后的版本中才会检查。Android 8.0版本之前,系统是允许展示没有tokenToast,在添加window时不会去检查token。到8.0版本之后,系统要求Toast必须拥有token,所以应用程序不能直接添加Toast,因为令牌是由NMS添加的。这也是为什么Toast的展示要交给NMS来处理。

mWM.addView()方法执行之后,Toast中的跟View就跟ViewRootImpl建立了联系,接着就会执行View的绘制流程,Toast就展示在屏幕中了。展示的逻辑这边就分析结束了,那Toast是怎么自动消失的呢?问题的答案还要回到NMS#showNextToastLocked()方法中:

//NotificationManagerService.java   
void showNextToastLocked() {
        ToastRecord record = mToastQueue.get(0);
        while (record != null) {
            try {
                //展示Toast。
                record.callback.show(record.token);
                //开启倒计时。
                scheduleDurationReachedLocked(record);
                return;
            } catch (RemoteException e) {
               //省略源码。。。
            }
        }
}

private void scheduleDurationReachedLocked(ToastRecord r){
        mHandler.removeCallbacksAndMessages(r);
        Message m = Message.obtain(mHandler, MESSAGE_DURATION_REACHED, r);
        int delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;
        //省略掉开启手机“辅助功能”的逻辑。
        mHandler.sendMessageDelayed(m, delay);
 }

在执行完show逻辑之后,就往NMS#mHandler中发送了一个延迟消息。该延迟的时间由ToastDuration决定,只有两个取值,LENGTH_LONG = 3.5s LENGTH_SHORT = 2s。对于Toast而言,展示的时间由系统控制,一般来说不需要对展示的时间有定制化需求,否则的话就只能使用反射动态的修改了。当延迟时间到了之后,最终会执行NMS#cancelToastLocked()方法:

//NotificationManagerService.java
void cancelToastLocked(int index) {
        //根据index获取ToastRecord对象。
        ToastRecord record = mToastQueue.get(index);
        try {
            //调用隐藏方法。
            record.callback.hide();
        } catch (RemoteException e) {
        }
        ToastRecord lastToast = mToastQueue.remove(index);
        mWindowManagerInternal.removeWindowToken(lastToast.token, false,
                lastToast.displayId);
        //我们将“removeWindows”传递为“false”,这样客户端就有时间停止渲染(因为上面的隐藏是一个单向消息),否则我们可能会导致客户端崩溃,因为客户端正在使用令牌生成的表面。然而,我们需要安排一个超时,以确保令牌最终以某种方式被杀死。
        scheduleKillTokenTimeout(lastToast);
        keepProcessAliveIfNeededLocked(record.pid);
        if (mToastQueue.size() > 0) {
            //开启下一个Toast的展示。
            showNextToastLocked();
        }
}

NMS中的ToastRecord对象就表示了Toast对象,在NMS中的方法中传递,控制着Toast的显示跟隐藏。方法中参数index是在handleDurationReached()方法中,根据ToastRecordpkg、callback获取的。然后执行hide()方法,这部分逻辑后续在分析。Toast隐藏之后,系统需要回收当前Toast的显示区域,注意这里调用removeWindowToken()方法的第二个参数是false,表示先不清除window token,先发送一个延迟消息,确保token最终肯定会被杀死。继续执行showNextToastLocked()方法,从队列中获取下一个Toast展示。

以下重点分析下hide()方法。该方法调用还是一次IPC通讯,最终会调用到Toast#handleHide()方法中:

//Toast.java
public void handleHide() {
      if (mView != null) {
         //注意:检查parent()只是为了确保视图已经被添加…我见过视图还没有添加的情况,所以让我们尽量不要崩溃。
         if (mView.getParent() != null) {
             mWM.removeViewImmediate(mView);
         }
         //既然我们已经删除了视图,服务器就可以安全地释放资源了。
         try {
             getService().finishToken(mPackageName, this);
         } catch (RemoteException e) {
         }
         mView = null;
      }
}

接收到隐藏消息之后,就通知NMS来立马移除掉windowNMS先在队列中将该Toast对应的ToastRecord移除掉,接着会再次执行removeWindowToken()方法,方法中的第二个参数传的是true,也就是将token跟显示区域一并的清除掉。最后将mView变量置为null,这样就可以使用同一个Toast对象多次显示。

转载请注明:Android开发中文站 » Android Toast源码详解(全)

您必须 登录 才能发表评论!