1. 理论上的原因
1.1 Android主线程是线程不安全的?
网上文章常常有说:Android主线程是线程不安全的。我就纳闷了,线程还有安全一说?
不能说主线程是线程不安全。线程没有安全不安全这一说。而是更新UI的方法不是线程安全的,即只能在单线程中完成UI的更新,不能使用多线程。(为什么呢?因为子线程可能会有多个,存在多个线程同时操作一个控件的情况)因此,只能在主线程中进行UI更新。
1.2 Android的单线程模型
Android的单线程模型有两条原则:
- 不要阻塞UI线程。
- 不要在UI线程之外访问Android UI toolkit(主要是这两个包中的组件:
android.widget
andandroid.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
方法,如下:
- https://blog.csdn.net/xyh269/article/details/52728861
大致流程是酱紫滴:
- 第一步,查看: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
方法回调之后