Android子線程是否能更新UI?

湖人總冠軍

還是先來(lái)一張圖片,慶祝一下19-20賽季的湖人總冠軍吧??!

Android中的子線程能否操作UI

現(xiàn)在進(jìn)入正題,想想從開(kāi)始工作到現(xiàn)在,被無(wú)數(shù)次的告誡子線程不能更新UI,UI操作必須在主線程中完成。然而作為辣雞代碼搬運(yùn)工的我,怎么能輕易就聽(tīng)你們的呢。

  public class MainActivity extends AppCompatActivity {
    private TextView mTvTest;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mTvTest = findViewById(R.id.tv_test);
        new Thread(new Runnable() {
            @Override
            public void run() {
                mTvTest.setText("子線程修改UI成功");
            }
        }).start();
    }
}

一番輸出,run起來(lái),完全沒(méi)有問(wèn)題啊,Activity上的TextView上面的的文字不也就更新了啊。

但是,在我們的實(shí)際開(kāi)發(fā)中,當(dāng)我們開(kāi)啟一個(gè)新的線程耗時(shí)操作之后,直接更新UI,就會(huì)出現(xiàn)問(wèn)題。

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        tv = findViewById(R.id.tv);
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                tv.setText("子線程修改UI成功");
            }
        }).start();
    }

瞬間就崩潰了

Process: cn.qiaowa.testapplication, PID: 21102
android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.
        at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:9219)
        at android.view.ViewRootImpl.requestLayout(ViewRootImpl.java:1600)

拋出了一個(gè)Only the original thread that created a view hierarchy can touch its views.的異常,告訴我們只能在主線程中操作UI。

從這兩段代碼,我們可以清楚的得出一個(gè)結(jié)論Android中子線程是可以操作UI的,但是為什么第二段代碼中,我們子線程中操作UI的時(shí)候,有會(huì)拋出異常Only the original thread that created a view hierarchy can touch its views.我們接下來(lái)繼續(xù)研究一下。

異常為何產(chǎn)生

android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

根據(jù)Log日志,我們可以知道這個(gè)異常是在 android.view.ViewRootImpl這個(gè)類(lèi)中拋出來(lái)的。定位一下源碼

    void checkThread() {
        if (mThread != Thread.currentThread()) {
            throw new CalledFromWrongThreadException(
                    "Only the original thread that created a view hierarchy can touch its views.");
        }
    }

繼續(xù)追蹤一下ViewRootImpl#checkThread()這個(gè)方法的調(diào)用地方

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

看到這里requestLayout () 方法,就是View里面請(qǐng)求重新測(cè)量布局的方法。

再看看ViewPootImpl的類(lèi)繼承關(guān)系

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, ThreadedRenderer.DrawCallbacks {
}

ViewPootImpl實(shí)現(xiàn)了一個(gè)ViewParent接口,而requestLayout ()方法就是繼承于這個(gè)地方。

public interface ViewParent {

    // 去除了其他代碼
    /**
     * Called when something has changed which has invalidated the layout of a
     * child of this view parent. This will schedule a layout pass of the view
     * tree.
     */
    public void requestLayout();
}

到這里我們已經(jīng)研究出這個(gè)異常的大致流程了,但是這個(gè)異常是從那個(gè)地方出發(fā)的呢?換句話說(shuō)就requestLayout()這個(gè)方法是怎么調(diào)用的呢?我們探究。

requestLayout()這個(gè)方法是View里面請(qǐng)求重新測(cè)量布局的。那么我們就回到TextView.setText(...)中看看。

private void setText(CharSequence text, BufferType type,
                         boolean notifyBefore, int oldlen) {
        //代碼省略

        if (mLayout != null) {
            //關(guān)鍵代碼所在
            checkForRelayout();
        }

        sendOnTextChanged(text, 0, oldlen, textLength);
        onTextChanged(text, 0, oldlen, textLength);

        notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);

        if (needEditableForNotification) {
            sendAfterTextChanged((Editable) text);
        } else {
            notifyAutoFillManagerAfterTextChangedIfNeeded();
        }

        // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
        if (mEditor != null) mEditor.prepareCursorControllers();
    }

再看看checkForRelayout ()

   /**
     * Check whether entirely new text requires a new view layout
     * or merely a new text layout.
     */
    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
                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
                && (mHint == null || mHintLayout != null)
                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
           //代碼省略

            // 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();
        }
    }

這里不管是走if還是else,都會(huì)走TextView#requestLayout()方法,那我們?cè)倏纯催@個(gè)TextView#requestLayout()

 public void requestLayout() {
        //代碼省略

        if (mParent != null && !mParent.isLayoutRequested()) {
            mParent.requestLayout();
        }
}

