面試官:啊?做了三年Android,子執行緒能不能更新 UI不知道,連UI 執行緒是什麼都說不清楚...
面試官:說說什麼是 UI 執行緒?
A:就是用來重新整理 UI 所在的執行緒嘛
面試官:多說點
A:UI 是單執行緒重新整理的,如果多個執行緒可以重新整理 UI 就無所謂是不是 UI 執行緒了,單執行緒的好處是,UI 框架裡不需要到處上鎖,做執行緒同步,寫起來也比較簡單有效
面試官:你說的這個 UI 執行緒,它到底是哪個執行緒?是主執行緒嗎?
A:拿 Activity 來說,我們在 Activity 裡非同步做完耗時操作,要重新整理 UI 可以呼叫 Activity.runOnUiThread 方法,在 UI 執行緒中執行,那麼我們看下這個方法自然就知道 UI 執行緒是哪個執行緒了。
public final void runOnUiThread(Runnable action) { if (Thread.currentThread() != mUiThread) { mHandler.post(action); } else { action.run(); } }
這個方法會判斷當前是不是在主執行緒,不是呢就通過 mHandler 拋到主執行緒去執行。 這個 mHandler 是 Activity 裡的一個全域性變數,在 Activity 建立的時候通過無參建構函式 new Handler()
一起建立了。
因為是無參,所以建立時用的哪個執行緒,Handler 裡的 Looper 用的就是哪個執行緒了。建立 Activity 是在應用的主執行緒,因此 mHandler.post 去執行的執行緒也是主執行緒。 剛也說 了,runOnUiThread 方法裡,先判斷是不是在 UI 執行緒,這個 mUiThread 又是什麼時候賦值的呢,答案還在 Activity 的原始碼裡
final void attach(Context context, ...) {
...省略無關程式碼
mUiThread = Thread.currentThread();
}
在 Activity.attach 方法裡,我們把當前執行緒賦值給 mUiThread,那當前執行緒是什麼執行緒呢,也是主執行緒。至於為什麼建立 Activity 和 attach 都是主執行緒,那又是另外一個故事了 通過前面的分析,我們知道了,對於 Activity 來講 UI 執行緒就是主執行緒
面試官:所以你的結論是 UI 執行緒就是主執行緒?
A:這是你說的,記住這個開發的時候不會錯,但是不夠準確。在子執行緒裡重新整理 UI 的時候會拋一個異常
ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
大意是隻有最初始建立 View 層級關係的執行緒可以 touch view,這裡指的也就是 ViewRootImpl 建立時所在的執行緒,嚴格來說這個執行緒不一定是主執行緒。這一點呢,讀 View.post 方法也可以得到相同的結論。所以對於 View 來說,UI 執行緒就是 ViewRootImpl 建立時所在的執行緒,Activity 的 DecorView 對應的 ViewRootImpl 是在主執行緒建立的
面試官:這個 ViewRootImpl 什麼時候建立
A:Activity 建立好之後,應用的主執行緒會呼叫 ActivityThread.handleResumeActivity,這個方法會把 Activity 的 DecorView 新增到 WindowManger 裡,就是在這個時候建立的 ViewRootImpl
面試官:那可以在非同步執行緒重新整理 View 嗎?
A:剛才我們說了,只要是 ViewRootImpl 建立的執行緒就可以 touch view,然後 WindowManger.addView 的時候又會去建立 ViewRootImpl,所以我們只要在子執行緒呼叫 WindowManger.addView,這個時候新增的這個 View,就只能在這個子執行緒重新整理了,這個子執行緒就是這個 View 的 UI 執行緒了。
面試官:好,我們再聊點別的
子執行緒能不能更新 UI?
從根源上分析,Only the original thread that created a view hierarchy can touch its views
產生的條件以及原因, 其實就是:
checkThread()
呼叫的條件.checkThread()
拋異常的原因.
當我們明白以上的結論後,我會分析以下問題:
- 在
Activity#onCreate()
中使用子執行緒更新TextView
內容崩不崩? - 在
ViewRootImpl
初始化完成之後,我能在子執行緒更新TextView
內容麼? - 走進
TextView.setText()
, 找requestLayout()
和invalidate
與checkThread()
的深層聯絡。
checkThread()
什麼情況下會被呼叫.
通過對 checkThread()
執行 「alt + F7」發現:
其中我們常用的就是 requestLayout()
和 invalidate()
其中的
invalidateChildInParent()
其實就是invalidate()
的呼叫結果。
需要特別注意的是:
即開啟硬體加速的情況下,invalidate()
不會呼叫 checkThread()
target API 級別為 14 及更高級別,則硬體加速預設處於啟用狀態
基於以上分析, 得出結論:
requestLayout()
和 未開啟硬體加速時 invalidate()
的呼叫會觸發 checkThread()
checkThread()
什麼情況下會拋異常
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
出錯的條件很明顯,mThread
呼叫者所線上程A和 checkThread()
呼叫者所線上程B不一致。
基於第一部分的分析,我們不難得出執行緒A 就是 ViewRootImpl
所在的執行緒。
那其中的 mThread
是什麼呢?那這個 mThread
就是主執行緒麼? 通過對其執行ALT+ f7
:
發現mThread
在 ViewRootImp
的構造方法中完成賦值,即ViewRootImp(...)
的呼叫者所線上程. 當我們繼續ALT+ f7
會發現無跡可尋。
可能你對
ViewRootImp
較為陌生,有時間我會從Activity啟動流程
出發,講一講ViewRootImp
,另外,ViewRootImp
和Window
有著強烈的關係。
先告訴大家一個較為上層的結論: ActivityThread#handleResumeActivity
所在的執行緒就是 ViewRootImp(...)
的呼叫者所線上程,即主執行緒。
基於以上分析, 得出結論:
View
所在的執行緒要和 setContentView(R.layout.xxx)
所在的執行緒不一致時,執行 checkThread()
會拋異常。
好了,現在開始分析一下實際問題。
在Activity#onCreate()
中使用子執行緒更新 TextView
內容崩不崩?
分情況,情景一:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val name = this.findViewById<TextView>(R.id.btn_question)
thread(start = true) {
val s: String = name.text.toString()
name.text = "2222"
}
}
不會崩。因為此時 ViewRootImp
並未完成例項化,更別說呼叫其例項方法 checkThread()
了。
情景二:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val name = this.findViewById<TextView>(R.id.btn_question)
thread(start = true) {
val s: String = name.text.toString()
Thread.sleep(3000)
name.text = "2222"
}
}
崩。因為此時 ViewRootImp
已經例項化完成,更新內容的過程中呼叫了例項方法 checkThread()
。
在ViewRootImpl
初始化完成之後,我能在子執行緒更新TextView
內容麼?
能,當 ViewRootImpl
和 setContentView(R.layout.xxx)
在同一子執行緒時。程式碼如下:
private fun showDialogInNonMainThread() {
thread(start = true, name = "non-ui-thread") {
showTestDialog()
}
}
private fun showTestDialog() {
Looper.prepare();
val questionDialog = TestDialog(this)
questionDialog.show()
Looper.loop();
}
class TestDialog(context: Context) : Dialog(context) {
private val mTvTitle: TextView
init {
setContentView(R.layout.dialog_layout)
mTvTitle = findViewById(R.id.tv_title)
mTvTitle.text = "Zhug SniffTheRose"
}
}
上面的示例就展示了在非主執行緒 non-ui-thread
中也能更新UI的場景。
走進 TextView.setText()
, 找 requestLayout()
和 invalidate
與 checkThread()
的深層聯絡
TextView.setText()
通過checkForRelayout()
完成UI更新。
@UnsupportedAppUsage
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
|| (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
&& (mHint == null || mHintLayout != null)
&& (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
// Static width, so try making a new text layout.
int oldht = mLayout.getHeight();
int want = mLayout.getWidth();
int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
/*
* No need to bring the text into view, since the size is not
* changing (unless we do the requestLayout(), in which case it
* will happen at measure).
*/
makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
false);
if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
// In a fixed-height view, so use our new text layout.
if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
&& mLayoutParams.height != LayoutParams.MATCH_PARENT) {
autoSizeText();
invalidate();
return;
}
// Dynamic height, but height has stayed the same,
// so use our new text layout.
if (mLayout.getHeight() == oldht
&& (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
autoSizeText();
invalidate();
return;
}
}
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}
我們可以發現,當 TextView 的寬高不發生變化時,只會呼叫 invalidate()
, 結合本文前部分的結論,如果開啟了硬體加速,就不會走 checkThrea()
所以就算在子執行緒,也不會崩潰。
Android中高階面試必知必會電子書(1294頁)
資料已經上傳在我的GitHub免費下載!誠意滿滿!!!
聽說一鍵三連的粉絲都面試成功了?如果本篇部落格對你有幫助,請支援下小編哦
Android高階面試精選題、架構師進階實戰文件傳送門:我的GitHub
整理不易,覺得有幫助的朋友可以幫忙點贊分享支援一下小編~
你的支援,我的動力;祝各位前程似錦,offer不斷!!!
參考:
https://juejin.cn/post/6915034015544115214