咱們從一個異常提及:java
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8820)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1530)
at android.view.View.requestLayout(View.java:24648)
at android.widget.TextView.checkForRelayout(TextView.java:9752)
at android.widget.TextView.setText(TextView.java:6326)
at android.widget.TextView.setText(TextView.java:6154)
at android.widget.TextView.setText(TextView.java:6106)
at com.hfy.demo01.MainActivity$9.run(MainActivity.java:414)
at android.os.Handler.handleCallback(Handler.java:888)
at android.os.Handler.dispatchMessage(Handler.java:100)
at android.os.Looper.loop(Looper.java:213)
at android.app.ActivityThread.main(ActivityThread.java:8147)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:513)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:1101)
複製代碼
通常狀況,咱們在子線程直接操做UI,沒有用handler切到主線程,就會報這個錯。android
==那若是我說,我這裏的這個錯誤就發生在 主線程,你信嗎?==安全
下面是具體代碼,handleAddWindow()按在MainActivity 的onCreate中執行。app
private void handleAddWindow() {
//子線程建立window,只能由這個子線程訪問 window的view
Button button = new Button(MainActivity.this);
button.setText("添加到window中的button");
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
MyToast.showMsg(MainActivity.this, "點了button");
}
});
new Thread(new Runnable() {
@Override
public void run() {
//由於添加window是IPC操做,回調回來時,須要handler切換線程,因此須要Looper
Looper.prepare();
addWindow(button);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
button.setText("文字變了!!!");
}
},3000);
//開啓looper,循環取消息。
Looper.loop();
}
}).start();
//這裏執行就會報錯:Only the original thread that created a view hierarchy can touch its views.
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
button.setText("文字 you 變了!!!");
}
},4000);
}
private void addWindow(Button view) {
WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(
WindowManager.LayoutParams.WRAP_CONTENT,
WindowManager.LayoutParams.WRAP_CONTENT,
0, 0,
PixelFormat.TRANSPARENT
);
// flag 設置 Window 屬性
layoutParams.flags= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
// type 設置 Window 類別(層級)
layoutParams.type = WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
layoutParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
}
layoutParams.gravity = Gravity.TOP | Gravity.LEFT;
layoutParams.x = 100;
layoutParams.y = 100;
WindowManager windowManager = getWindowManager();
windowManager.addView(view, layoutParams);
}
複製代碼
主要是:開了個子線程,而後添加了一個系統window,window中只有一個button。而後3秒後在子線程中直接改變Button的文字,而後又過一秒,在主線程中再改變button文字。ide
(其中涉及知識有handler、window。可點擊查看相關知識)oop
執行效果以下,可見 打開App後,左上角的Button,3秒後變了,接着一秒後crash了。 post
那爲啥 子線程更新UI沒報錯,主線程報錯呢?ui
首先,咱們看報錯緣由的描述: Only the original thread that created a view hierarchy can touch its views. 翻譯就是說 只有建立了view樹的線程,才能訪問它的子view。並無說子線程必定不能訪問UI。那能夠猜測到,button的確實是在子線程被添加到window中的,子線程確實能夠直接訪問,而主線程訪問確實會拋出異常。看來能夠解釋這個錯誤的緣由了。 下面就具體分析下。this
錯誤的發生在ViewRootImpl的checkThread方法中,且UI的更新都會走到這個方法:spa
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
複製代碼
(ViewRootImpl相關知識能夠戳這裏View的工做原理)
經過window的相關知識,咱們知道,調用windowManager.addView添加window時會給這個window建立一個ViewRootImpl實例:
public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
...
}
}
複製代碼
而後ViewRootImpl構造方法中會拿到當前的線程,
public ViewRootImpl(Context context, Display display) {
mContext = context;
...
mThread = Thread.currentThread();
...
}
複製代碼
因此在ViewRootImpl的checkThread()中,確實是 拿 當前想要更新UI的線程 和 添加window時的線程做比較,不是同一個線程機會報錯。
經過window的相關知識,咱們還知道,Activity也是一個window,window的添加是在ActivityThread的handleResumeActivity()。ActivityThread就是主線程,因此Activity的view訪問只能在主線程中。
通常狀況,UI就是指Activity的view,這也是咱們一般稱主線程爲UI線程的緣由,其實嚴謹叫法應該是activity的UI線程。而咱們這個例子中,這個子線程也能夠稱爲button的UI線程。
那爲啥要必定須要checkThread呢?根據handler的相關知識:
由於UI控件不是線程安全的。那爲啥不加鎖呢?一是加鎖會讓UI訪問變得複雜;二是加鎖會下降UI訪問效率,會阻塞一些線程訪問UI。因此乾脆使用單線程模型處理UI操做,使用時用Handler切換便可。
咱們再看一個問題,Toast能夠在子線程show嗎?答案是能夠的
new Thread(new Runnable() {
@Override
public void run() {
//由於添加window是IPC操做,回調回來時,須要handler切換線程,因此須要Looper
Looper.prepare();
addWindow(button);
new Handler().postDelayed(new Runnable() {
@Override
public void run() {
button.setText("文字變了!!!");
}
},3000);
Toast.makeText(MainActivity.this, "子線程showToast", Toast.LENGTH_SHORT).show();
//開啓looper,循環取消息。
Looper.loop();
}
}).start();
複製代碼
在上面的例子,線程中showToast,運行發現確實能夠的。由於根據window的相關知識,知道Toast也是window,show的過程就是添加Window的過程。
另外注意1,這個線程中Looper.prepare()和Looper.loop(),這是必要的。 由於添加window的過程是和WindowManagerService進行IPC的過程,IPC回來時是執行在binder線程池的,而ViewRootImpl中是默認有Handler實例的,這個handler就是用來切換binder線程池的消息到當前線程。 另外Toast還與NotificationMamagerService進行IPC,也是須要Handler實例。既然須要handler,那因此線程是須要looper的。另另外Activity還與ActivityManagerService進行IPC交互,而主線程是默認有Looper的。 擴展開,想在子線程show Toast、Dialog、popupWindow、自定義window,只要在先後調Looper.prepare()和Looper.loop()便可。
另外注意2,在activity的onCreate到首次onResume的時期,建立子線程在其中更新UI也是能夠的。這不是違背上面的結論了嗎?其實沒有,上面說了,由於Activity的window添加在首次onResume以後執行的的,那ViewRootImpl的建立也是在這以後,因此也就沒法checkThread了。實際上這個時期也不checkThread,由於View根本尚未顯示出來。
onCreate()中執行是OK的:
new Thread(new Runnable() {
@Override
public void run() {
tv.setText("text");
}
}).start();
複製代碼
.
最後,歡迎留言討論,若是你喜歡這篇文章,請幫忙 點贊、收藏和轉發,感謝!
歡迎關注個人 公 衆 號:
![]()