Toast执行流程分析与复用
2016-01-27 20:07
295 查看
意图分析的问题
Toast的显示是异步的还是同步。Toast是否可以在子线程中实例化并调用
Toast.show方法。
Toast对象复用的可行性。
源码执行流程分析
Toast的本地创建和show()方法
实例化Toast对象
在实际使用中我们常使用使用如下代码实例化一个Toast对象。Toast.makeText(Context, msg, Toast.LENGTH_SHOW);
下面看看makeText方法的源码:
/** * Make a standard toast that just contains a text view. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. * @param text The text to show. Can be formatted text. * @param duration How long to display the message. Either {@link #LENGTH_SHORT} or * {@link #LENGTH_LONG} * */ public static Toast makeText(Context context, CharSequence text, @Duration int duration) { //实例化一个新的Toast对象 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); //注意此处把加载的view的引用给mNextView,该对象其实就是Toast实际显示的View //这里实际也告诉我们,可以给mNextView对象设置不同的View(通过setView(view)方法), //从而自定义Toast的显示样式和内容。 result.mNextView = v; result.mDuration = duration; return result; }
很简单的一个过程,实例化一个对象,加载要显示的View,设置View到Toast对象,返回新建的对象。注意上面的方法中,每次调用时都会新建一个
Toast对象,看下Toast的构造器源码:
/** * Construct an empty Toast object. You must call {@link #setView} before you * can call {@link #show}. * * @param context The context to use. Usually your {@link android.app.Application} * or {@link android.app.Activity} object. */ 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对象是什么,后面会讲到。
Toast.show()方法调用过程
show()方法是最终执行显示Toast消息的地方吗?下面我们来看源码。
/** * Show the view for the specified duration. */ public void show() { if (mNextView == null) { throw new RuntimeException("setView must have been called"); } INotificationManager service = getService(); String pkg = mContext.getOpPackageName(); TN tn = mTN; tn.mNextView = mNextView; try { service.enqueueToast(pkg, tn, mDuration); } catch (RemoteException e) { // Empty } }
可以看到show()方法很简短,没有明显显示Toast的代码,看
INotificationManager service = getService();
这行代码中,INotificationManager是个用于Binder机制IPC通信的接口,看
getService()的源码。
static private INotificationManager getService() { if (sService != null) { return sService; } sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification")); return sService; }
从代码中,可以发现是返回了一个以
"notification"为标记的远程服务对象的代理。这个远程服务对象就是NotificationManagerService,以下简称NMS,所以在show()方法中的
service.enqueueToast(pkg, tn, mDuration);
实际调用的就是是NMS中的
enqueueToast方法,先来看下传递给该方法的三个参数:
pkg 这个不用多说,就是表示应用的包名的字符串。
mDuration 也很简单,就是Toast显示的时长。
tn 这个参数是实现显示逻辑的核心部分,参数的类型为TN,是Toast类中的一个内部类,看到类定义的继承体系:
private static class TN extends ITransientNotification.Stub {
我们就知道了,TN实际上也是一个使用Binder机制IPC通信的远程调用的ITransientNotification接口的实现类,该类的具体实现后面再看。到目前为止,可以知道Toast.show()方法中并没有直接显示Toast消息,只是调用了NMS的
enqueueToast方法,那这个方法中会不会直接通过某种方式显示Toast呢?先来看下NMS这个类的继承结构:
public class NotificationManagerService extends INotificationManager.Stub { ...... }
显然NMS就是INotificationManager.Stub这个抽象类的具体实现类,注意在API23中NMS已经改为继承SystemServer类,其内部新增一个mService 对象实现该抽象类:
//API = 23 private final IBinder mService = new INotificationManager.Stub() {
NMS中Toast的执行流程
保存到Toast队列过程
上面说到show方法中调用NMS中的enqueueToast方法,下面是其关键部分的源码:
public void enqueueToast(String pkg, ITransientNotification callback, int duration) { ...... //省略部分代码 synchronized (mToastQueue) { int callingPid = Binder.getCallingPid(); long callingId = Binder.clearCallingIdentity(); try { ToastRecord record; int index = indexOfToastLocked(pkg, callback); // If it's already in the queue, we update it in place, we don't // move it to the end of the queue. if (index >= 0) { record = mToastQueue.get(index); record.update(duration); } else { 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; } } } } record = new ToastRecord(callingPid, pkg, callback, duration); mToastQueue.add(record); index = mToastQueue.size() - 1; keepProcessAliveLocked(callingPid); } // If it's at index 0, it's the current toast. It doesn't matter if it's // new or just been updated. Call back and tell it to show itself. // If the callback fails, this will remove it from the list, so don't // assume that it's valid after this. if (index == 0) { showNextToastLocked(); } } finally { Binder.restoreCallingIdentity(callingId); } } }
上面这段代码的总体意思是,将新发送过来的Toast消息(实际是Toast对象的一些参数)添加到
mToastQueue队列中,如果当前没有正在显示的Toast,则直接显示新的Toast消息。
mToastQueue实际上是一个ArrayList对象,具体逻辑来说:
6~15行代码中,首先根据传递过来的包名和TN binder通讯对象,获取该Toast对象在队列中的位置,如果存在,则更新Toast的显示时间,且并不会改变它在队列中的位置。
32~33行中,其实就是该Toast在队列中不存在时,则创建一个包裹Toast参数的ToastRecord对象,放入队列中。
41~42行,只有当前没有正在显示的Toast或当前更新的Toast正在显示时,才直接调用显示Toast的
showNextToastLocked()方法,这里如果是Toast正在显示这种情况下调用的该方法,则会重置原来的超时计时,以更新的数据重新设置超时计时,这点会在后面分析的代码中体现。
循环获取、显示Toast过程
来看下showNextToastLocked()方法的源码:
void showNextToastLocked() { ToastRecord record = mToastQueue.get(0); while (record != null) { if (DBG) Slog.d(TAG, "Show pkg=" + record.pkg + " callback=" + record.callback); try { 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; } } } }
方法的逻辑很简单,不管是正常执行流程还是异常流程,都是获取队列中的第一个ToastRecord对象,然后调用它的callback成员的show方法,这时候发现这里的callback成员就是我们在上边的
enqueueToast方法中传入的远程TN对象,从这里看出NMS中并没有直接显示Toast,其显示的过程还是通过TN的远程代理对象来实现的,其show方法的具体实现后面再分析。
从上面的分析我们知道
showNextToastLocked()方法每次只取队列中的第一个元素,那么是怎么实现不同的Toast的显示的呢?注意下第7行的代码:
scheduleTimeoutLocked(record);
这个方法是做什么的呢?我们看下源码:
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); }
代码逻辑很简单,其实质就是为每个Toast消息设置一个显示超时的处理器,需要注意的是代码第3行会先移除ToastRecord原来超时处理器(也就是消息),超时处理器的原理就是使用handler的延时消息机制,mHandler发送一个标记为MESSAGE_TIMEOUT的延时消息,延时时长取决于我们已经显示的Toast的显示时长标记,mHandler是一个WorkHandler对象,WorkHandler中收到该消息时,直接调用handleTimeout方法处理,看下该方法的源码:
private void handleTimeout(ToastRecord record) { if (DBG) Slog.d(TAG, "Timeout pkg=" + record.pkg + " callback=" + record.callback); synchronized (mToastQueue) { int index = indexOfToastLocked(record.pkg, record.callback); if (index >= 0) { cancelToastLocked(index); } } } // lock on mToastQueue 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; } void cancelToastLocked(int index) { ToastRecord record = mToastQueue.get(index); try { record.callback.hide(); } catch (RemoteException e) { Slog.w(TAG, "Object died trying to hide notification " + record.callback + " in package " + record.pkg); // 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(); } }
handleTimeout方法中先调用
indexOfToastLocked(String pkg, ITransientNotification callback)获取ToastRecord对象在队列中位置,该对象是由msg.obj转换而来的,再根据该下标调用
cancelToastLocked(int index)去取消已到超时时间的Toast,看
cancelToastLocked(int index)的实现中:
30行record.callback.hide(),隐藏正在显示的Toast,具体的实现后面再分析。
37~44行代码,先移除队列中该下标的元素,再调用
showNextToastLocked()方法,接下来就是重复上面分析的该方法的执行流程。
到这里总结下Toast在NMS中的执行流程:
每当增加一个Toast的时候,先判断队列中是否存在该Toast,若存在则直接更新显示时长,不改变其在队列中的位置,若不存在则将参数封装到一个ToastRecord对象中,并放入队列中,若新增的Toast在队列的最前端,则直接显示。
当正在显示的Toast的显示时长到达预先设置的显示时间时,清除正在显示的Toast,从队列中移除该Toast,显示队列中的下一条Toast,重复这个过程,直到队列中没有新的Toast需要显示。
到目前我们可以得出结论一:
对于某个具体的Toast的显示是一个异步串行的过程,只要当队列中其前面的Toast全部显示完或取消掉,才能显示该Toast。
Toast的最终显示过程
上面分析了那么多,并没有涉及到Toast的真正的显示过程,这一小节分析的Toast的显示、隐藏过程。上面说过,显示Toast调用的是TN.show()方法,那show方法的实现是怎样的呢?
public void show() { if (localLOGV) Log.v(TAG, "SHOW: " + this); mHandler.post(mShow); }
show方法直接调用了mHandler.post发送了一个消息,也就是说Toast的显示依赖mHandler来分发,来看下mHandler的定义,和mShow这个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; } }; private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams(); final Handler mHandler = new Handler();
代码18行,可以看到mHandler的初始化调用的是其默认构造器,也就是mHandler所绑定的线程就是初始化TN对象的线程,而TN对象是在Toast.makeToast方法中初始化的,又Handler的初始化成功的前提是,该线程存在消息循环即:
new Thread(){ public void run(){ ...... Looper.prepare(); ...... Looper.loop(); ...... } }
代码第4行,mShow这个Runable作为消息的实际执行体,没有其他额外的操作,直接调用了handleShow()方法,这个方法才是真正最终显示Toast的地方,看其源码:
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(); String packageName = mView.getContext().getOpPackageName(); 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; mParams.packageName = packageName; 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(); } }
看到这个方法的具体实现,一下子就明白,Toast的View的最终显示是使用WindowManager的addView方法添加到Window中的,同样取消正在显示的View实际就是调用WindowManager的removeView方法。到这里可能会有个疑惑,不是说Android中刷新界面只能在UI线程中吗?为什么这里显示View没有对非UI线程做限制呢?这个问题,这里推荐一篇文章:Android子线程真的不能更新UI么,这里可以解决我们的疑惑,View只能在与其关联的ViewRootImpl对象创建时所在线程刷新,否则将抛出异常。所以前面说Toast依赖handler来分发显示,更确认的说它需要mHandler初始化时绑定的线程来提供刷新UI时所需要的线程安全。
所以到这里可以得出第二个结论:
Toast可以在子线程中创建,只需要该线程存在消息循环即可,且调用Toast.makeToast方法的线程就是其所属的线程。
Toast的取消流程与显示的流程实际是一样的,这里就不再额外分析了。
到此Toast的执行流程算是分析结束了,做一个的流程执行总结:
Toast.makeToast方法创建一个全新的Toast对象,包括初始化TN对象。
Toast.show()方法通过传递TN对象, packageName, 显示时长三个参数给远程调用NMS的
enqueueToast方法,将其加入到显示Toast的队列中。
当上面传递的Toast参数的封装对象位于队列的最前端时,则再通过远程回调TN对象的show方法显示Toast。
TN对象在show方法中在初始化mHandler时所绑定的线程中将Toast要显示的View通过WindowManager的addView方法添加到Window中,最终实现显示Toast。
在NMS中若Toast的显示时长到达或远程取消,则远程回调TN的hide方法,清除Toast,如果存在下一条Toast,则继续显示。
附上一张UML时序图:
Toast的复用
Toast对象复用的实际意义
在某些情况下,在一个应用中可能在一段短时间内,集中弹出几个Toast,有时候我们需要忽略已经显示或还未显示的Toast,直接显示最新的Toast,因为Toast的显示是串行的原因,要达到这一目的,有两种方式:cancel掉要显示Toast之前的所有Toast,让要显示的Toast位于队列的最前端。
重复使用同一个Toast对象,每次要显示新的Toast只需更新该Toast对象的View等参数就可以了。
它们各有自己的优缺点,这里主要只分析复用带来的问题。
复用带来的问题
使用上述第一种方式,有一个弊端,需要保持每一个Toast的对象,手动去控制它们的生命周期,使原本简单的Toast操作,变的复杂化,如果控制不当,可能造成内存泄露。使用第二种方法,使用起来确实更方便,全局只保存一个Toast对象,且能更快速的更新Toast消息(如果Toast正在显示,则不需要先移除当前Toast,再显示新的Toast,直接显示最新消息),但是是否就真的可以全局任意使用呢?前面结论一说到Toast可以在子线程中显示调用,先看下面的代码:
//MainThread final Toast toast = Toast.makeText(this, "Main Thread show Toast", Toast.LENGTH_LONG); toast.show(); //start a new child thread new Thread() { public void run() { Looper.prepare(); //此处延时1000ms,为了两个目的: //1、保证先显示第一条Toast的效果 //2、保证Toast的TextView在重新设置值以前已经依附到Window中,因为在TextView或者说整个Toast的 //布局在没有添加到Window(没有创建其对应的ViewRootImpl)之前,setText方法是可以在非UI线程中调用的 try { Thread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } toast.setText("Child Thread show Toast"); //调用toast.setText方法时,若Toast正在显示,是否调用toast.show()方法会展示两种显示效果: //1、不调用toast.show(),只刷新显示的Toast消息,不刷新显示的时长,显示的新消息时间为剩余的显示时长 //即:显示时长 = Toast设置显示时长 - 已使用时间 //2、调用toast.show()方法,刷新Toast消息,重置显示时长,从新开始计时,显示时长为最新设置的时长标志位。 //即:显示时长 >= Toast设置显示时长(>=是因为调用toast.setText方法就会先更新View) //注意如果是toast.setDuration(),toast.setView()方法,需要调用toast.show()方法来保证更改的字段生效 toast.show(); Looper.loop(); }; }.start();
上述代码是先在主线程构建Toast对象并显示,再在子线程中直接复用该Toast,设置不同的Text值而已,然而在实际执行中显示完”Main Thread show Toast”后欲显示”Child Thread show Toast”消息时会抛出以下异常:
02-04 10:32:40.840: E/AndroidRuntime(25368): android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
这个异常的描述的大致意思是:只有原来创建了视图层的线程能够控制它的view,这个也是通常在非UI线程中刷新UI元素抛出的异常,这个异常是在ViewRootImpl的checkThread方法中抛出来的,具体原因细节请看Android子线程真的不能更新UI么这篇文章。
上面的代码中
Toast.makeText方法是在主线程中调用的,从上节的内容分析可知,这也意味着Toast的TN成员对象的mHandler所绑定的消息队列是主线程的消息队列,这也导致最终在第一次调用
WindowManager.addView方法时创建ViewRootImpl对象初始化时获取的是主线程对象的引用,所以当在子线程中重新刷新View内容时,做线程对象检查不匹配导致异常抛出。
以上实际执行结果说明当使用同一个Toast对象时,在多线程使用时Toast需要满足Android单线程刷新UI这一原则。
如何实现多线程安全复用Toast
无论使用何种方法都必须要符合Android单线程刷新UI这一原则,这里提供两种方法:- 将所有对Toast的操作放到UI线程中去执行,这样就保证了单一原则,参看以下代码:
public class ToastTestActivity extends Activity { private Button sendToastBtn; //handler初始化时绑定到主线程消息循环 private static Handler mHandler = new Handler(Looper.getMainLooper()); private static Toast mToast = null; public static void showToastToUiThread(final Toast toast, final CharSequence text, final int duration) { if(toast != null) { //如果当前线程不是主线程,则将Toast方法的调用放到主线程中去显示,主线程的ID与进程ID一致 if(Process.myTid() != Process.myPid()) { mHandler.post(new Runnable() { @Override public void run() { // TODO Auto-generated method stub toast.setText(text); toast.setDuration(duration); toast.show(); } }); } else { toast.setText(text); toast.setDuration(duration); toast.show(); } } } /** {@inheritDoc} */ @Override protected void onCreate(Bundle savedInstanceState) { // TODO Auto-generated method stub super.onCreate(savedInstanceState); setContentView(R.layout.toast_test_activity_layout); //初始化Toast if(mToast == null) { mToast = Toast.makeText(this, "init toast", Toast.LENGTH_LONG); mToast.show(); } sendToastBtn = (Button) findViewById(R.id.sendToastBtn); sendToastBtn.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { // TODO Auto-generated method stub showToastToUiThread(mToast, "first toast msg", Toast.LENGTH_LONG); new Thread() { public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } showToastToUiThread(mToast, "second toast msg", Toast.LENGTH_LONG); }; }.start(); } }); } }
使用这种方法可以保证线程安全,使用起来也非常简单,但是这样有一个问题,当主线程业务比较繁忙的时候,可能会导致Toast延时显示,同时如果有大量的Toast集中在某个时间点全部堆积到主线程上去显示,有较小概率会对主线的其他消息响应有影响。
创建一个子线程用于Toast的显示,即将Toast的构造过程放到一个专门的非UI线程中去执行,从而保证线程安全,也防止影响主线程的运行,参考以下示例代码:
public class MyToast { private static final String TAG = "MyToast"; private static MyToast myToast = new MyToast(); private Toast toast; /** * 使用一个线程用于专门显示Toast * */ private static Thread thread = new Thread() { public void run() { Looper.prepare(); Log.d(TAG, "init handle and toast!!"); handle = new HelperHandler(); synchronized (myToast) { myToast.toast = Toast.makeText(MyApplication.getInstance(), "", 0); myToast.notifyAll(); } Looper.loop(); }; }; private static Handler handle = null; //初始化启动线程 static { thread.start(); } public static MyToast getDefaultInstance() { synchronized (myToast) { if(myToast.toast == null) { try { myToast.wait(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } } return myToast; } /** * 此方法的创建过程是一个同步的,可能会比较耗时 * 使用此方法创建的Toast对象都是在thread线程中的。 * */ public static MyToast newToast(Context context, CharSequence text, int duration) { Log.d(TAG, "create a new toast"); MyToast toast = new MyToast(); synchronized (toast) { handle.post(new CreateToastRunnable(toast, context, text, duration)); try { toast.wait(); Log.d(TAG, "create finished "); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } if(toast.toast == null) { return null; } return toast; } private static class HelperHandler extends Handler { private static final int QUIT_LOOP = -103; /** {@inheritDoc} */ @TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR2) @Override public void handleMessage(Message msg) { // TODO Auto-generated method stub switch (msg.what) { case QUIT_LOOP: if(Build.VERSION.SDK_INT >= 18) { Looper.myLooper().quitSafely(); } else { Looper.myLooper().quit(); } break; default: break; } } } /** * * */ public void showToastByThread(CharSequence text) { handle.post(new MyRunnable(this, text)); } /** * * */ public void showToastByThread() { Log.d(TAG, "pre to post msg"); handle.post(new Runnable() { @Override public void run() { // TODO Auto-generated method stub Log.d(TAG, "is read to show toast = " + MyToast.this); MyToast.this.toast.show(); } }); } /** {@inheritDoc} */ public MyToast setDuration(int duration) { // TODO Auto-generated method stub toast.setDuration(duration); return this; } public MyToast setView(View view) { toast.setView(view); return this; } private static class MyRunnable implements Runnable { private CharSequence text; private MyToast toasts; public MyRunnable(MyToast toasts, CharSequence text) { // TODO Auto-generated constructor stub this.text = text; this.toasts = toasts; } /** {@inheritDoc} */ @Override public void run() { // TODO Auto-generated method stub long startT = System.currentTimeMillis(); Log.d(TAG, "startTime = " + startT); toasts.toast.setText(text); toasts.toast.show(); Log.d(TAG, "totalTime = " + (System.currentTimeMillis() - startT)); } } public static void stopToastThread() { handle.sendEmptyMessage(HelperHandler.QUIT_LOOP); } /** * 用于在thread线程中构建一个Toast对象 * **/ private static class CreateToastRunnable implements Runnable { private MyToast mytoast; private Context context; private CharSequence text; private int duration; public CreateToastRunnable(MyToast mytoast, Context context, CharSequence text, int duration) { // TODO Auto-generated constructor stub this.mytoast = mytoast; this.context = context; this.text = text; this.duration = duration; } /** {@inheritDoc} */ @Override public void run() { // TODO Auto-generated method stub synchronized (mytoast) { mytoast.toast = Toast.makeText(context, text, duration); mytoast.notifyAll(); } } } } //以下为测试用代码: //使用默认创建的单例Toast对象,在不同的线程中复用它 MyToast.getDefaultInstance().setDuration(1).showToastByThread("first toast msg"); new Thread() { public void run() { try { Thread.sleep(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } MyToast.getDefaultInstance().setDuration(0).showToastByThread("second toast msg"); try { Thread.sleep(3500); } catch (InterruptedException e1) { // TODO Auto-generated catch block e1.printStackTrace(); } //新建一个Toast对象,在不同的线程中重复使用它 final MyToast tempToast = MyToast.newToast(ToastTestActivity.this, "third Toast msg", 0); tempToast.showToastByThread(); new Thread() { public void run() { try { Thread.sleep(1500); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } tempToast.showToastByThread("fourth toast msg"); }; }.start(); }; }.start();
上述就是采用自定义的一个线程来实现的Toast多线程安全使用的例程,其实现多线程安全访问的本质就是借用绑定了指定Thread的Handler对象将所有Toast相关操作发送异步消息到指定线程中执行。
可以发现使用这种方法虽然实现了线程安全刷新UI,但是同时也引入了一个运行在后台的线程,且因为消息循环的原因,需要用户自己手动去停止消息循环才能终止线程,这样相比于方法一的简单使用Toast,无疑这种方式在稳定性和简单性上都不如第一种方式,所以这里推荐使用第一种方式。
注意上面不管使用哪种方式的Toast复用,都存在一个问题,当Toast消息还在NMS的队列中未显示时,复用它们都会导致原有的Toast消息被替换掉,比如注释掉上面253-261这几行代码,那么”third Toast msg”这条消息就直接别它后面那条替换掉了。
以上就是所有关于Toast的一点理解,如有不正确之处,欢迎大家指正。
相关文章推荐
- MySQL 5.5 服务器变量详解(一)
- python基础之异常
- zjnu1189 土地租用(完整版)
- 成都Java培训机构太多,该如何选择呢?
- 文章标题
- Unity简单的实现动画三连击脚本
- String \StringBuffer \StringBuilder之间的区别
- Android API Level在11前后及16之后时Notification的不同用法
- OpenStack入门之【OpenStack-havana】之单网卡-All In One 安装(基于CentOS6.4)
- 网红淘宝店的成与败
- 宏定义后不加分号
- javaEE+Linux学习总结
- [BZOJ3524][Poi2014]Couriers
- Block
- Dreamweaver软件的快捷键
- 2016.1.27win装系统备忘
- 使用 JavaScript 将网站后台的数据变化实时更新到前端-【知乎总结】
- iOS中常见的报错及解决方案
- Xcode7中你一定要知道的炸裂调试神技
- SQL Server Management Studio(SSMS)的使用与配置整理(不定期更新 2016/02/04)