如何在子線程中更新UI

一:報錯情況

 android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8798)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1606)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at android.view.View.requestLayout(View.java:25390)
        at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3593)
        at android.view.View.requestLayout(View.java:25390)
        at android.widget.TextView.checkForRelayout(TextView.java:9719)
        at android.widget.TextView.setText(TextView.java:6311)

我嘗試在子線程中更新UI:

binding.textView.setOnClickListener {
            thread {
                (it as TextView).text = "ldkjfla;66666sdf"
            }
        }

 

二:報錯原因

首先,我們更新UI,會調用text view的request layout方法, 然後view 的request layout方法又會調用到它父view的 request layout方法:

子view request layout  ——>    父view request layout

這樣一層層調用上去,因為view系統的最上層是一個叫作view root impl的view,所以最終會調用到它的request layout方法。

我們來看看它的request layout方法,然後看看有沒有什麼對策:

 

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();  和之前報錯代碼的最上面對應起來了
            mLayoutRequested = true;
            scheduleTraversals();
        }
    }

注意啦注意啦!!當在子view中更新UI,就會調用到view root impl的request layout方法!!然後裏面會調用check thread方法來看看更新UI的線程等不等於view root impl創建時的線程!!

view root impl是在activity的onResume生命周期主線程中創建的。所以這個checkThread就不通過啦!!

 

三:解決辦法

首先,上面說,更新UI時,會調用request layout方法一層層調用上去,那我們去看看這個路徑,看看有沒有辦法斬斷這個路徑。

 1:進入text view的set text方法

2:進入check for relayout方法

 

3:進入 request layout方法

 

 

4:然後就進入了view的request layout方法之中

 

 

然後接下來的過程就是我剛剛說的,一直向上調用到view root impl的request layout最後報錯了。注意啦!!注意啦!!在判斷向不向上去傳遞調用request layout的時候,會看看

!mParent.isLayoutRequested()

如果我們讓view root impl的LayourRequested參數為true,然後表達式為false,就不會調用到view root impl的requst layout方法,就不會check thread了。

所以我們要讓這個參數為true!!!

 

四:讓這個參數為true

我們可以注意到, view root impl的request layout方法中,在check thread之後,順手就把這個參數置為true了。所以我們可以調用一次textview的request layout方法,然後調用到view root impl的方法,把它置為true,然後我們再在子線程更新UI,就不會進入view root impl的request layout方法了。

binding.textView.setOnClickListener {
            it.requestLayout()//因為在主線程,所以最後check thread沒有事
            thread {
                (it as TextView).text = "ldkjfla;66666sdf"
            }
        }

 

五:LayoutRequested的意義

 我們更新UI,就會調用到view root impl的request layout方法,在check thread之後,就會把Layoutrequested置為true!!表示自己正處於被請求重新去布局的狀態!!置為true,之後,下一個方法就是鼎鼎有名的

scheduleTraversals()

 

它會執行view root impl的performTraversal!!  對整個view tree進行從上到下的測量、布局和繪製!!在這個perform traversal結束時,會把LayoutRequested置為false。不然,下一次更新UI不就會沒辦法到這個方法然後失敗了嘛!!
所以為了性能原因,在打算開展一個perform traversal之前,會把進入標誌改一下,perform traversal結束之後,又把進入標誌改回來。

我們就是趁它標誌位為true的時候更改UI.
要是它perform traversal結束之後把標誌位改了回來,,,那還是會報錯。
binding.textView.setOnClickListener {
            it.requestLayout()//因為在主線程,所以最後check thread沒有事
            thread {
                sleep(1000)//等了一秒, performTraversal結束之後標誌位恢復,又會去到check thread了。
                (it as TextView).text = "ldkjfla;66666sdf"
            }
        }

 

 

番外:

對於text view,在checkForRelayout方法中,會看看寬高是否改變然後決定是否向上傳遞request layout,所以,如果一個text view固定寬高,即使不主動request layout,在子線程中也可以修改文字不報錯!!試一試!!