0
点赞
收藏
分享

微信扫一扫

为什么子线程都不能刷新UI?


1. 理论上的原因

1.1 Android主线程是线程不安全的?

网上文章常常有说:Android主线程是线程不安全的。我就纳闷了,线程还有安全一说?

不能说主线程是线程不安全。线程没有安全不安全这一说。而是更新UI的方法不是线程安全的,即只能在单线程中完成UI的更新,不能使用多线程。(为什么呢?因为子线程可能会有多个,存在多个线程同时操作一个控件的情况)因此,只能在主线程中进行UI更新。

1.2 Android的单线程模型

Android的单线程模型有两条原则:

  • 不要阻塞UI线程。
  • 不要在UI线程之外访问Android UI toolkit(主要是这两个包中的组件:​​android.widget​​​ and​​android.view​​)

1.3 APP Process

在一个Android 程序开始运行的时候,会单独启动一个进程Process。默认的情况下,所有这个程序中的Activity或者Service(Service和Activity只是Android提供的Components中的两种,除此之外还有Content Provider和Broadcast Receiver)都会跑在这个进程空间里。

1.4 UI线程(主线程)

一个Android 程序默认情况下也只有一个进程Process,但可以有许多个线程Thread。在这么多Thread当中,有一个Thread,我们称之为UI Thread。UI Thread在Android程序运行的时候就被创建,是一个Process当中的主线程Main Thread,主要是负责控制UI界面的显示、更新和控件交互。

1.5 为什么在主线程更新UI,在子线程执行耗时操作?

在Android程序创建之初,一个Process呈现的是单线程模型,所有的任务都在一个线程中运行。因此,我们认为,UI Thread所执行的每一个函数,所花费的时间都应该是越短越好。

如果所有的工作都在UI线程,一些比较耗时的工作比如(访问网络,下载数据,查询数据库等),很容易造成主线程的阻塞,导致事件停止分发(包括绘制事件)。轻则降低用户体验,更坏的情况是,如果主线程被阻塞超过5秒,就会导致ANR,弹出应用程序没有响应,是等待还是关闭的警告。

另外,Andoid UI toolkit并不是线程安全的,所以不能从非UI线程来操纵UI组件。必须把所有的UI操作放在UI线程里。

1.6 为什么只能有一个线程操作 UI?

  • 两个线程不能同时draw,否则屏幕会花;
  • 不能同时insert map,否则内存会花;
  • 不能同时write buffer,否则文件会花。

需要互斥,比如锁。多线程操作一个UI,很容易导致,或者极其容易导致反向加锁和死锁问题。

结果就是同一时刻只有一个线程可以做ui。那么当两个线程互斥几率较大时,或者保证互斥的代码复杂时,选择其中一个长期持有其他发消息就是典型的解决方案。所以普遍的要求ui只能单线程。

  • ​​https://www.zhihu.com/question/37334646​​

2. 源码分析

如果在子线程更新 UI:

new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
main_tv.setText("子线程中访问");
}
}).start();

Crash msg:

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6581)
at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:924)

​ViewRootImpl​​​ 的 ​​checkThread​​方法:

void checkThread() {
// mThread是主线程,在应用程序启动的时候,就已经被初始化了
if (mThread != Thread.currentThread()) {
throw new CalledFromWrongThreadException(
"Only the original thread that created a view hierarchy can touch its views.");
}
}

​requestLayout​​ 方法:

@Override
public void requestLayout() {
if (!mHandlingLayoutInLayoutRequest) {
checkThread();
mLayoutRequested = true;
scheduleTraversals();
}
}

​scheduleTraversals()​​:

void scheduleTraversals() {
if (!mTraversalScheduled) {
mTraversalScheduled = true;
mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
mChoreographer.postCallback(
Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
if (!mUnbufferedInputDispatch) {
scheduleConsumeBatchedInput();
}
notifyRendererOfFramePending();
pokeDrawLockIfNeeded();
}
}

注意到postCallback方法的的第二个参数传入了很像是一个后台任务。那再点进去

final class TraversalRunnable implements Runnable {
@Override
public void run() {
doTraversal();
}
}

void doTraversal() {
if (mTraversalScheduled) {
mTraversalScheduled = false;
mHandler.getLooper().getQueue().removeSyncBarrier(mTraversalBarrier);

if (mProfile) {
Debug.startMethodTracing("ViewAncestor");
}

performTraversals();

if (mProfile) {
Debug.stopMethodTracing();
mProfile = false;
}
}
}

可以看到里面调用了一个 ​​performTraversals()​​​ 方法,​​View​​​ 的绘制过程就是从这个 ​​performTraversals​​ 方法开始的。分析到了这里,其实异常信息对我们帮助也不大了,它只告诉了我们子线程中访问UI在哪里抛出异常。

当访问UI时,​​ViewRootImpl​​​ 会调用 ​​checkThread​​​ 方法去检查当前访问UI的线程是哪个,如果不是UI线程则会抛出异常,这是没问题的。但是为什么一开始在 ​​MainActivity​​​ 的 ​​onCreate​​方法中创建一个子线程访问UI,程序还是正常能跑起来呢?

唯一的解释就是执行 ​​onCreate​​​ 方法的那个时候 ​​ViewRootImpl​​ 还没创建,无法去检查当前线程。

那么就可以这样深入进去。寻找 ​​ViewRootImpl​​ 是在哪里,是什么时候创建的。好,继续前进

在 ​​ActivityThread​​​ 中,我们找到 ​​handleResumeActivity​​ 方法,如下:大致流程是酱紫滴:

  • 第一步,查看:ActivityThread --> handleResumeActivity
  • handleResumeActivity 调用了 performResumeActivity
  • performResumeActivity 调用了 r.activity.performResume()
  • Instrumentation调用了callActivityOnResume方法
  • activity调用了makeVisible
  • 往WindowManager中添加DecorView
  • WindowManagerImpl的addView
  • WindowManagerGlobal的addView方法
  • ViewRootImpl --> root.setView(view, wparams, panelParentView);

简而言之

​ViewRootImpl​​​ 的创建在 ​​onResume​​ 方法回调之后


举报

相关推荐

0 条评论