问题由来
我们知道,Andoird由于修改UI是线程不安全的,只能在主线程中修改。如果多个线程修改UI肯定会花屏,于是谷歌做了限制,只能在主线程中修改UI。但是有次我在子线程中修改了UI没弹异常。
先来看两段代码
//正常运行
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { resultTv.setText("更新TextView"); } }).start(); } });
闪退,控制台异常为:Only the original thread that created a view hierarchy can touch its views.
//弹出异常
btn1.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { resultTv.setText("更新TextView ");//这里不一样,多了个换行符 } }).start(); } });
源码解读
之前的博客有解读ViewRootImpl是负责View的绘制,在requestLayout这个方法中会检查是否是当前线程。所以只要子线程修改UI但不改变UI布局时,不会弹出非主线程的异常。
@Override public void requestLayout() { if (!mHandlingLayoutInLayoutRequest) { checkThread(); mLayoutRequested = true; scheduleTraversals(); } } void checkThread() { if (mThread != Thread.currentThread()) { throw new CalledFromWrongThreadException( "Only the original thread that created a view hierarchy can touch its views."); } }
那么问题来了,假设我在onCreate的时候修改UI,layout也变了,为什么没报错呢?
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); new Thread(new Runnable() { @Override public void run() { resultTv.setText("onCreate "); } }).start(); }
在ActivityThread中发现,ViewRootImpl是在onResume的时候被初始化的,上面那段代码sleep久一点等ViewRootImpl初始化完毕就会报错
final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward) { if (r.window == null && !a.mFinished && willBeVisible) { r.window = r.activity.getWindow(); View decor = r.window.getDecorView(); decor.setVisibility(View.INVISIBLE); ViewManager wm = a.getWindowManager();//ViewRootImpl在这里被初始化 WindowManager.LayoutParams l = r.window.getAttributes(); a.mDecor = decor; l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION; l.softInputMode |= forwardBit; if (a.mVisibleFromClient) { a.mWindowAdded = true; wm.addView(decor, l); } } }
总结
- 子线程可以在部分情况下修改UI,如不改变布局,在onResume之前
- 不推荐在子线程中修改UI