Toast源码解析

来源:互联网 发布:北京行知小学百度百科 编辑:程序博客网 时间:2024/05/22 06:42

Toast是我们平时开发过程中常用的一个类,用于弹出一个文本提示,使用方法也非常简单,不过看似简单的Toast也并没有大家想象的那么简单,不信,有个小问题问问大家,Toast可以在子线程中弹出吗?大家可以先想一想这个问题,文章的最后我们会公布答案。

Toast的入口

我们直接从最常用Toast的使用方法开始分析Toast的原理,Toast的用法如下:

Toast.makeText(this,"I am a Toast",Toast.LENGTH_SHORT).show();

Toast的makeText方法

该方法主要做了3项工作,我们来逐项分析:

  • 创建Toast对象
  • 加载Toast布局文件,并设置文本内容
  • 保存Toast布局和显示时间到Toast对象中
public static Toast makeText(Context context, CharSequence text, @Duration int duration) {    //1,创建Toast对象    Toast result = new Toast(context);    //2,获取布局加载器,并加载布局设置文本    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);    //3,将布局和持续时间保存到Toast对象中    result.mNextView = v;    result.mDuration = duration;    return result;    }

Toast的构造方法

Toast的构造方法看起来很简单,仅仅是保存了context,创建了TN对象,并将Toast显示的垂直位置和对齐属性保存在TN中,但是有一个问题,TN对象究竟是何方神圣呢?

public Toast(Context context) {    mContext = context;    //创建TN对象    mTN = new TN();    //保存Toast显示的垂直位置    mTN.mY = context.getResources().getDimensionPixelSize(            com.android.internal.R.dimen.toast_y_offset);    //保存Toast显示的对齐方式    mTN.mGravity = context.getResources().getInteger(            com.android.internal.R.integer.config_toastDefaultGravity);}

TN类的构造方法

TN继承自ITransientNotification.Stub,熟悉android binder机制的同学看到XXX.Stub这个名称就可以知道这个类是一个AIDL文件自动生成的binder服务端,该类同时继承了ITransientNotification 接口,提供了show和hide2个方法供客户端也就是NMS调用。
在该类的构造方法中,我们初始化了Toast窗口所需要的布局参数,为以后将Toast窗口添加到wms中提供了方便。不熟悉binder机制的同学可以查看下面链接了解binder的大概用法。
http://blog.csdn.net/huachao1001/article/details/51504469
http://www.jianshu.com/p/1eff5a13000d

oneway interface ITransientNotification {    void show();    void hide();}
private static class TN extends ITransientNotification.Stub{  TN() {        //获取Toast窗口布局参数        final WindowManager.LayoutParams params = mParams;        //窗口宽高为wrap_content        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;        //窗口类型为TOAST,这个参数很重要,决定了它在wms中如何排列        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;    }}

Toast类默认布局文件

Toast类默认的布局文件就是上面makeText方法中加载的transient_notification.xml,可以看到,该布局非常简单,就是一个LinearLayout加上TextView来显示文本。

<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的show方法

该方法主要分为3个步骤
- mNextView(需要加载的布局)为空,则抛出异常。
- 获取NotificationManagerService服务。
- 调用NMS的enqueueToast方法将Toast加入系统Toast队列。

public void show() {    //如果mNextView方法为空,则抛出异常    //还记得我们之前看的makeText方法将加载出来的view保存在这里么    if (mNextView == null) {        throw new RuntimeException("setView must have been called");    }    //1,获取NotificationManager服务    INotificationManager service = getService();    String pkg = mContext.getOpPackageName();    //将需要加载的View保存到TN中    TN tn = mTN;    tn.mNextView = mNextView;    try {        //2,binder跨进程通信,调用NMS的enqueueToast方法        service.enqueueToast(pkg, tn, mDuration);    } catch (RemoteException e) {        // Empty    }}

Toast的getService方法

Toast类中使用sService静态变量保存了NotificationManagerService这个系统服务在客户端的Binder代理对象,如果该对象不为空,则直接返回。否则我们从ServiceManager类查询NMS服务,并将它转化为Binder代理对象保存到sService变量中。熟悉binder机制的同学应该可以知道,这也是老套路了。

static private INotificationManager getService() {    //如果sService不为空,直接返回    if (sService != null) {        return sService;    }    //获取NotificationManagerService服务的Binder客户端代理    sService =INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));    return sService;}

NMS类的enqueueToast方法

  • 在NMS(NotificationManagerService)中,使用了一个ToastRecord类型的列表mToastQueue来保存所有的Toast。
  • 对于新的Toast请求,如果该ToastRecord已经在队列中存在,仅仅更新它的显示时间,否则我们就创建一个ToastRecord对象记录该Toast的信息,并将它插入到列表末尾。
  • 如果我们的Toast是系统中的第一个Toast,那么在调用 showNextToastLocked方法调度Toast开始显示。

该方法的详细流程图如下:

public void enqueueToast(String pkg, ITransientNotification callback, int duration){    //如果是System(1000)或者phone的uid(1001),或者包名为android(这个一般是指framework-res.apk)    final boolean isSystemToast = isCallerSystem() || ("android".equals(pkg));    final boolean isPackageSuspended =            isPackageSuspendedForUser(pkg, Binder.getCallingUid());    //同步ToastRecord列表    synchronized (mToastQueue) {        //获取pid和callId        int callingPid = Binder.getCallingPid();        long callingId = Binder.clearCallingIdentity();        try {            ToastRecord record;            //1,根据callback和包名查找ToastRecord在列表中的位置            int index = indexOfToastLocked(pkg, callback);            //如果ToastRecord已经存在,则更新它的显示时间信息            if (index >= 0) {                record = mToastQueue.get(index);                record.update(duration);            } else {                //下面这些代码的意思是,如果不是系统Toast,                //则限制同一个包名最多可以存在Toast的个数为50个                //应该是为了防止应用程序恶意的无限产生Toast                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) {                                 return;                             }                         }                    }                }                //2,创建ToastRecord对象,记录Toast信息                record = new ToastRecord(callingPid, pkg, callback, duration);                //将ToastRecord对象加入列表                mToastQueue.add(record);                //获取列表最末端的一个元素                index = mToastQueue.size() - 1;                //这里是通知AMS将想要显示Toast的进程设置为前台进程                //防止该进程被系统杀死,导致Toast无法显示                keepProcessAliveLocked(callingPid);            }            //3,这里的index代表的是当前插入的ToastRecord在列表中的位置            //如果当前的ToastRecord已经在列表的头部了,那么直接显示它。            //我们在此先假设系统中就只有这一条Toast,因此执行            //showNextToastLocked方法            if (index == 0) {                showNextToastLocked();            }        } finally {            Binder.restoreCallingIdentity(callingId);        }    }}

NMS的indexOfToastLocked方法

该方法根据包名和ITransientNotification类的变量callback,查找需要的ToastRecord在全局列表mToastQueue中的位置,如果未找到则返回-1,mToastQueue是一个ArrayList,存储了系统中所有要显示的Toast的信息。

//全局变量,存储了所有要显示的Toast的信息ArrayList<ToastRecord> mToastQueue = new ArrayList<ToastRecord>(); int indexOfToastLocked(String pkg, ITransientNotification callback){    IBinder cbak = callback.asBinder();    ArrayList<ToastRecord> list = mToastQueue;    int len = list.size();    //遍历ToastRecord列表    for (int i=0; i<len; i++) {        ToastRecord r = list.get(i);        //如果包名和callback都相同,则认为是同一个ToastRecord        if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {            return i;        }    }    //没有找到,返回-1    return -1;}

ToastRecord类的构造方法

ToastRecord类是NSM的内部类,它是客户端Toast在服务端NMS中的代表,它仅仅是记录了一些简单的信息方便NSM管理。

private static final class ToastRecord{    //要显示Toast的进程的pid    final int pid;    //显示Toast进程的包名    final String pkg;    //还记得我们之前分析的enqueueToast方法么,这里就是Toast的内部类Tn的binder代理对象    //通过该binder代理对象,我们可以操作Toast的显示和隐藏    final ITransientNotification callback;    //Toast的显示时间    int duration;    ToastRecord(int pid, String pkg, ITransientNotification callback, int duration)    {        this.pid = pid;        this.pkg = pkg;        this.callback = callback;        this.duration = duration;    }}

NMS的showNextToastLocked方法

该方法从Toast队列mToastQueue头部取出第一个ToastRecord,调用该ToastRecord的成员变量callback的show方法,callback的类型为ITransientNotification,就是之前在Toast类中通过enqueueToast方法传过来的TN类的binder代理对象。NMS通过它来与Toast类取得联系。通讯接口见下图。
这里写图片描述

void showNextToastLocked() {    //取出列表中的第一个ToastRecord,因为我们插入    //的Toast信息总是在列表的末尾,而拿出信息在列表的开头    //所以这个列表实际上是一个先进先出的队列    ToastRecord record = mToastQueue.get(0);    //如果该record不为空    while (record != null) {        try {            //1,ToastRecord的callback就是我们之前从Toast客户端传入的ITransientNotification            //binder代理对象,它的binder服务端就是Toast类中的TN,这里是通知Toast显示            record.callback.show();            //2,前面的语句是通知Toast显示出来,但是我们都知道Toast是显示一小段时间就会自动消失的            //这里就是为Toast设置消失的方法            scheduleTimeoutLocked(record);            return;        } catch (RemoteException e) {            //发生了异常,则查找ToastRecord在列表中的位置            int index = mToastQueue.indexOf(record);            //如果在列表中找到了该对象,将它移除            if (index >= 0) {                mToastQueue.remove(index);            }            //这里还是将该Toast所对应的进程设置为前台进程            //防止它被系统杀死            keepProcessAliveLocked(record.pid);            //获取队列中的下一个ToastRecord,并尝试显示            if (mToastQueue.size() > 0) {                record = mToastQueue.get(0);            } else {                record = null;            }        }    }}

将Toast真正显示出来的show方法等我们之后回到Toast的TN类中在分析,我们知道Toast显示一段时间后就会消失,这是怎么做到的呢?就是通过scheduleTimeoutLocked方法。

NMS的scheduleTimeoutLocked方法

该方法的实现很简单,首先移除掉当前ToastRecord的超时信息,然后创建一个新的超时信息,并使用handler发送出去,handler在这里起了延时的作用。

private void scheduleTimeoutLocked(ToastRecord r){    //移除信息    mHandler.removeCallbacksAndMessages(r);    //创建一个类型为MESSAGE_TIMEOUT,数据为ToastRecord的信息    Message m = Message.obtain(mHandler, MESSAGE_TIMEOUT, r);    //超时时间,只有2s和3.5s俩种    long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;    //延时delay ms后,将该信息发送出去    mHandler.sendMessageDelayed(m, delay);}

查找mHandler的实现类,发现是NMS的内部类WorkerHandler,我们继续查看WorkerHandler对MESSAGE_TIMEOUT信息的处理。

NMS的WorkerHandler内部类

我们只关心对之前传入的MESSAGE_TIMEOUT信息的处理,可以看到,调用handleTimeout对Message传入的ToastRecord信息进行处理,我们继续分析handleTimeout方法。

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

NMS类的handleTimeout方法

该方法查找ToastRecord在队列中的位置,如果找到,调用cancelToastLocked方法进行进一步的处理。

private void handleTimeout(ToastRecord record){    synchronized (mToastQueue) {        //查找ToastRecord在全局队列中的位置        int index = indexOfToastLocked(record.pkg, record.callback);        //如果ToastRecord在队列中存在,调用cancelToastLocked方法处理        if (index >= 0) {            cancelToastLocked(index);        }    }}

NMS类的cancelToastLocked方法

该方法首先找出ToastRecord对象,然后调用它的callback的hide方法,通知Toast的TN类真正执行隐藏Toast的操作,该过程我们稍候分析。然后从ToastRecord队列中移除该Toast。最后如果队列中还有其他ToastRecord需要显示,则再显示下一条Toast。

void cancelToastLocked(int index) {    //从队列中取出ToastRecord    ToastRecord record = mToastQueue.get(index);    try {        //1,调用callback的hide方法,这里的callback未ITransientNotification类型,】        //通过binder通信,最终调用Toast内部类TN中的hide方法,我们之后分析        record.callback.hide();    } catch (RemoteException e) {    }    //从队列中移除Toast    mToastQueue.remove(index);    //设置要显示Toast的进程为前台进程,    //防止它被系统杀死导致Toast显示不出来。    keepProcessAliveLocked(record.pid);    //如果ToastRecord队列中还有其他Toast    //继续显示下一条Toast    if (mToastQueue.size() > 0) {        showNextToastLocked();    }}

Toast内部类TN的show和hide方法

通过之前的分析我们知道,NMS只是管理全局的ToastRecord列表,调度Toast的显示和消失。但是真正将Toast显示到屏幕上和让Toast从屏幕上消失的则是TN类的show和hide方法,下面我们就来分析这2个方法。

@Overridepublic void show() {    mHandler.post(mShow);}@Overridepublic void hide() {    mHandler.post(mHide);}final Runnable mShow = new Runnable() {    @Override    public void run() {        handleShow();    }};final Runnable mHide = new Runnable() {    @Override    public void run() {        handleHide();        mNextView = null;    }};

可以看到show和hide方法都是仅仅向handler发送一个runnalbe信息,该runnable信息中也仅仅只是调用了handleShow和handleHide方法而已,这2个方法才是真正执行Toast显示和隐藏的地方。

至于这里为什么要使用handler发送信息,而不是直接在show和hide方法中执行处理流程呢?还是和binder机制有关,在TN中的show和hide方法实际运行在binder服务端的线程池中,我们通过handler机制,让实际的handleShow和handleHide方法可以运行在handler所关联的线程中。

TN的handleShow方法

该方法主要是使用TN中的参数更新了WindowManager的布局参数,并将Toast的view添加到WindowManager中,最终让它显示出来。

public void handleShow() {    //还记得mNextView吗,我们通过Toast.makeText或者toast.setView放方法    //设置的view就存储在该变量中。这里的意思是如果我们要显示的Toast的view    //和已经在显示的不一致时,我们应该更新view的显示    if (mView != mNextView) {        //1,移除掉原来显示的view        handleHide();        //将现在要显示的view保存到mView中        mView = mNextView;        //获取context和包名,代码略        //获取WMS服务        mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);        //这里对一些对齐方式进行处理        //这里设置布局参数例如显示位置和水平垂直边距等,具体代码略        //将mView添加到WindowManager中,最终会将view添加到wms中,显示在屏幕上。        mWM.addView(mView, mParams);        //这个方法设置了一些无障碍服务,和主线流程无关,暂时不研究        trySendAccessibilityEvent();    }}

TN的handleHide方法

该方法将mView从WMS中移除,也即让Toast从屏幕上消失。

public void handleHide() {    if (mView != null) {        //这里的view.getParent因为该view已经是顶层view了,        //所以得到的是ViewRootImpl,它不为空的时候,表示该        //view已经被添加到了wms中,此时将它移除。        //如果View没有被添加到WMS中就移除,会导致奔溃        if (mView.getParent() != null) {            mWM.removeView(mView);        }        mView = null;    }}

Toast的显示和消失涉及到了一个重要的系统服务WindowManagerService,该服务对系统所有的窗口进行管理,我们要将View显示到系统屏幕上就必须将它添加到WMS中,平时使用的Activity,dialog乃至系统的状态栏等显示在屏幕上的信息,都是WMS中的一个窗口。

整个WMS系统非常庞大,它管理系统中所有窗口的surface,根据窗口层级和大小分配surface,将surface上的内容交给surfaceflinger混合后输出给FrameBuffer,最终显示到屏幕上。和AMS,surfaceflinger等重要系统服务都有相当多的交互,这里没法细说,放上几个链接给有兴趣的同学参考。
http://www.tuicool.com/articles/MjAjIfU
http://blog.csdn.net/innost/article/details/47660193

Toast显示总结

对于Toast代码的分析在此告一段落,从上面的分析中我们可以得到以下结论:
- Toast的显示及取消是通过NotificationManagerService来管理的,它跨进程,使用AIDL来实现进程间通信。
- 所有Toast都会加到NotificationManagerService的队列中,对于非系统程序,它会限制Toast的数量(当前我所读的代码中该值为50)以防止DOS攻击及内存泄露的问题。
- Toast里的TN对象的显示及隐藏命令通过new出来的handler来发送。所以没有队列的线程是不能显示Toast的。
- Toast的显示的时间只有两个,duration相当于一个标志位,用于标志显示的时间是长还是短,而不是具体的显示时间。
- 当有Toast要显示时,其所在进程会被设为前台进程。

我们再来看一看下面的时序图,本文分析的所有方法均在该图中有所体现,大家也可以自己对照该图进行分析。
这里写图片描述

在子线程中显示Toast

我们在平时的开发中都知道,不能在子线程中更新ui(其实该说法并不准确,应该叫做不能在创建ViewRootImpl的线程之外更新ui,只不过是ViewRootImpl在我们常用的Activity中是在主线程中被创建的,详细的分析过程涉及到AMS,ViewRootImpl等,就不展开了,有兴趣的同学可以看这里http://www.cnblogs.com/xuyinhuan/p/5930287.html)。

那么我们直接在子线程中显示一个Toast试试,代码如下:

new Thread(new Runnable() {    @Override    public void run() {        showToast("我是子线程中弹出的Toast");    }}).start();

果然奔溃了,奔溃信息如下:

java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()      at android.os.Handler.<init>(Handler.java:208)      at android.os.Handler.<init>(Handler.java:122)      at android.widget.Toast$TN.<init>(Toast.java:351)      at android.widget.Toast.<init>(Toast.java:106)      at android.widget.Toast.makeText(Toast.java:265)      at com.hyc.test.MainActivity.showToast(MainActivity.java:72)      at com.hyc.test.MainActivity.access$200(MainActivity.java:24)      at com.hyc.test.MainActivity$2.run(MainActivity.java:65)      at java.lang.Thread.run(Thread.java:761)

分析该奔溃信息,发现不是我们熟悉的不能在主线程更新ui的异常,而是handler创建的时候抛出的异常,还记得Toast初始化的时候的TN类么,在TN类构造时,也创建了一个Handler对象。

关于handler和Looper,MessageQueue的知识,这里不详细说了,网上的参考资料非常多,如果有不熟悉的同学参考这个链接http://blog.csdn.net/iispring/article/details/47180325

我们知道在创建Handler的时候,必须先在线程中使用Looper.prepare()方法先为该线程创建一个Looper,否则就会抛出上面的异常。至于主线程中为什么不需要准备Looper呢?那是因为在主线程执行的ActivityThread类的main方法中已经执行过Looper.prepareMainLooper()方法将主线程的Looper准备好了,这里就不详细展开了,有兴趣的同学可以自己研究。

private static class TN extends ITransientNotification.Stub {      final Handler mHandler = new Handler();}

于是我们就知道创建Handler的时候,需要先创建Looper对象,于是修改我们的代码,先准备Looper在弹出Toast,这次成功的在子线程中将Toast显示出来了。

new Thread(new Runnable() {    @Override    public void run() {        Looper.prepare();        showToast("我是子线程中弹出的Toast");        Looper.loop();    }}).start();

app中连续弹出多个Toast

平时我们使用Toast的时候,一般的代码如下

Toast.makeText(this,"I am a Toast",Toast.LENGTH_SHORT).show();

这样一般情况下是没问题的,但是有时候,例如网络请求失败时,我们可能多次调用该方法产生很多提示,例如“网络异常”,“服务器响应超时”,“token失效等等”。根据之前的源码分析,我们知道,这些Toast信息都会加入系统ToastRecord队列,再一条条显示出来,这样会让人觉得提示过多,显示时间也较长。

我们可以在app中采用一个静态变量来存储Toast,这样我们的app中就只有一个Toast对象了,需要显示多条Toast信息的时候,就不会向NMS的ToastRecord队列插入多个待显示的Toast信息,而仅仅只是更新当前的Toast信息。这样就不会出现连续弹出多个Toast的情况了。

public class T {    //存储toast对象    private static Toast mToast;    /**     * 私有构造方法,阻止实例化     */    private T()      {          throw new UnsupportedOperationException("cannot be instantiated");      }      public static void showLong(Context context,String msg)    {        show(context, msg, Toast.LENGTH_LONG);    }    public static void showLong(Context context,int msg)    {        show(context, msg, Toast.LENGTH_LONG);    }    public static void showShort(Context context,String msg)    {        show(context, msg, Toast.LENGTH_SHORT);    }    public static void showShort(Context context,int msg)    {        show(context, msg, Toast.LENGTH_SHORT);    }    public static void show(Context context, int msg, int duration)    {        if(mToast!=null)        {            mToast.setText(msg);        }        else        {            mToast = Toast.makeText(context, msg, duration);        }        mToast.setGravity(Gravity.CENTER, 0, 0);        mToast.show();    }    public static void show(Context context, String msg, int duration)    {        if(mToast!=null)        {            mToast.setText(msg);        }        else        {            mToast = Toast.makeText(context, msg, duration);        }        mToast.setGravity(Gravity.CENTER, 0, 0);        mToast.show();    }}
原创粉丝点击