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(); }}
- Android -Toast源码解析
- Toast源码解析
- Toast源码解析
- Toast实现源码解析
- Toast自定义及源码解析
- 系统窗口Toast显示源码解析
- Android源码解析——Toast
- Android 高级自定义Toast及源码解析
- Android 高级自定义Toast及源码解析
- Snackbar新版Toast 从源码角度完全解析
- Android源码解析(二十二)-->Toast加载绘制流程
- Android Toast源码分析
- Android Toast源码实现
- Toast源码浅析
- Toast源码分析
- Android:Toast源码分析
- Toast源码分析
- [android] toast解析
- 数据库(fmdb)
- Java动态代理--jdk代理
- UIView设置边框(整体设置和分开设置)
- const vector<int> vec(10)
- python的基础语法
- Toast源码解析
- Redis之配置文件:单位,包含,通用
- 广播
- Java设计模式-适配器模式
- 基于MQTT的推送,连接服务器问题
- 621. Task Scheduler--任务调度
- 关于#include <iomanip>中iomanip的作用~
- Moment.js进行时间类型转换
- MySQL(3):可视化数据库管理工具