????對于所有的Android開發者來說,“View的更新必須在UI線程中進行”是一項最基本常識。
????如果不在UI線程中更新View,系統會拋出CalledFromWrongThreadException異常。那么有沒有什么辦法可以不在UI線程中更新View?答案當然是有的!
一.ViewRootImpl渲染體系
????在Android系統中,ViewRootImpl負責View的繪制調度、事件分發、窗口管理等功能。
????各層級View遵循單一父View對應多個子View的關系,通過嵌套形成樹形結構。
????由于ViewRootImpl不是真正的View,因此ViewRootImpl只是View調度的根節點,并不是View樹的根節點。View樹真正的根節點是DecorView。DecorView繼承自FrameLayout,是真正的View容器。ViewRootImpl通過管理DecorView,間接統籌管理所有層級的View。
1.DecorView的創建
????當啟動Activity時,系統會調用ActivityThread的handleLaunchActivity方法處理Activity的啟動流程。
????在ActivityThread的handleLaunchActivity方法中,會分別調用performLaunchActivity方法、handleStartActivity方法、handleResumeActivity方法,反射創建Activity,并回調Activity的生命周期,如下圖所示:
????在實際的開發過程中,通常會在Activity的onCreate方法中,調用setContentView方法,為Activity設置對應的View。在setContentView方法中,會調用installDecor方法,創建DecorView,如下圖所示:
2.ViewRootImpl的創建
????在ActivityThread的handleResumeActivity方法中,主要做了兩件事:
1)回調Activity的onResume方法,切換生命周期。
2)調用Activity的makeVisible方法,創建ViewRootImpl與DecorView進行綁定。
????在Activity的makeVisible方法中,會通過WindowManager創建ViewRootImpl對象,并與DecorView進行綁定,如下圖所示:
????在ViewRootImpl的setView方法中,ViewRootImpl會與DecorView進行雙向綁定,如下圖所示:
3.渲染體系與生命周期
????在Activity的首次啟動過程中:
- 回調onCreate方法時:調用setContentView方法,觸發DecorView的創建。
- 回調onStart方法時:DecorView完成創建,ViewRootImpl未創建。
- 回調onResume方法時:DecorView完成創建,ViewRootImpl未創建。回調后立刻創建ViewRootImpl,并與DecorView完成綁定。
二.線程檢測機制
1.異常產生
????CalledFromWrongThreadException異常的拋出發生在ViewRootImpl類的checkThread方法中。當對View進行更新時,最終都會調用ViewRootImpl類的checkThread方法進行線程檢測,代碼如下:
void checkThread() {Thread current = Thread.currentThread();if (mThread != current) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."+ " Expected: " + mThread.getName()+ " Calling: " + current.getName());}
}
????當判斷調用checkThread方法的線程和mThread不一致時,會拋出CalledFromWrongThreadException異常。
2.檢測路徑
????在ViewRootImpl中,共有13個方法在執行時會進行線程檢測。如下所示:
- requestFitSystemWindows:請求調整View的布局以適應系統窗口。
- requestLayout:請求重新對View布局。
- invalidateChildInParent:通知父View某個子View需要重繪。
- setWindowStopped:設置Window的停止狀態。
- requestTransparentRegion:請求計算View的透明區域。
- requestChildFocus:請求將焦點設置到某個子View上。
- clearChildFocus:清除子View焦點。
- focusableViewAvailable:通知父View某個子View可以獲取焦點。
- recomputeViewAttributes:重新計算View的屬性。
- playSoundEffect:播放與View交互相關的音效。
- focusSearch:在View樹中搜索下一個可以獲取焦點的View。
- keyboardNavigationClusterSearch:在鍵盤導航集群中搜索下一個可以獲取焦點的View。
- doDie:銷毀當前的ViewRootImpl。
????但與View更新最為密切的是requestLayout方法和invalidateChildInParent方法。
????在Android系統中,任何對View的更新操作,最終都要直接或間接調用View的invalidate方法或requestLayout方法。這兩個方法會觸發ViewRootImpl中的相應邏輯,在繪制調度前進行線程檢測。
????View的invalidate方法和requestLayout方法都會觸發ViewRootImpl對View重新進行繪制調度(measure、layout、draw),但二者的區別在于:
- invalidate方法:標記當前區域為dirty,表示需要重新繪制,并在下一次繪制調度中觸發draw流程,不會觸發measure流程和layout流程。
- requestLayout方法:清除已經測量的數據,并在下一次繪制調度中觸發measure流程和layout流程,如果在layout過程中發現View的大小發生變化,則會通過調用setFrame方法,間接觸發調用一次invalidate方法,并在下一次繪制調度中觸發draw流程。
1)invalidate方法觸發線程檢測
????當調用View的invalidate方法時,invalidate方法內部會調用父View的invalidateChild方法,通過循環的方式,一層一層的獲取父View,通知重新繪制,最終通知到ViewRootImpl,如下圖所示:
????在ViewRootImpl的invalidateChildInParent方法中,會進行線程檢測,代碼如下:
@Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {// 線程檢測checkThread();...return null;
}
2)requestLayout方法觸發線程檢測
????當調用View的requestLayout方法時,會調用父View的requestLayout方法。通過一層一層的遞歸調用向上通知,最終通知到ViewRootImpl,如下圖所示:
????在ViewRootImpl的requestLayout方法中,會進行線程檢測,代碼如下:
@Override
public void requestLayout() {if (!mHandlingLayoutInLayoutRequest) {// 線程檢測checkThread();mLayoutRequested = true;scheduleTraversals();}
}
三.子線程更新View
????子線程更新View的方式分為兩種:基于獨立渲染體系和基于ViewRootImpl渲染體系。需要注意的是,盡管ViewRootImpl渲染體系支持在子線程更新View,但為了保證View狀態的一致性,還是建議在UI線程更新View。
1.基于獨立渲染體系
1)使用SurfaceView繪制
????SurfaceView依靠自身維護BLASTBufferQueue獲取Surface,在SurfaceFlinger中擁有獨立的Layer。在繪制時不經過ViewRootImpl,詳情參考:SurfaceView與TextureView的繪制渲染,代碼如下:
class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 獲取SurfaceViewval view = findViewById<SurfaceView>(R.id.surface_view)// 創建調度器為IO線程的協程作用域val scope = CoroutineScope(Dispatchers.IO)// 監聽Surface變化view.holder.addCallback(object : SurfaceHolder.Callback {override fun surfaceCreated(holder: SurfaceHolder) {// Surface創建時啟動運行在IO線程的協程scope.launch {while (true) {// 每隔100ms繪制一次背景delay(100)val canvas = holder.lockCanvas()canvas.drawColor(Color.RED)holder.unlockCanvasAndPost(canvas)}}}override fun surfaceChanged(holder: SurfaceHolder,format: Int,width: Int,height: Int) {}override fun surfaceDestroyed(holder: SurfaceHolder) {// Surface銷毀時取消作用域內的協程scope.cancel()}})}
}
2)使用TextureView繪制
????TextureView依靠自身維護的SurfaceTexture獲取Surface,在繪制時不經過ViewRootImpl。
????但與SurfaceView不同的是,通過TextureView的Surface繪制后的內容,不會直接提交到SurfaceFlinger,而是通過回調的方式觸發調用一次invalidate方法,并在下一次繪制時通過硬件加速層的方式掛在View樹下一起繪制,詳情參考:SurfaceView與TextureView的繪制渲染,代碼如下:
class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 獲取TextureViewval view = findViewById<TextureView>(R.id.texture_view)// 創建調度器為IO線程的協程作用域val scope = CoroutineScope(Dispatchers.IO)// 監聽SurfaceTexture變化view.surfaceTextureListener = object : TextureView.SurfaceTextureListener {override fun onSurfaceTextureAvailable(surface: SurfaceTexture,width: Int,height: Int) {// SurfaceTexture創建時啟動運行在IO線程的協程scope.launch {while (true) {// 每隔100ms繪制一次背景delay(100)val canvas = view.lockCanvas() ?: continuecanvas.drawColor(Color.RED)view.unlockCanvasAndPost(canvas)}}}override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture,width: Int,height: Int) {}override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {// SurfaceTexture銷毀時取消作用域內的協程scope.cancel()return true}override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}}}
}
3)接管ViewRootImpl的Surface
????當在Activity中調用Window的takeSurface方法,會接管ViewRootImpl的Surface,Activity的渲染會脫離ViewRootImpl渲染體系,相當于整個Activity都變成了SurfaceView,代碼如下:
class TestActivity : AppCompatActivity() {override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)// 創建調度器為IO線程的協程作用域val scope = CoroutineScope(Dispatchers.IO)// 接管ViewRootImpl的Surfacewindow.takeSurface(object : SurfaceHolder.Callback2 {override fun surfaceCreated(holder: SurfaceHolder) {// Surface創建時啟動運行在IO線程的協程scope.launch {while (true) {// 每隔100ms繪制一次背景delay(100)val canvas = holder.lockCanvas()canvas.drawColor(Color.RED)holder.unlockCanvasAndPost(canvas)}}}override fun surfaceChanged(holder: SurfaceHolder,format: Int,width: Int,height: Int) {}override fun surfaceDestroyed(holder: SurfaceHolder) {// Surface銷毀時取消作用域內的協程scope.cancel()}override fun surfaceRedrawNeeded(holder: SurfaceHolder) {}})}
}
2.基于ViewRootImpl渲染體系
1)ViewRootImpl渲染體系形成前
????當Activity首次啟動并在onCreate方法內調用setContentView方法后,在onCreate方法、onStart方法、onResume方法中,使用非UI線程更新View,不會觸發線程檢測,代碼如下:
class TestActivity : AppCompatActivity() {// 創建調度器為IO線程的協程作用域private val scope = CoroutineScope(Dispatchers.IO)// 標記在onResume方法中執行一次private var firstResume = true// 標記在onStart方法中執行一次private var firstStart = trueoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// onCreate方法中,啟動運行在IO線程的協程scope.launch {// 更新TextView的文字內容findViewById<TextView>(R.id.text_view)?.text = "hello world"}}override fun onStart() {super.onStart()// 使用標志位,確保只在首次調用onStart時執行if(!firstStart) returnfirstStart = false// onStart方法中,啟動運行在IO線程的協程scope.launch {// 更新TextView的文字內容findViewById<TextView>(R.id.text_view)?.text = "hello world !"}}override fun onResume() {super.onResume()// 使用標志位,確保只在首次調用onResume時執行if (!firstResume) returnfirstResume = false// onResume方法中,啟動運行在IO線程的協程scope.launch {// 更新TextView的文字內容findViewById<TextView>(R.id.text_view)?.text = "hello world !!"}}
}
2)綁定ViewRootImpl渲染體系前
????當動態創建完View后,在沒有添加到與ViewRootImpl有關聯的ViewGroup前,在非UI線程更新View,不會觸發線程檢測,代碼如下:
class TestActivity : AppCompatActivity() {// 創建調度器為IO線程的協程作用域private val scope = CoroutineScope(Dispatchers.IO)override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 獲取TextView,監聽點擊事件findViewById<TextView>(R.id.text_view)?.setOnClickListener {// 當點擊TextView時,啟動運行在IO線程的協程scope.launch {// 創建一個TextViewval view = TextView(this@TestActivity)// 設置文本內容view.text = "hello world"// 切換到UI線程withContext(Dispatchers.Main) {// 添加到DecorView中this@TestActivity.addContentView(view, ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT))}}}}
}
4)硬件渲染模式下的invalidate方法
????在軟件渲染模式下,當調用View的invalidate方法時,會調用父類的invalidateChild方法。但在硬件渲染模式下,為了防止循環遍歷耗時,會直接調用onDescendantInvalidated方法,代碼如下:
@Override
public final void invalidateChild(View child, final Rect dirty) {final AttachInfo attachInfo = mAttachInfo;if (attachInfo != null && attachInfo.mHardwareAccelerated) {// HW accelerated fast pathonDescendantInvalidated(child, child);return;}...
}
????在ViewGroup的onDescendantInvalidated方法中,會通過遞歸調用的方式,最終調用ViewRootImpl的onDescendantInvalidated方法,如下圖所示:
????在ViewRootImpl的onDescendantInvalidated方法中,會直接調用invalidate方法,跳過線程檢查,代碼如下:
private static boolean sToolkitEnableInvalidateCheckThreadFlagValue =Flags.enableInvalidateCheckThread();@Override
public void onDescendantInvalidated(@NonNull View child, @NonNull View descendant) {// Android Tool Kit為debug留的開關,默認為falseif (sToolkitEnableInvalidateCheckThreadFlagValue) {checkThread();}if ((descendant.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0) {mIsAnimating = true;}invalidate();
}@UnsupportedAppUsage
void invalidate() {mDirty.set(0, 0, mWidth, mHeight);if (!mWillDrawSoon) {// 啟動繪制流程scheduleTraversals();}
}
????Android系統默認的渲染模式為硬件渲染,這里在AndroidManifest中再手動聲明一下,代碼如下:
<manifest xmlns:tools="http://schemas.android.com/tools"xmlns:android="http://schemas.android.com/apk/res/android"package="com.test.ui">...<!-- 啟動應用級別的硬件渲染模式 --><application android:hardwareAccelerated="true">...</application></manifest>
????在代碼使用上,硬件渲染與軟件渲染基本沒有差別。當開啟硬件渲染模式后,在子線程直接或間接調用View的invalidate方法不會產生崩潰,代碼如下:
class TestActivity : AppCompatActivity() {// 創建調度器為IO線程的協程作用域private val scope = CoroutineScope(Dispatchers.IO)// 文字大小private var size = 30foverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)}override fun onResume() {super.onResume()// 每次onResume時,啟動一個運行在IO線程的協程scope.launch {// 更新文字大小size += 10f// 獲取TextView,設置文字大小findViewById<TextView>(R.id.text_view)?.textSize = size}}
}
5)子線程中創建ViewRootImpl
????實際上,Android系統并未要求View的更新必須在UI線程中進行。
????通過分析CalledFromWrongThreadException異常拋出時的提示可以知道:View的更新必須在original thread中。而original thread就是ViewRootImpl中mThread字段保存的線程。
void checkThread() {Thread current = Thread.currentThread();if (mThread != current) {throw new CalledFromWrongThreadException("Only the original thread that created a view hierarchy can touch its views."+ " Expected: " + mThread.getName()+ " Calling: " + current.getName());}
}
????在ViewRootImpl的構造方法中,會對mThread進行初始化,代碼如下:
public ViewRootImpl(@UiContext Context context,Display display,IWindowSession session,WindowLayout windowLayout) {...// 獲取當前的線程并保存mThread = Thread.currentThread();...
}
????因此,Android系統要求View更新必須在UI線程執行,本質上是因為ViewRootImpl在UI線程被創建,并在構造方法中保存當前線程引用(mThread),并在每次操作時通過checkThread方法驗證調用線程是否與mThread一致。
????由于Activity的啟動需要系統調度,系統會將Activity的啟動安排在UI線程中進行,這也就導致無法在子線程中啟動Activity,進而無法在子線程中創建ViewRootImpl。
????但是在Android系統中,不僅Activity擁有ViewRootImpl,Dialog和PopupWindow等組件也各自擁有獨立的ViewRootImpl。
????如果在子線程中創建了Dialog或PopupWindow,那么后續對Dialog或PopupWindow中View的更新也必須在該子線程中進行,代碼如下:
class TestActivity : AppCompatActivity() {// 創建HandlerThread,并啟動子線程updateprivate val handleThread = HandlerThread("update").apply { start() }override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.layout_activity_test)// 創建子線程Handler,并向子線程update中提交一個任務Handler(handleThread.looper).post {// 獲取容器Viewval parent = findViewById<ViewGroup>(R.id.container)// 通過加載XML的方式,創建一個子Viewval view = LayoutInflater.from(this).inflate(R.layout.layout_test_popup_window, parent, false)// 創建PopupWindow,并將子View添加進去val popupWindow = PopupWindow(view,ViewGroup.LayoutParams.MATCH_PARENT,ViewGroup.LayoutParams.MATCH_PARENT)// 從子View中獲取TextViewval textView = popupWindow.contentView.findViewById<TextView>(R.id.pop_text)// 監聽TextView的點擊事件textView.setOnClickListener {// 更新TextView的文字內容,// 這里注意點擊事件的回調線程變成了子線程updatetextView.text = "${Thread.currentThread()}"// 這里會產生CalledFromWrongThreadException異常// 因為沒有在子線程update中更新window.decorView.post { textView.text = "${Thread.currentThread()}" }}// 這里先將任務提交到UI線程執行// 因為在onCreate方法中,容器View對應的Window還未創建好// 獲取不到Window的Token,會產生異常window.decorView.post {// 切換到子線程,創建子線程Handler,并向子線程update中提交一個任務Handler(handleThread.looper).post {// 子線程中展示popupWindow,會觸發ViewRootImpl在子線程update中創建popupWindow.showAtLocation(parent, Gravity.CENTER, 0, 0)}}}}
}
三.總結
1.View的更新必須在UI線程進行的原因
????ViewRootImpl在UI線程中被創建,并在構造方法中保存了當前線程的引用(mThread)。在每次更新View時,通過調用View的invalidate方法或requestLayout方法觸發ViewRootImpl的checkThread方法,驗證調用線程是否與mThread一致。
2.Activity啟動流程中渲染體系的創建
- 回調onCreate方法時:調用setContentView方法,觸發DecorView的創建。
- 回調onStart方法時:DecorView完成創建,ViewRootImpl未創建。
- 回調onResume方法時:DecorView完成創建,ViewRootImpl未創建。回調后立刻創建ViewRootImpl,并與DecorView完成綁定。
3.invalidate方法與requestLayout方法的區別
????View的invalidate方法和requestLayout方法都會觸發ViewRootImpl對View重新進行繪制調度(measure、layout、draw),但二者的區別在于:
- invalidate方法:標記當前區域為dirty,表示需要重新繪制,并在下一次繪制調度中觸發draw流程,不會觸發measure流程和layout流程。
- requestLayout方法:清除已經測量的數據,并在下一次繪制調度中觸發measure流程和layout流程,如果在layout過程中發現View的大小發生變化,則會通過調用setFrame方法,間接觸發調用一次invalidate方法,并在下一次繪制調度中觸發draw流程。
4.子線程更新View的方法
- 基于獨立渲染體系
- 使用SurfaceView,直接對Surface進行繪制。
- 使用TextureView,直接對Surface進行繪制。
- 接管ViewRootImpl的Surface,直接對Surface進行繪制。
- 基于ViewRootImpl渲染體系
- 在ViewRootImpl渲染體系形成前,使用子線程更新View。
- 在綁定ViewRootImpl渲染體系前,使用子線程更新View。
- 硬件渲染模式下,子線程直接或間接調用View的invalidate方法。
- 對于獨立擁有ViewRootImpl的組件,在子線程中觸發組件創建ViewRootImpl,并在對應的子線程中更新View。