1. 程式人生 > 實用技巧 >面試官:啊?做了三年Android,子執行緒能不能更新 UI不知道,連UI 執行緒是什麼都說不清楚...

面試官:啊?做了三年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() 拋異常的原因.

當我們明白以上的結論後,我會分析以下問題:

  1. Activity#onCreate()中使用子執行緒更新 TextView 內容崩不崩?
  2. ViewRootImpl初始化完成之後,我能在子執行緒更新 TextView 內容麼?
  3. 走進 TextView.setText() , 找 requestLayout()invalidatecheckThread()的深層聯絡。

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:

發現mThreadViewRootImp 的構造方法中完成賦值,即ViewRootImp(...) 的呼叫者所線上程. 當我們繼續ALT+ f7會發現無跡可尋。

可能你對 ViewRootImp 較為陌生,有時間我會從 Activity啟動流程出發,講一講ViewRootImp,另外,ViewRootImpWindow有著強烈的關係。

先告訴大家一個較為上層的結論: 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 內容麼?

能,當 ViewRootImplsetContentView(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()invalidatecheckThread()的深層聯絡

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

https://mp.weixin.qq.com/s/B5zIMIR1rPT8euTK-spnbg

https://juejin.cn/post/6844904147943178247