您的位置:首页 > 其它

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的一点理解,如有不正确之处,欢迎大家指正。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: