如何在子執行緒中更新UI

靈劍山真人發表於2021-12-14

一:報錯情況

 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,在子執行緒中也可以修改文字不報錯!!試一試!!

 

 

 

 

相關文章