Toast執行流程分析與複用
意圖分析的問題
- 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的一點理解,如有不正確之處,歡迎大家指正。