只有主线程才能更新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,那么必然会涉及到同步的问题,所以就会在各个地方加锁,这样就会导致性能损耗。而如果只是在一个线程内更新的话,则不会存在这个问题。

你可能感兴趣的:(android,bugs)