详解Android中的Toast源码


Toast源码实现
Toast入口
    我们在应用中使用Toast提示的时候,一般都是一行简单的代码调用,如下所示:
[java] view plaincopyprint?在CODE上查看代码片派生到我的代码片

  Toast.makeText(context, msg, Toast.LENGTH_SHORT).show(); 

    makeText就是Toast的入口,我们从makeText的源码来深入理解Toast的实现。源码如下(frameworks/base/core/java/android/widget/Toast.java):

  public static Toast makeText(Context context, CharSequence text, int duration) { 
    Toast result = new Toast(context); 
   
    LayoutInflater inflate = (LayoutInflater) 
        context.getSystemService(Context.LAYOUT_INFLATER_SERVICE); 
    View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null); 
    TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message); 
    tv.setText(text); 
     
    result.mNextView = v; 
    result.mDuration = duration; 
   
    return result; 
  } 

    从makeText的源码里,我们可以看出Toast的布局文件是transient_notification.xml,位于frameworks/base/core/res/res/layout/transient_notification.xml:

  <?xml version="1.0" encoding="utf-8"?> 
  <LinearLayout xmlns: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()方法,研究一下布局形成之后的展示代码实现:

  

 public void show() { 
    if (mNextView == null) { 
      throw new RuntimeException("setView must have been called"); 
    } 
   
    INotificationManager service = getService(); 
    String pkg = mContext.getPackageName(); 
    TN tn = mTN; 
    tn.mNextView = mNextView; 
   
    try { 
      service.enqueueToast(pkg, tn, mDuration); 
    } catch (RemoteException e) { 
      // Empty 
    } 
  } 

    show方法中有两点是需要我们注意的。(1)TN是什么东东?(2)INotificationManager服务的作用。带着这两个问题,继续我们Toast源码的探索。
