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,那麼必然會涉及到同步的問題,所以就會在各個地方加鎖,這樣就會導致性能損耗。而如果只是在一個線程內更新的話,則不會存在這個問題。