最終它會(huì)調(diào)用mParent.requestLayout();這個(gè)mParent是一個(gè)ViewParent接口類(lèi),而在開(kāi)始我們分析ViewRootImpl時(shí)候我們就知道了它就實(shí)現(xiàn)了ViewParent接口,所以通過(guò)TextView.setText(...)最終就會(huì)調(diào)用到ViewRootImpl #RequestLayout方法。

再回到setText()的源碼中的

if (mLayout != null) {
    checkForRelayout();
}

如果mLayout不為空的時(shí)候才會(huì)進(jìn)行線程的檢測(cè),這個(gè)時(shí)候如果再子線程操作UI的時(shí)候必然會(huì)拋出異常。想一下,如果這個(gè)mLayout位空的話,不就可以繞開(kāi)線程的校驗(yàn)了么(第一個(gè)事例子線程更新UI的事例就是這種情況),就不會(huì)拋出異常了。

走到這里我們可以得到一個(gè)更加準(zhǔn)確的結(jié)論:Android中子線程是可以更新UI的,當(dāng)mLayout還沒(méi)有初始化完成的時(shí)候,子線程更新UI不會(huì)拋出異常。而在mLayout初始化完成之后,更新UI就會(huì)進(jìn)行線程校驗(yàn),如果是在當(dāng)前線程是子線程就會(huì)拋出異常,告訴我們只能在主線程中更新UI

由此可見(jiàn),子線程操作UI的時(shí)候,是否拋出異常的關(guān)鍵就是這個(gè)mLayout的初始化情況了,那我們繼續(xù)研究一下mLayout的初始化情況。

mLayout 的初始化

從開(kāi)始的兩個(gè)事例中發(fā)現(xiàn)唯一不同的是,第二段代碼讓子線程sleep了3秒后才去更新UI的,結(jié)果就拋出了異常,也就是mLayout已經(jīng)初始化完成了??梢灾肋@個(gè)肯定是個(gè)生命周期有一定的關(guān)系。那么先看看ActivityThread這個(gè)類(lèi)。

    @Override
    public void handleResumeActivity(IBinder token, boolean finalStateRequest, boolean isForward,
            String reason) {
        //代碼省略

        // TODO Push resumeArgs into the activity for consideration
        final ActivityClientRecord r = performResumeActivity(token, finalStateRequest, reason);
      //代碼省略
        if (r.window == null && !a.mFinished && willBeVisible) {
            r.window = r.activity.getWindow();
            View decor = r.window.getDecorView();
            decor.setVisibility(View.INVISIBLE);
            ViewManager wm = a.getWindowManager();
            WindowManager.LayoutParams l = r.window.getAttributes();
            a.mDecor = decor;
            l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
            l.softInputMode |= forwardBit;
            if (r.mPreserveWindow) {
                a.mWindowAdded = true;
                r.mPreserveWindow = false;
                // Normally the ViewRoot sets up callbacks with the Activity
                // in addView->ViewRootImpl#setView. If we are instead reusing
                // the decor view we have to notify the view root that the
                // callbacks may have changed.
                ViewRootImpl impl = decor.getViewRootImpl();
                if (impl != null) {
                    impl.notifyChildRebuilt();
                }
            }
            if (a.mVisibleFromClient) {
                if (!a.mWindowAdded) {
                    a.mWindowAdded = true;
                    wm.addView(decor, l);
                } else {
                    // The activity will get a callback for this {@link LayoutParams} change
                    // earlier. However, at that time the decor will not be set (this is set
                    // in this method), so no action will be taken. This call ensures the
                    // callback occurs with the decor set.
                    a.onWindowAttributesChanged(l);
                }
            }

            // If the window has already been added, but during resume
            // we started another activity, then don't yet make the
            // window visible.
        } else if (!willBeVisible) {
            if (localLOGV) Slog.v(TAG, "Launch " + r + " mStartedActivity set");
            r.hideForNow = true;
        }
    //代碼省略
    }

關(guān)鍵代碼wm.addView(decor, l),這個(gè)就是將 decorView 添加到 window中。我們?cè)诳匆幌?WindowManager的實(shí)現(xiàn)類(lèi) WindowManagerImpl


    @Override
    public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
        applyDefaultToken(params);
        mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);
    }

發(fā)現(xiàn)此時(shí),addView操作交給了mGlobal這個(gè)代理類(lèi)來(lái)處理,而這個(gè)代理類(lèi)正是WindowManagerGlobal,那我們繼續(xù)看看

    public void addView(View view, ViewGroup.LayoutParams params,
                        Display display, Window parentWindow) {

        ViewRootImpl root;
        ...

        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;
        }
    }

初始化一個(gè)ViewRootImpl類(lèi),然后調(diào)用setView方法,

view.assignParent(this);

在setView放里面調(diào)用了assignParent方法,將ViewRootImpl傳入到View當(dāng)中。

由此,可以看出在activity 的onResume生命周期中進(jìn)行了 mLayout的初始化,從初始化完成之后,操作UI就要進(jìn)行線程的檢測(cè)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容