只有主線程才能更新view嗎?

1.子線程更新產生異常

做過Android開發的同學都知道只有在主線程才能夠更新view,如果在子線程更新view,則會拋出異常。我們來看下這個異常到底是哪裏拋出來的。
如下代碼所示,新建了一個線程去更新view

new Thread(() -> {
    jumpBtn.setText("測試");
}).start();

這時拋出的異常如下

    android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8191)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1420)
        at android.view.View.requestLayout(View.java:24454)
        at android.view.View.requestLayout(View.java:24454)
        at android.view.View.requestLayout(View.java:24454)
        at android.view.View.requestLayout(View.java:24454)
        at android.view.View.requestLayout(View.java:24454)
        at android.view.View.requestLayout(View.java:24454)
        at android.view.View.requestLayout(View.java:24454)
        at android.widget.TextView.checkForRelayout(TextView.java:9681)
        at android.widget.TextView.setText(TextView.java:6269)
        at android.widget.TextView.setText(TextView.java:6097)
        at android.widget.TextView.setText(TextView.java:6049)
        at com.android.hdemo.MainActivity.lambda$onClick$0$MainActivity(MainActivity.java:27)
        at com.android.hdemo.MainActivity$$Lambda$0.run(Unknown Source:2)
        at java.lang.Thread.run(Thread.java:919)

從堆棧當中可以看出,異常是android.view.ViewRootImpl.checkThread拋出的,我們看下源碼。
從註釋2處,我們可以看到當mThread不等於當前線程時,就直接拋出異常,而mThread是ViewRootImpl在初始化的時候被賦的值,指的是初始化時候的線程。也就是更新view的線程必須要和創建ViewRootImpl的線程保持一致,否則就會拋出異常。

//ViewRootImpl構造函數
public ViewRootImpl(Context context, Display display) {
    ......
    //1.構造函數賦值
    mThread = Thread.currentThread();
    ......
}

void checkThread() {
    //2.若不相等,則拋出異常
    if (mThread != Thread.currentThread()) {
        throw new CalledFromWrongThreadException(
                "Only the original thread that created a view hierarchy can touch its views.");
    }
}

我們再來看下ViewRootImpl是啥時候被初始化的。
在ActivityThread的handleResumeActivity當中會執行WindowManagerImpl.addView,接着繼續執行WindowManagerGlobal.addView,在這個函數當中會創建ViewRootImpl。而handleResumeActivity是在主線程上執行,因此ViewRootImpl也是在主線程上被創建的,所以只有在主線程上才能更新view。

//ActivityThread
final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
            ......
                if (a.mVisibleFromClient) {
                    if (!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        //1.WindowManagerImpl.addView
                        wm.addView(decor, l);
                    } else {
                        a.onWindowAttributesChanged(l);
                    }
                }
              ......
}

//WindowManagerImpl
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
    applyDefaultToken(params);
    //2.WindowManagerGlobal.addView
    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
}

//WindowManagerGlobal
public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    ......
    //3.創建ViewRootImpl
    ViewRootImpl root;
    root = new ViewRootImpl(view.getContext(), display);
    .....
}

如果在子線程上創建的ViewRootImpl呢?是不是就可以在子線程更新view了?

2.子線程更新view

如下代碼所示,我們在子線程裏面調用WindowManagerImpl的addview方法,往窗口上加一個View,這樣在子線程創建了一個ViewRootImpl,此時如果在主線程或者其他的子線程更新我們添加的button,就會爆出異常。
所以並不是只能在主線程更新view,而是必須要在創建ViewRootImpl的線程裏面更新view。

new Thread(() -> {
    Looper.prepare();
    WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams();
    layoutParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
    layoutParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
    getWindowManager().addView(button,layoutParams);
    Looper.loop();
}).start();

3.爲什麼要這麼設計

爲什麼Google要這麼設計呢?如果不這麼設計會有什麼問題?
如果不這麼設計的話,那麼所有的線程均可以更新view,那麼必然會涉及到同步的問題,所以就會在各個地方加鎖,這樣就會導致性能損耗。而如果只是在一個線程內更新的話,則不會存在這個問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章