什麼?你跟我說只能在子線程更新View!

背景

今天作了一個功能,註冊一個監聽wifi鏈接的廣播,鏈接後顯示一個懸浮Textview,並在外面設置一個Button,用戶點擊後能夠傳遞一些內容給這個懸浮Textview來顯示,一切都是那麼順利,可等到運行的時候出錯了:html

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
                      at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
                      at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
                      at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
                      at android.view.View.invalidateInternal(View.java:13656)
                      at android.view.View.invalidate(View.java:13620)
                      at android.view.View.invalidate(View.java:13604)
                      at android.widget.TextView.checkForRelayout(TextView.java:7347)
                      at android.widget.TextView.setText(TextView.java:4480)
                      at android.widget.TextView.setText(TextView.java:4337)
                      at android.widget.TextView.setText(TextView.java:4312)
        at com.renny.demo.view.activity.HomeActivity$onCreate$1.onClick(HomeActivity.kt:57)
複製代碼

HomeActivity是我發送數據的地方。java

這個錯誤在我剛入門Android的時候遇到過幾回,如今已經很熟悉了,因而我花了1.25秒就想出了了問題所在,我確定是在子線程更新view了。嗯,雖然看了下代碼沒看出來這個子線程是哪一個,但機智的我仍是無論三七二十一,直接用handler拋到了主線程:android

new Handler(Looper.getMainLooper()).post(new Runnable() {
            @Override
            public void run() {
                mTextviw.setText("hello");
            }
        });
複製代碼

無奈,問題依舊,看來事情並不簡單。bash

分析

根據錯誤信息裏的提示,先來介紹下ViewRootImpl,當它建立的時候,建立它的Thread會賦值給mThread成員變量。app

public ViewRootImpl(Context context, Display display) {
        mContext = context;
        mWindowSession = WindowManagerGlobal.getWindowSession();
        mDisplay = display;
        mBasePackageName = context.getBasePackageName();
        mThread = Thread.currentThread();
複製代碼

當咱們給TextView設置文本內容的時候,會判斷mLayout是否爲空,若是不會空,則會調用checkForRelayout()方法。ide

private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {
    // ......
    if (mLayout != null) {
        checkForRelayout();
    }
   // ......
}

複製代碼

這個mLayout先不用管,在第一次onMeasure的時候會賦值,如今Textview已經先顯示,因此此時不爲空。隨後在checkForRelayout()中會調用oop

private void checkForRelayout() {
    if (...) {
        requestLayout();
        invalidate();
    } else {
        nullLayouts();
        requestLayout();
        invalidate();
    }
}
複製代碼

而後當View(或者是ViewGroup)調用invalidate()方法的時候,會調用父View的invalidateChild(View child, final Rect dirty),這裏面有一個do while循環,每循環一次會調用parent = parent.invalidateChildInParent(location, dirty),直到到最外面的View節點。post

當遍歷到根節點,也就是ViewRoot,調用ViewRoot的invalidateChildInParent方法,實際是調用ViewRoot的實現類ViewRootImpl身上的invalidateChildInParent方法。 當ViewGroup中的invalidateChild(View child, final Rect dirty)方法循環到最外層的時候,這個mParent就是ViewRootImpl。 當調用到ViewRootImpl的invalidateChildInParent(int[] location, Rect dirty)方法的時候會去檢測線程,也就是checkThread()。ui

checkThread()裏面會判斷當前線程是否是建立ViewRootImpl的那個線程,若是不是的就拋出異常。this

void checkThread() {
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}
複製代碼

因此只要看到ViewRootImpl建立的線程是否是主線程就好了。添加懸浮窗的過程當中會建立ViewRootImpl

val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
            val lp = WindowManager.LayoutParams().apply {
                type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT or WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
                gravity = Gravity.LEFT or Gravity.TOP
            }
            textView = TextView(applicationContext)
            windowManager.addView(textView, lp)
複製代碼

在addView方法中建立:

public void addView(View view, LayoutParams params) {
        //...
        root = new ViewRootImpl(view.getContext(), display);
        view.setLayoutParams(wparams);
        this.mViews.add(view);
        this.mRoots.add(root);
        this.mParams.add(wparams);
    }
複製代碼

最後加了下打印線程信息,竟然發現建立這個懸浮窗的線程不是主線程。。。就是由於註冊廣播的時候:

mContext.registerReceiver(mReceiver, mFilter, null, mWorkHandler)
複製代碼

因此onReceive會在我指定的線程回調。

結論

view只能在UI線程更新沒錯,但UI線程不必定等於主線程,而是建立ViewRootImpl所在的線程,更準確的說是windowManager添加rootview所在的線程,當在子線程添加View時,這個子線程就是這個view和子view(若是有)所在的UI線程,咱們也就不能在另外的線程更新view了,包括主線程。最後,對於這個UI線程要求就是必定要開啓loop循環:

thread {
            Looper.prepare()
                   val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
            val lp = WindowManager.LayoutParams().apply {
                type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT or WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
                gravity = Gravity.LEFT or Gravity.TOP
            }
            textView = TextView(applicationContext)
            windowManager.addView(textView, lp)
            Looper.loop()
        }
複製代碼

參考

Only the original thread that created a view hierarchy can touch its views. 是怎麼產生的

相關文章
相關標籤/搜索