一:報錯情況
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結束之後,又把進入標誌改回來。
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,在子執行緒中也可以修改文字不報錯!!試一試!!