当前位置: 移动技术网 > IT编程>开发语言>Java > 详解Android中的Toast源码

详解Android中的Toast源码

2019年07月22日  | 移动技术网IT编程  | 我要评论

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网