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