1. 程式人生 > >Toast執行流程分析與複用

Toast執行流程分析與複用

意圖分析的問題

  1. Toast的顯示是非同步的還是同步。
  2. Toast是否可以在子執行緒中例項化並呼叫Toast.show方法。
  3. 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的一點理解,如有不正確之處,歡迎大家指正。