Toast必須在UI(主)線程使用?

背景

依稀記得,從最開始幹Android這一行就常常聽到有人說:toast(吐司)不能在子線程調用顯示,只能在UI(主)線程調用展現。android

很是慚愧的是,我以前也這麼認爲,而且這個問題也一直沒有深究。bash

直至前兩天個人朋友 「林小海」 同窗說toast不能在子線程中顯示,這句話使我忽然想起了點什麼。markdown

我以爲我有必要證實、而且糾正一下。ide

toast不能在子線程調用展現的結論真的是謬論~函數

疑點

前兩天在說到這個toast的時候一瞬間對於只能在UI線程中調用展現的說法產生了兩個疑點:oop

  1. 在子線程更新UI通常都會有如下報錯提示:源碼分析

    Only the original thread that created a view hierarchy can touch its views.ui

    可是,咱們在子線程直接toast的話,報錯的提示以下:this

    Can't toast on a thread that has not called Looper.prepare()spa

    明顯,兩個報錯信息是不同的,從toast這條來看的話是指不能在一個沒有調用Looper.prepare()的線程裏面進行toast,字面意思上有說是不能在子線程更新UI嗎?No,沒有!這也就有了下面第2點的寫法。

  2. 曾見過一種在子線程使用toast的用法以下(正是那時候沒去深究這個問題):

new Thread(new Runnable() {
           @Override
           public void run() {
                Looper.prepare();
                Toast.makeText(MainActivity.this.getApplicationContext(),"SHOW",Toast.LENGTH_SHORT).show();
                Looper.loop();
            }
        }).start();
複製代碼

關於Looper這個東西,我想你們都很熟悉了,我就很少說looper這塊了,下面主要分析一下爲何這樣的寫法就能夠在子線程進行toast了呢?

而且Looper.loop()這個函數調用後是會阻塞輪循的,這種寫法是會致使線程沒有及時銷燬,在toast完以後我特地給你們用以下代碼展現一下這個線程的狀態:

Log.d("Wepon", "isAlive:"+t[0].isAlive());
    Log.d("Wepon", "state:" + t[0].getState());
    
 D/Wepon: isAlive:true
 D/Wepon: state:RUNNABLE
複製代碼

能夠看到,線程還活着,沒有銷燬掉。固然,這種代碼裏面若是想要退出looper的循環以達到線程能夠正常銷燬的話是可使用looper.quit相關的函數的,可是這個調用quit的時機倒是很差把握的。

下面將經過Toast相關的源碼來分析一下爲何會出現上面的狀況?

源碼分析

Read the fuck source code.

1.分析Toast.makeText()方法

首先看咱們的調用Toast.makeText,makeText這個函數的源碼:

// 這裏通常是咱們外部調用Toast.makeText(this, "xxxxx", Toast.LENGTH_SHORT)會進入的方法。
    // 而後會調用下面的函數。
    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
        return makeText(context, null, text, duration);
    }

    /**
     * Make a standard toast to display using the specified looper.
     * If looper is null, Looper.myLooper() is used.  //  1. 注意這一句話
     * @hide
     */
    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
            @NonNull CharSequence text, @Duration int duration) {
        
        // 2. 構造toast實例,有傳入looper,此處looper爲null
        Toast result = new Toast(context, looper); 

        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);

        result.mNextView = v;
        result.mDuration = duration;

        return result;
    }
複製代碼

從上面的源碼中看第1點註釋,looper爲null的時候會調用Looper.myLooper(),這個方法的做用是取咱們線程裏面的looper對象,這個調用是在Toast的構造函數裏面發生的,看咱們的Toast構造函數:

2.分析Toast構造函數

/**
     * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
     * @hide
     */
    public Toast(@NonNull Context context, @Nullable Looper looper) {
        mContext = context;
        // 1.此處建立一個TN的實例,傳入looper,接下來主要分析一下這個TN類
        mTN = new TN(context.getPackageName(), looper);  
        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(String packageName, @Nullable Looper looper) {
            // .....
            // ..... 省略部分源碼,這
            // .....
            
            // 重點
            // 2.判斷looper == null,這裏咱們從上面傳入的時候就是null,因此會進到裏面去。
            if (looper == null) {
                // Use Looper.myLooper() if looper is not specified.
                // 3.而後會調用Looper.myLooper這個函數,也就是會從ThreadLocal<Looper> sThreadLocal 去獲取當前線程的looper。
                // 若是ThreadLocal這個不太清楚的能夠先去看看handler源碼分析相關的內容瞭解一下。
                looper = Looper.myLooper();
                if (looper == null) {
                // 4.這就是報錯信息的根源點了!! 
                // 沒有獲取到當前線程的looper的話,就會拋出這個異常。
                // 因此分析到這裏,就能夠明白爲何在子線程直接toast會拋出這個異常
                // 而在子線程中建立了looper就不會拋出異常了。
                    throw new RuntimeException(
                            "Can't toast on a thread that has not called Looper.prepare()");
                }
            }
            // 5.這裏不重點講toast是如何展現出來的源碼了,主要都在TN這個類裏面,
            // Toast與TN中間有涉及aidl跨進程的調用,這些能夠看看源碼。
            // 大體就是:咱們的show方法實際是會往這個looper裏面放入message的,
            // looper.loop()會阻塞、輪循,
            // 當looper裏面有Message的時候會將message取出來,
            // 而後會經過handler的handleMessage來處理。
            mHandler = new Handler(looper, null) {
                @Override
                public void handleMessage(Message msg) {
                    switch (msg.what) {
                    // .... 省略代碼
                        case SHOW:  // 顯示,與WindowManager有關,這部分源碼不作說明了,能夠本身看看,就在TN類裏面。
                        case HIDE:  // 隱藏
                        case CANCEL: // 取消
                    }
                }
            };
        }
複製代碼

總結

從第1點能夠看到會建立TN的實例,並傳入looper,此時的looper仍是null。

進入TN的構造函數能夠看到會有looper是否爲null的判斷,而且在當looper爲null時,會從當前線程去獲取looper(第3點,Looper.myLooper()),若是仍是獲取不到,剛會拋出咱們開頭說的這個異常信息:Can't toast on a thread that has not called Looper.prepare()。

而有同窗會誤會只能在UI線程toast的緣由是:UI(主)線程在剛建立的時候就有建立looper的實例了,在主線程toast的話,會經過Looper.myLooper()獲取到對應的looper,因此不會拋出異常信息。

而不能直接在子線程程中toast的緣由是:子線程中沒有建立looper的話,去經過Looper.myLooper()獲取到的爲null,就會throw new RuntimeException( "Can't toast on a thread that has not called Looper.prepare()");

另外,兩個點說明一下:

  1. Looper.prepare() 是建立一個looper並經過ThreadLocal跟當前線程關聯上,也就是經過sThreadLocal.set(new Looper(quitAllowed));
  2. Looper.loop()是開啓輪循,有消息就會處理,沒有的話就會阻塞。

綜上,「Toast必須在UI(主)線程使用」這個說法是不對滴!,之後千萬不要再說toast只能在UI線程顯示啦.....

相關文章
相關標籤/搜索