今天作了一個功能,註冊一個監聽wifi鏈接的廣播,鏈接後顯示一個懸浮Textview,並在外面設置一個Button,用戶點擊後能夠傳遞一些內容給這個懸浮Textview來顯示,一切都是那麼順利,可等到運行的時候出錯了:html
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6891)
at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:1083)
at android.view.ViewGroup.invalidateChild(ViewGroup.java:5205)
at android.view.View.invalidateInternal(View.java:13656)
at android.view.View.invalidate(View.java:13620)
at android.view.View.invalidate(View.java:13604)
at android.widget.TextView.checkForRelayout(TextView.java:7347)
at android.widget.TextView.setText(TextView.java:4480)
at android.widget.TextView.setText(TextView.java:4337)
at android.widget.TextView.setText(TextView.java:4312)
at com.renny.demo.view.activity.HomeActivity$onCreate$1.onClick(HomeActivity.kt:57)
複製代碼
HomeActivity是我發送數據的地方。java
這個錯誤在我剛入門Android的時候遇到過幾回,如今已經很熟悉了,因而我花了1.25秒就想出了了問題所在,我確定是在子線程更新view了。嗯,雖然看了下代碼沒看出來這個子線程是哪一個,但機智的我仍是無論三七二十一,直接用handler拋到了主線程:android
new Handler(Looper.getMainLooper()).post(new Runnable() {
@Override
public void run() {
mTextviw.setText("hello");
}
});
複製代碼
無奈,問題依舊,看來事情並不簡單。bash
根據錯誤信息裏的提示,先來介紹下ViewRootImpl
,當它建立的時候,建立它的Thread會賦值給mThread成員變量。app
public ViewRootImpl(Context context, Display display) {
mContext = context;
mWindowSession = WindowManagerGlobal.getWindowSession();
mDisplay = display;
mBasePackageName = context.getBasePackageName();
mThread = Thread.currentThread();
複製代碼
當咱們給TextView設置文本內容的時候,會判斷mLayout是否爲空,若是不會空,則會調用checkForRelayout()方法。ide
private void setText(CharSequence text, BufferType type,boolean notifyBefore, int oldlen) {
// ......
if (mLayout != null) {
checkForRelayout();
}
// ......
}
複製代碼
這個mLayout先不用管,在第一次onMeasure的時候會賦值,如今Textview已經先顯示,因此此時不爲空。隨後在checkForRelayout()
中會調用oop
private void checkForRelayout() {
if (...) {
requestLayout();
invalidate();
} else {
nullLayouts();
requestLayout();
invalidate();
}
}
複製代碼
而後當View(或者是ViewGroup)調用invalidate()方法的時候,會調用父View的invalidateChild(View child, final Rect dirty)
,這裏面有一個do while循環,每循環一次會調用parent = parent.invalidateChildInParent(location, dirty)
,直到到最外面的View節點。post
當遍歷到根節點,也就是ViewRoot,調用ViewRoot的invalidateChildInParent
方法,實際是調用ViewRoot的實現類ViewRootImpl身上的invalidateChildInParent
方法。 當ViewGroup中的invalidateChild(View child, final Rect dirty)
方法循環到最外層的時候,這個mParent就是ViewRootImpl。 當調用到ViewRootImpl的invalidateChildInParent(int[] location, Rect dirty)
方法的時候會去檢測線程,也就是checkThread()。ui
checkThread()裏面會判斷當前線程是否是建立ViewRootImpl的那個線程,若是不是的就拋出異常。this
void checkThread() {
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
複製代碼
因此只要看到ViewRootImpl
建立的線程是否是主線程就好了。添加懸浮窗的過程當中會建立ViewRootImpl
:
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val lp = WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT or WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
gravity = Gravity.LEFT or Gravity.TOP
}
textView = TextView(applicationContext)
windowManager.addView(textView, lp)
複製代碼
在addView方法中建立:
public void addView(View view, LayoutParams params) {
//...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
this.mViews.add(view);
this.mRoots.add(root);
this.mParams.add(wparams);
}
複製代碼
最後加了下打印線程信息,竟然發現建立這個懸浮窗的線程不是主線程。。。就是由於註冊廣播的時候:
mContext.registerReceiver(mReceiver, mFilter, null, mWorkHandler)
複製代碼
因此onReceive
會在我指定的線程回調。
view只能在UI線程更新沒錯,但UI線程不必定等於主線程,而是建立ViewRootImpl
所在的線程,更準確的說是windowManager
添加rootview
所在的線程,當在子線程添加View時,這個子線程就是這個view和子view(若是有)所在的UI線程,咱們也就不能在另外的線程更新view了,包括主線程。最後,對於這個UI線程要求就是必定要開啓loop循環:
thread {
Looper.prepare()
val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager
val lp = WindowManager.LayoutParams().apply {
type = WindowManager.LayoutParams.TYPE_SYSTEM_ALERT or WindowManager.LayoutParams.TYPE_SYSTEM_OVERLAY
gravity = Gravity.LEFT or Gravity.TOP
}
textView = TextView(applicationContext)
windowManager.addView(textView, lp)
Looper.loop()
}
複製代碼
Only the original thread that created a view hierarchy can touch its views. 是怎麼產生的