Toast是我们平时开发过程中常用的一个类,用于弹出一个文本提示,使用方法也非常简单,不过看似简单的Toast也并没有大家想象的那么简单,不信,有个小问题问问大家,Toast可以在子线程中弹出吗?大家可以先想一想这个问题,文章的最后我们会公布答案。
我们直接从最常用Toast的使用方法开始分析Toast的原理,Toast的用法如下:
Toast.makeText(this,"I am a Toast",Toast.LENGTH_SHORT).show();
该方法主要做了3项工作,我们来逐项分析:
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的构造方法看起来很简单,仅仅是保存了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继承自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类默认的布局文件就是上面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>
该方法主要分为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类中使用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;
}
该方法的详细流程图如下:
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; ifinal 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);
}
}
}
该方法根据包名和ITransientNotification类的变量callback,查找需要的ToastRecord在全局列表mToastQueue中的位置,如果未找到则返回-1,mToastQueue是一个ArrayList,存储了系统中所有要显示的Toast的信息。
//全局变量,存储了所有要显示的Toast的信息
ArrayList mToastQueue = new ArrayList();
int indexOfToastLocked(String pkg, ITransientNotification callback){
IBinder cbak = callback.asBinder();
ArrayList list = mToastQueue;
int len = list.size();
//遍历ToastRecord列表
for (int i=0; i//如果包名和callback都相同,则认为是同一个ToastRecord
if (r.pkg.equals(pkg) && r.callback.asBinder() == cbak) {
return i;
}
}
//没有找到,返回-1
return -1;
}
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;
}
}
该方法从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方法。
该方法的实现很简单,首先移除掉当前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信息的处理。
我们只关心对之前传入的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;
}
}
}
该方法查找ToastRecord在队列中的位置,如果找到,调用cancelToastLocked方法进行进一步的处理。
private void handleTimeout(ToastRecord record){
synchronized (mToastQueue) {
//查找ToastRecord在全局队列中的位置
int index = indexOfToastLocked(record.pkg, record.callback);
//如果ToastRecord在队列中存在,调用cancelToastLocked方法处理
if (index >= 0) {
cancelToastLocked(index);
}
}
}
该方法首先找出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();
}
}
通过之前的分析我们知道,NMS只是管理全局的ToastRecord列表,调度Toast的显示和消失。但是真正将Toast显示到屏幕上和让Toast从屏幕上消失的则是TN类的show和hide方法,下面我们就来分析这2个方法。
@Override
public void show() {
mHandler.post(mShow);
}
@Override
public 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中的参数更新了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();
}
}
该方法将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的显示及取消是通过NotificationManagerService来管理的,它跨进程,使用AIDL来实现进程间通信。
- 所有Toast都会加到NotificationManagerService的队列中,对于非系统程序,它会限制Toast的数量(当前我所读的代码中该值为50)以防止DOS攻击及内存泄露的问题。
- Toast里的TN对象的显示及隐藏命令通过new出来的handler来发送。所以没有队列的线程是不能显示Toast的。
- Toast的显示的时间只有两个,duration相当于一个标志位,用于标志显示的时间是长还是短,而不是具体的显示时间。
- 当有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.(Handler.java:208)
at android.os.Handler.(Handler.java:122)
at android.widget.Toast$TN.(Toast.java:351)
at android.widget.Toast.(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();
平时我们使用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();
}
}