TN源码
    很多问题都能通过阅读源码找到答案,关键在与你是否有与之匹配的耐心和坚持。mTN的实现在Toast的构造函数中,源码如下:

  public Toast(Context context) { 
    mContext = context; 
    mTN = new TN(); 
    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源码如下:

 

  private static class TN extends ITransientNotification.Stub { 
    final Runnable mShow = new Runnable() { 
      @Override 
      public void run() { 
        handleShow(); 
      } 
    }; 
   
    final Runnable mHide = new Runnable() { 
      @Override 
      public void run() { 
        handleHide(); 
        // Don't do this in handleHide() because it is also invoked by handleShow() 
        mNextView = null; 
      } 
    }; 
   
    private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); 
    final Handler mHandler = new Handler();   
   
    int mGravity; 
    int mX, mY; 
    float mHorizontalMargin; 
    float mVerticalMargin; 
   
   
    View mView; 
    View mNextView; 
   
    WindowManager mWM; 
   
    TN() { 
      // XXX This should be changed to use a Dialog, with a Theme.Toast 
      // defined that sets up the layout params appropriately. 
      final WindowManager.LayoutParams params = 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] Support multi-user 
      params.privateFlags = WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS; 
    } 
   
    /** 
     * schedule handleShow into the right thread 
     */ 
    @Override 
    public void show() { 
      if (localLOGV) Log.v(TAG, "SHOW: " + this); 
      mHandler.post(mShow); 
    } 
   
    /** 
     * schedule handleHide into the right thread 
     */ 
    @Override 
    public void hide() { 
      if (localLOGV) Log.v(TAG, "HIDE: " + this); 
      mHandler.post(mHide); 
    } 
   
    public void handleShow() { 
      if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView 
          + " mNextView=" + mNextView); 
      if (mView != mNextView) { 
        // remove the old view if necessary 
        handleHide(); 
        mView = mNextView; 
        Context context = mView.getContext().getApplicationContext(); 
        if (context == null) { 
          context = mView.getContext(); 
        } 
        mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 
        // We can resolve the Gravity here by using the Locale for getting 
        // the layout direction 
        final Configuration config = mView.getContext().getResources().getConfiguration(); 
        final int gravity = 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(); 
      } 
    } 
   
    private void trySendAccessibilityEvent() { 
      AccessibilityManager accessibilityManager = 
          AccessibilityManager.getInstance(mView.getContext()); 
      if (!accessibilityManager.isEnabled()) { 
        return; 
      } 
      // treat toasts as notifications since they are used to 
      // announce a transient piece of information to the user 
      AccessibilityEvent event = AccessibilityEvent.obtain( 
          AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED); 
      event.setClassName(getClass().getName()); 
      event.setPackageName(mView.getContext().getPackageName()); 
      mView.dispatchPopulateAccessibilityEvent(event); 
      accessibilityManager.sendAccessibilityEvent(event); 
    }     
   
    public void handleHide() { 
      if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView); 
      if (mView != null) { 
        // note: checking parent() just to make sure the view has 
        // been added... i have seen cases where we get here when 
        // the view isn't yet added, so let's try not to crash. 
        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,源码如下:

  package android.app; 
   
  /** @hide */ 
  oneway interface ITransientNotification { 
    void show(); 
    void hide(); 
  } 

    ITransientNotification定义了两个方法show()和hide(),它们的具体实现就在TN类当中。TN类的实现为:

  /** 
   * schedule handleShow into the right thread 
   */ 
  @Override 
  public void show() { 
    if (localLOGV) Log.v(TAG, "SHOW: " + this); 
    mHandler.post(mShow); 
  } 
   
  /** 
   * schedule handleHide into the right thread 
   */ 
  @Override 
  public void hide() { 
    if (localLOGV) Log.v(TAG, "HIDE: " + this); 
    mHandler.post(mHide); 
  } 

    这里我们就能知道,Toast的show和hide方法实现是基于Handler机制。而TN类中的Handler实现是:

  final Handler mHandler = new Handler();   

    而且,我们在TN类中没有发现任何Looper.perpare()和Looper.loop()方法。说明,mHandler调用的是当前所在线程的Looper对象。所以,当我们在主线程(也就是UI线程中)可以随意调用Toast.makeText方法,因为Android系统帮我们实现了主线程的Looper初始化。但是,如果你想在子线程中调用Toast.makeText方法,就必须先进行Looper初始化了,不然就会报出java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare() 。Handler机制的学习可以参考我之前写过的一篇博客:http://blog.csdn.net/wzy_1988/article/details/38346637。
    接下来,继续跟一下mShow和mHide的实现,它俩的类型都是Runnable。

 

  final Runnable mShow = new Runnable() { 
    @Override 
    public void run() { 
      handleShow(); 
    } 
  }; 
   
  final Runnable mHide = new Runnable() { 
    @Override 
    public void run() { 
      handleHide(); 
      // Don't do this in handleHide() because it is also invoked by handleShow() 
      mNextView = null; 
    } 
  }; 

    可以看到,show和hide的真正实现分别是调用了handleShow()和handleHide()方法。我们先来看handleShow()的具体实现:
   

 public void handleShow() { 
    if (mView != mNextView) { 
      // remove the old view if necessary 
      handleHide(); 
      mView = mNextView; 
      Context context = mView.getContext().getApplicationContext(); 
      if (context == null) { 
        context = mView.getContext(); 
      } 
      mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE); 
      // We can resolve the Gravity here by using the Locale for getting 
      // the layout direction 
      final Configuration config = mView.getContext().getResources().getConfiguration(); 
      final int gravity = 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服务,源码如下:

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

    得到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,源码如下:

  public void enqueueToast(String pkg, ITransientNotification callback, int duration) 
  { 
    // packageName为null或者tn类为null,直接返回,不进队列 
    if (pkg == null || callback == null) { 
      return ; 
    } 
   
    // (1) 判断是否为系统Toast 
    final boolean isSystemToast = 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) { 
      int callingPid = Binder.getCallingPid(); 
      long callingId = Binder.clearCallingIdentity(); 
      try { 
        ToastRecord record; 
        // (2) 查看该Toast是否已经在队列当中 
        int index = indexOfToastLocked(pkg, callback); 
        // 如果Toast已经在队列中,我们只需要更新显示时间即可 
        if (index >= 0) { 
          record = mToastQueue.get(index); 
          record.update(duration); 
        } else { 
          // 非系统Toast,每个pkg在当前mToastQueue中Toast有总数限制,不能超过MAX_PACKAGE_NOTIFICATIONS 
          if (!isSystemToast) { 
            int count = 0; 
            final int N = mToastQueue.size(); 
            for (int i=0; i<N; i++) { 
               final ToastRecord r = mToastQueue.get(i); 
               if (r.pkg.equals(pkg)) { 
                 count++; 
                 if (count >= MAX_PACKAGE_NOTIFICATIONS) { 
                   Slog.e(TAG, "Package has already posted " + count 
                      + " toasts. Not showing more. Package=" + pkg); 
                   return; 
                 } 
               } 
            } 
          } 
   
          // 将Toast封装成ToastRecord对象,放入mToastQueue中 
          record = new ToastRecord(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()方法来判断。该方法的实现源码为:

 

  boolean isUidSystem(int uid) { 
    final int appid = UserHandle.getAppId(uid); 
    return (appid == Process.SYSTEM_UID || appid == Process.PHONE_UID || uid == 0); 
  } 
  boolean isCallerSystem() { 
    return isUidSystem(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来实现的,具体源码如下所示:

 

  private 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() == cbak) { 
        return i; 
      } 
    } 
    return -1; 
  } 

    通过上述代码,我们可以得出一个结论,只要Toast的pkg名称和tn对象是一致的,则系统把这些Toast认为是同一个Toast。
    (3) 将当前Toast所在进程设置为前台进程。源码如下所示:

  private void keepProcessAliveLocked(int pid) 
  { 
    int toastCount = 0; // toasts from this pid 
    ArrayList<ToastRecord> list = mToastQueue; 
    int N = list.size(); 
    for (int i=0; i<N; i++) { 
      ToastRecord r = list.get(i); 
      if (r.pid == pid) { 
        toastCount++; 
      } 
    } 
    try { 
      mAm.setProcessForeground(mForegroundToken, pid, toastCount > 0); 
    } catch (RemoteException e) { 
      // Shouldn't happen. 
    } 
  } 

    这里的mAm=ActivityManagerNative.getDefault(),调用了setProcessForeground方法将当前pid的进程置为前台进程,保证不会系统杀死。这也就解释了为什么当我们finish当前Activity时,Toast还可以显示,因为当前进程还在执行。
    (4) index为0时,对队列头的Toast进行显示。源码如下:

 

  private void showNextToastLocked() { 
    // 获取队列头的ToastRecord 
    ToastRecord record = mToastQueue.get(0); 
    while (record != null) { 
      try { 
        // 调用Toast的回调对象中的show方法对Toast进行展示 
        record.callback.show(); 
        scheduleTimeoutLocked(record); 
        return; 
      } catch (RemoteException e) { 
        Slog.w(TAG, "Object died trying to show notification " + record.callback 
            + " in package " + record.pkg); 
        // remove it from the list and let the process die 
        int index = 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消息机制)。

 

  private static final int LONG_DELAY = 3500; // 3.5 seconds 
  private static final int SHORT_DELAY = 2000; // 2 seconds 
  private void scheduleTimeoutLocked(ToastRecord r) 
  { 
    mHandler.removeCallbacksAndMessages(r); 
    Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r); 
    long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY; 
    mHandler.sendMessageDelayed(m, delay); 
  } 

    首先,我们看到这里并不是直接发送了MESSAGE_TIMEOUT消息,而是有个delay的延迟。而delay的时间从代码中“long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;”看出只能为2s或者3.5s,这也就解释了为什么系统Toast的呈现时间只能是2s或者3.5s。自己在Toast.makeText方法中随意传入一个duration是无作用的。
    接下来,我们来看一下WorkerHandler中是如何处理MESSAGE_TIMEOUT消息的。mHandler对象的类型为WorkerHandler,源码如下:

  private final class WorkerHandler extends Handler 
  { 
    @Override 
    public void handleMessage(Message msg) 
    { 
      switch (msg.what) 
      { 
        case MESSAGE_TIMEOUT: 
          handleTimeout((ToastRecord)msg.obj); 
          break; 
      } 
    } 
  } 

    可以看到,WorkerHandler对MESSAGE_TIMEOUT类型的消息处理是调用了handlerTimeout方法,那我们继续跟踪handleTimeout源码:

  private void handleTimeout(ToastRecord record) 
  { 
    synchronized (mToastQueue) { 
      int index = indexOfToastLocked(record.pkg, record.callback); 
      if (index >= 0) { 
        cancelToastLocked(index); 
      } 
    } 
  } 

    handleTimeout代码中,首先判断当前需要消失的Toast所属ToastRecord对象是否在队列中,如果在队列中,则调用cancelToastLocked(index)方法。真相就要浮现在我们眼前了,继续跟踪源码:

  private void cancelToastLocked(int index) { 
    ToastRecord record = mToastQueue.get(index); 
    try { 
      record.callback.hide(); 
    } catch (RemoteException e) { 
      // don't worry about this, we're about to remove it from 
      // the list anyway 
    } 
    mToastQueue.remove(index); 
    keepProcessAliveLocked(record.pid); 
    if (mToastQueue.size() > 0) { 
      // 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(); 
    } 
  } 

    哈哈,看到这里,我们回调对象的hide方法也被调用了,同时也将该ToastRecord对象从mToastQueue中移除了。到这里,一个Toast的完整显示和消失就讲解结束了。


« 
» 
快速导航

Copyright © 2016 phpStudy | 豫ICP备2021030365号-3