从Android开发的第一课开始,我们就有一个常识,即子线程不能更新UI,只能在转到主线程去更新?所以我们在编码时都遵照着这个原则,获取到数据后通过handler去转到主线程,通过Message拿到子线程发送过来的数据,具体可以看我的另一篇博客:
Android之Handler消息机制——深入理解 Looper、Handler、Message、MessageQueue
那么问题来了,为什么我们不能在子线程去更新UI呢?我们先动手做一个实验!
在XML文件中只添加一个TextView,在onCreate()方法中新建一个线程名为Thread#2的线程,并在Thread#2线程中去更新UI
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
tv = (TextView) findViewById(R.id.tv);
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
Log.i("TAG", Thread.currentThread().getName());
tv.setText("随机数" + (1 + Math.random() * (100 - 1 + 1)));
}
});
thread.setName("Thread#2");
thread.start();
通过点击生成随机数,并打印当前执行更新UI是否在子线程
以上是执行和日志,可以看到,当前更新UI执行的线程是Thread#2,但是我们的APP并没有死掉,textView的内容也更新了!也就是说,子线程可以更新UI!但是为什么谷歌官方禁止在子线程更新UI呢?我们接下来继续做实例!
我们在子线程进行延时2秒,在看下效果!
竟然崩溃了,那问题来了,到底子线程能不能更新Ui呢?
2019-12-18 14:12:57.299 18904-18995/com.insigma.sdcard E/AndroidRuntime: FATAL EXCEPTION: Thread#2
Process: com.insigma.sdcard, PID: 18904
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:8000)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1292)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at android.view.View.requestLayout(View.java:23147)
at androidx.constraintlayout.widget.ConstraintLayout.requestLayout(ConstraintLayout.java:3026)
at android.view.View.requestLayout(View.java:23147)
at android.widget.TextView.checkForRelayout(TextView.java:8914)
at android.widget.TextView.setText(TextView.java:5736)
at android.widget.TextView.setText(TextView.java:5577)
at android.widget.TextView.setText(TextView.java:5534)
at com.insigma.sdcard.MainActivity$82.run(MainActivity.java:1336)
at java.lang.Thread.run(Thread.java:764)
我们看一下报错日志,出错的线程是Thread#2,也就是我们创建的线程,出错的原因大概意思就是:只有创建视图层级的原始线程,有权利处理它的视图,创建视图的线程就是主线程(UI线程),那为什么第一个例子没有出现崩溃呢?
我们看一下错误日志中的处理,是在checkThread()方法出现了崩溃,大概调用的流程是:
setText() ——> checkForRelayout()——>requestLayout()——>ViewRootImpl.checkThread()
但是还是没有解释为什么一下能更新一下又不能更新,不要着急,我们跟着调用的流程继续去解析源码!
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
//省略其他代码
if (mLayout != null) {
checkForRelayout();
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
//省略其他代码
}
sendOnTextChanged()和onTextChanged()方法应该都是用来绘制文本的,我们接着看checkForRelayout()方法
private void checkForRelayout() {
// If we have a fixed width, we can just swap in a new text layout
// if the text height stays the same or if the view height is fixed.
if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
//省略代码
// We lose: the height has changed and we have a dynamic height.
// Request a new view layout using our new text layout.
requestLayout();
invalidate();
} else {
// Dynamic width, so we have no choice but to request a new
// view layout with a new text layout.
nullLayouts();
requestLayout();
invalidate();
}
}
invalidate()方法是用来重新绘制视图的
我们继续看requestLayout()方法
public void requestLayout() {
//省略其他代码
if (mParent != null && !mParent.isLayoutRequested()) {
mParent.requestLayout();
}
if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
mAttachInfo.mViewRequestingLayout = null;
}
}
//View获取到父View,mParent,然后调用父View的requestLayout方法,比如示例中的父View就是xml文件的
//根布局就是RelativeLayout
@Override
public void requestLayout() {
super.requestLayout();
mDirtyHierarchy = true;
}
//通过调用父类的ViewRootImp.requestLayout(),最终到checkThread()
@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}
最重要的是checkThread()方法了,光看名字应该是用来检查线程的
void checkThread() {
// 如果当前线程不是主线程就抛出异常
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}
最重要的方法来了,mThread线程是主线程,Thread.currentThread()是当前线程,即我们运行的子线程
假如当前更新UI不在主线程,就会导致CalledFromWrongThreadException异常
由此可见,每一次刷新View都会调用ViewRootImp的checkThread()方法去检测是否在主线程
继续上一个问题,为什么onCreate里面可以更新UI?是因为没有调用checkThread()方法吗?
这个时候,我们可以搬出Activity的生命周期来解释一下了!
activity的声明周期经过oncreate()->onstart()->onResume()->onRestart()->onPouse()->onStop()->onDestory()
先强行解释一波:会不会是因为onCreate()时UI还没有绘制完,所以不需要通过ViewRootImp去检查是否是哪个线程,延时两秒之后,Activity回调onResume()方法,View绘制完成,刷新View需要走checkThread()方法
我们直接看onResume()方法:
@Override
public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
String reason) {
...省略
// 初始化onResume方法
final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
...
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
...省略
if (r.activity.mVisibleFromClient) {
r.activity.makeVisible();
}
}
...省略
}
直接看 r.activity.makeVisible()
void makeVisible() {
if (!mWindowAdded) {
ViewManager wm = getWindowManager();
wm.addView(mDecor, getWindow().getAttributes());
mWindowAdded = true;
}
mDecor.setVisibility(View.VISIBLE);
}
makeVisible方法将View添加到WindowMannager窗口中,即准备展示View
public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
...
ViewRootImpl root;
View panelParentView = null;
synchronized (mLock) {
...
root = new ViewRootImpl(view.getContext(), display);
view.setLayoutParams(wparams);
mViews.add(view);
mRoots.add(root);
mParams.add(wparams);
// do this last because it fires off messages to start doing things
try {
root.setView(view, wparams, panelParentView);
} catch (RuntimeException e) {
// BadTokenException or InvalidDisplayException, clean up.
if (index >= 0) {
removeViewLocked(index, true);
}
throw e;
}
}
}
在addView方法中,我们才会对ViewRootImpl对象进行初始化,也就是说,执行onCreate()在子线程中更新UI时ViewRootImpl还没出生,当然也不会去检查线程!
按理来说,这样是可以的!但是google为什么要这样去设计呢?
(1)如果在不同的线程去控制用一个控件,由于网络延时或者大量耗时操作,会使UI绘制错乱,出了问题也很难去排查到底是哪个线程更新时出了问题;
(2)主线程负责更新,子线程负责耗时操作,能够大大提高响应效率
(3)UI线程非安全线程,多线程进行并发访问有可能会导致内存溢出,降低硬件使用寿命;且非线程安全不能加Lock线程锁,否则会阻塞其他线程对View的访问,效率就会变得低下!