爲什麼子線程都不能刷新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)

ViewRootImplcheckThread方法:

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線程則會拋出異常,這是沒問題的。但是爲什麼一開始在 MainActivityonCreate方法中創建一個子線程訪問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 方法回調之後

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章