為什么有時候在子線程更新UI沒報錯?

抓住十一月的尾巴,分享一首童年回憶: ??brave heart

在這里插入圖片描述

看到這個標題,好多人第一時間想到的是什么?
感興趣的不妨跟著下面的代碼看看會發(fā)生什么?
首先我在 onCreate 方法里調(diào)用 setText() 方法

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mContext = this
        Log.e(TAG, "onCreate")
        Thread {
            val simpleDateFormat = SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss") // HH:mm:ss
            //獲取當前時間
            val date = Date(System.currentTimeMillis())
            // 更新TextView文本
            tv_title.text="Date獲取當前日期時間" + simpleDateFormat.format(date))
        }.start()

    }

這個時候允許呢,會發(fā)現(xiàn),哎?為什么正常呢,不應(yīng)該報錯嗎?
但是呢, 當我改成了這樣以后再次運行

        Thread {
            try {
                Thread.sleep(2000)
                val simpleDateFormat = SimpleDateFormat("yyyy年MM月dd日 HH:mm:ss") // HH:mm:ss
                //獲取當前時間
                val date = Date(System.currentTimeMillis())
                // 更新TextView文本內(nèi)容
                tv_title.text = "Date獲取當前日期時間" + simpleDateFormat.format(date)
            } catch (e: InterruptedException) {
                e.printStackTrace()
            }
        }.start()

結(jié)果

   android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
       at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6557)
       at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:943)
       at android.view.ViewGroup.invalidateChild(ViewGroup.java:5081)
       at android.view.View.invalidateInternal(View.java:12719)
       at android.view.View.invalidate(View.java:12683)
       at android.view.View.invalidate(View.java:12667)
       at android.widget.TextView.checkForRelayout(TextView.java:7167)
       at android.widget.TextView.setText(TextView.java:4347)
       at android.widget.TextView.setText(TextView.java:4204)
       at android.widget.TextView.setText(TextView.java:4179)

這應(yīng)該就是大家熟悉的報錯了吧,不允許在非UI線程中更新UI線程
既然報這個錯了,那就跟進去,看看 ViewRootImpl.java 為什么報這個錯,之前分享過看源碼的方式。
點我看源碼
既然報錯已經(jīng)告訴我們在哪一行了,那我們就點進去看看,可以很容易的找到
在這里要說明一下,在Android2.2以后是用ViewRootImpl來代替ViewRoot的,用來連接WindowManager和DecorView,而且View的繪制也是通過ViewRootImpl來完成的。

6554  void checkThread() {
6555        if (mThread != Thread.currentThread()) {
6556            throw new CalledFromWrongThreadException(
6557                    "Only the original thread that created a view hierarchy can touch its views.");
6558        }
6559    }

當Activity對象被創(chuàng)建完畢后,將DecorView添加到Window中,同時會創(chuàng)建ViewRootImpl對象,在源碼中可以看到 mThread 是在ViewRootImpl 的構(gòu)造方法里這樣初始化的。然后再把他設(shè)為主線程。
在這里插入圖片描述

那現(xiàn)在捋一下,從上面的錯誤棧里,可以看到調(diào)用的流程是:

at android.widget.TextView.setText(TextView.java:4347)
at android.widget.TextView.checkForRelayout(TextView.java:7167)
at android.view.View.invalidate(View.java:12667)
at android.view.View.invalidateInternal(View.java:12719)
atandroid.view.ViewGroup.invalidateChild(ViewGroup.java:5081)
atandroid.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:943)
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6557)

所以現(xiàn)在聰明的同事是不是知道了,要去看看 ViewRootImpl 這個是在哪里被初始化的?

3126    final void handleResumeActivity(IBinder token,
3127            boolean clearHide, boolean isForward, boolean reallyResume) {
                                    ...
3158            if (r.window == null && !a.mFinished && willBeVisible) {
3159                r.window = r.activity.getWindow();
3160                View decor = r.window.getDecorView();
3161                decor.setVisibility(View.INVISIBLE);
3162                ViewManager wm = a.getWindowManager();
3163                WindowManager.LayoutParams l = r.window.getAttributes();
3164                a.mDecor = decor;
3165                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
3166                l.softInputMode |= forwardBit;
3167                if (a.mVisibleFromClient) {
3168                    a.mWindowAdded = true;
3169                    wm.addView(decor, l);
3170                }
3171
3172           ...
3247    }

最后的 wm.addView(decor, l) 就是我們要找的答案,這個時候看一下windowManager.addView(decorView)

  public void addView(View view, ViewGroup.LayoutParams params,
232            Display display, Window parentWindow) {
233        if (view == null) {
234            throw new IllegalArgumentException("view must not be null");
235        }
236        if (display == null) {
237            throw new IllegalArgumentException("display must not be null");
238        }
239        if (!(params instanceof WindowManager.LayoutParams)) {
240            throw new IllegalArgumentException("Params must be WindowManager.LayoutParams");
241        }
242
257        ViewRootImpl root;
258        View panelParentView = null;

                 ...
303            mViews.add(view);
304            mRoots.add(root);
305            mParams.add(wparams);
306        }
307
308        // do this last because it fires off messages to start doing things
309        try {
310            root.setView(view, wparams, panelParentView);
311        } catch (RuntimeException e) {
312            // BadTokenException or InvalidDisplayException, clean up.
313            synchronized (mLock) {
314                final int index = findViewLocked(view, false);
315                if (index >= 0) {
316                    removeViewLocked(index, true);
317                }
318            }
319            throw e;
320        }
321    }

看到這里,是不是有種豁然開朗的感覺,因為已經(jīng)找到了答案,答案就是跟 ViewRootImpl 的初始化有關(guān),因為我之前的代碼是在 onCreate() 的時候此時去設(shè)置textview,此時呢 View 還沒被繪制出來,ViewRootImpl 還未創(chuàng)建,它的創(chuàng)建是在 handleResumeActivity() 的調(diào)用到 windowManager.addView(decorView) 時候。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容