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
方法回調之後