Android消息機制原理——爲什麼不能在子線程更新UI?

序言

從Android開發的第一課開始,我們就有一個常識,即子線程不能更新UI,只能在轉到主線程去更新?所以我們在編碼時都遵照着這個原則,獲取到數據後通過handler去轉到主線程,通過Message拿到子線程發送過來的數據,具體可以看我的另一篇博客:

Android之Handler消息機制——深入理解 Looper、Handler、Message、MessageQueue

那麼問題來了,爲什麼我們不能在子線程去更新UI呢?我們先動手做一個實驗!

子線程更新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()

但是還是沒有解釋爲什麼一下能更新一下又不能更新,不要着急,我們跟着調用的流程繼續去解析源碼!

源碼解析

setText()

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()方法

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()方法

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?延時之後又不能了呢?

繼續上一個問題,爲什麼onCreate裏面可以更新UI?是因爲沒有調用checkThread()方法嗎?

這個時候,我們可以搬出Activity的生命週期來解釋一下了!

activity的聲明週期經過oncreate()->onstart()->onResume()->onRestart()->onPouse()->onStop()->onDestory()

  • onCreate:Activity已經創建了,View還沒有進行繪製
  • onStart:Activity處於可見狀態,可以做一些初始化的操作
  • onResume:View已經繪製完成,並且已經處於可見狀態

先強行解釋一波:會不會是因爲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還沒出生,當然也不會去檢查線程!

問題:如果不做這個校驗,是不是我也是可以正常在子線程更新UI呢?

按理來說,這樣是可以的!但是google爲什麼要這樣去設計呢?

(1)如果在不同的線程去控制用一個控件,由於網絡延時或者大量耗時操作,會使UI繪製錯亂,出了問題也很難去排查到底是哪個線程更新時出了問題;

(2)主線程負責更新,子線程負責耗時操作,能夠大大提高響應效率

(3)UI線程非安全線程,多線程進行併發訪問有可能會導致內存溢出,降低硬件使用壽命;且非線程安全不能加Lock線程鎖,否則會阻塞其他線程對View的訪問,效率就會變得低下!

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