如何在子線程中更新UI
- 2021 年 12 月 14 日
- 筆記
- android底層一點, Android自定義view和控件
一:報錯情況
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,在子線程中也可以修改文字不報錯!!試一試!!