非 UI 線程真的不能更新 UI 嗎?

這是我參與8月更文挑戰的第6天,活動詳情查看:8月更文挑戰android

首先先看一個例子:程序員

TextView tv;

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main3);

    tv = findViewById(R.id.tv);



    new Thread(new Runnable() {

        @Override

        public void run() {

            tv.setText("非UI線程更新TextView");

        }

    }).start();

}
複製代碼

例子很簡單,在就在Activity的onCreate()方法中開啓一個線程,在線程中更新TextView的內容。而後運行程序,「出乎意料」的事情發生了,程序正常運行。因此結論是:子線程中能夠更新UI。web

上面的結論是正確的,不用懷疑,可是答案並不重要。咱們在看一個例子:面試

TextView tv;

@Override

protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState);

    setContentView(R.layout.activity_main3);

    tv = findViewById(R.id.tv);



    new Thread(new Runnable() {

        @Override

        public void run() {

            try {

                Thread.sleep(2000);

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

            tv.setText("非UI線程更新TextView");

        }

    }).start();

}
複製代碼

這個例子和第一個例子基本相同,不一樣的是在run方法中加了Thread.sleep(2000),讓線程先睡2秒。程序剛啓後並無報錯,2秒後程序崩了,報錯的信息很熟悉:

因此子線程中又不能更新UI了。
那麼若是換成Thread.sleep(100)呢?換成Thread.sleep(10)呢?markdown

咱們都知道上述的報錯信息,必定是由於這句話:app

tv.setText("非UI線程更新TextView")
複製代碼

所以須要查看setText()都作了哪些事情。ide

@android.view.RemotableViewMethod

public final void setText(CharSequence text) {

    setText(text, mBufferType);

}
複製代碼

繼續查看setText(text, mBufferType):佈局

public void setText(CharSequence text, BufferType type) {

    setText(text, type, true, 0);



    if (mCharWrapper != null) {

        mCharWrapper.mChars = null;

    }

}
複製代碼

依舊沒有有價值的信息,繼續查看setText(text, type, true, 0):post

private void setText(CharSequence text, BufferType type,

                         boolean notifyBefore, int oldlen) {

        mTextFromResource = false;

        if (text == null) {

            text = "";

        }

        if (!isSuggestionsEnabled()) {

            text = removeSuggestionSpans(text);

        }



        if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);



        if (text instanceof Spanned

                && ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {

            if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {

                setHorizontalFadingEdgeEnabled(true);

                mMarqueeFadeMode = MARQUEE_FADE_NORMAL;

            } 

         ……

         ……

         ……

      if (mLayout != null) {

            checkForRelayout();

        }



        sendOnTextChanged(text, 0, oldlen, textLength);

        onTextChanged(text, 0, oldlen, textLength);

        ……

        ……

        ……
複製代碼

在第23行咱們發現了checkForRelayout()這個方法,這是咱們所要找的:學習

private void checkForRelayout() {



        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT

                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))

                && (mHint == null || mHintLayout != null)

                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {



            int oldht = mLayout.getHeight();

            int want = mLayout.getWidth();

            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,

                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),

                          false);



            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {



                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT

                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {

                    autoSizeText();

                    invalidate();

                    return;

                }

                if (mLayout.getHeight() == oldht

                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {

                    autoSizeText();

                    invalidate();

                    return;

                }

            }

            requestLayout();

            invalidate();

        } else {

            nullLayouts();

            requestLayout();

            invalidate();

        }

    }
複製代碼

checkForRelayout()中代碼不算太多,就一併複製到了這裏。咱們能夠看到無論在30行仍是34行,都會調用 requestLayout(), invalidate()兩個方法,到這裏咱們應該多去再思考一個問題,畢竟這兩個方法對於咱們並不陌生,而且還時常一塊兒出現。

requestLayout(), invalidate()的做用分別是什麼以及兩個的區別

不過對於本文來講,咱們須要關注的是 invalidate():

public void invalidate() {

    invalidate(true);

}
複製代碼

繼續查看 invalidate(true):

public void invalidate(boolean invalidateCache) {

    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);

}
複製代碼

繼續查看invalidateInternal():

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,

            boolean fullInvalidate) {

        if (mGhostView != null) {

            mGhostView.invalidate(true);

            return;

        }



        if (skipInvalidate()) {

            return;

        }



        if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)

                || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)

                || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED

                || (fullInvalidate && isOpaque() != mLastIsOpaque)) {

            if (fullInvalidate) {

                mLastIsOpaque = isOpaque();

                mPrivateFlags &= ~PFLAG_DRAWN;

            }



            mPrivateFlags |= PFLAG_DIRTY;



            if (invalidateCache) {

                mPrivateFlags |= PFLAG_INVALIDATED;

                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;

            }

            final AttachInfo ai = mAttachInfo;

            final ViewParent p = mParent;

            if (p != null && ai != null && l < r && t < b) {

                final Rect damage = ai.mTmpInvalRect;

                damage.set(l, t, r, b);

                p.invalidateChild(this, damage);

            }





            if (mBackground != null && mBackground.isProjected()) {

                final View receiver = getProjectionReceiver();

                if (receiver != null) {

                    receiver.damageInParent();

                }

            }

        }

    }
複製代碼

這裏咱們須要關注的是28-33行,重點關注28行的ViewParent p,而後32行調用了p.invalidateChild(this,damage),這裏的ViewParent就是TextView的父佈局,這裏假設其父佈局是LinearLayout,而後咱們再查看下父佈局中invalidateChild方法作了什麼事情:

@Deprecated

    @Override

    public final void invalidateChild(View child, final Rect dirty) {

        final AttachInfo attachInfo = mAttachInfo;

        if (attachInfo != null && attachInfo.mHardwareAccelerated) {



            onDescendantInvalidated(child, child);

            return;

        }



        ViewParent parent = this;

        if (attachInfo != null) {



            final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;

            Matrix childMatrix = child.getMatrix();

            final boolean isOpaque = child.isOpaque() && !drawAnimation &&

                    child.getAnimation() == null && childMatrix.isIdentity();



            int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;



            if (child.mLayerType != LAYER_TYPE_NONE) {

                mPrivateFlags |= PFLAG_INVALIDATED;

                mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;

            }



            final int[] location = attachInfo.mInvalidateChildLocation;

            location[CHILD_LEFT_INDEX] = child.mLeft;

            location[CHILD_TOP_INDEX] = child.mTop;

            if (!childMatrix.isIdentity() ||

                    (mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {

                RectF boundingRect = attachInfo.mTmpTransformRect;

                boundingRect.set(dirty);

                Matrix transformMatrix;

                if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {

                    Transformation t = attachInfo.mTmpTransformation;

                    boolean transformed = getChildStaticTransformation(child, t);

                    if (transformed) {

                        transformMatrix = attachInfo.mTmpMatrix;

                        transformMatrix.set(t.getMatrix());

                        if (!childMatrix.isIdentity()) {

                            transformMatrix.preConcat(childMatrix);

                        }

                    } else {

                        transformMatrix = childMatrix;

                    }

                } else {

                    transformMatrix = childMatrix;

                }

                transformMatrix.mapRect(boundingRect);

                dirty.set((int) Math.floor(boundingRect.left),

                        (int) Math.floor(boundingRect.top),

                        (int) Math.ceil(boundingRect.right),

                        (int) Math.ceil(boundingRect.bottom));

            }



            do {

                View view = null;

                if (parent instanceof View) {

                    view = (View) parent;

                }



                if (drawAnimation) {

                    if (view != null) {

                        view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;

                    } else if (parent instanceof ViewRootImpl) {

                        ((ViewRootImpl) parent).mIsAnimating = true;

                    }

                }

                if (view != null) {

                    if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&

                            view.getSolidColor() == 0) {

                        opaqueFlag = PFLAG_DIRTY;

                    }

                    if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {

                        view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;

                    }

                }



                parent = parent.invalidateChildInParent(location, dirty);

                if (view != null) {



                    Matrix m = view.getMatrix();

                    if (!m.isIdentity()) {

                        RectF boundingRect = attachInfo.mTmpTransformRect;

                        boundingRect.set(dirty);

                        m.mapRect(boundingRect);

                        dirty.set((int) Math.floor(boundingRect.left),

                                (int) Math.floor(boundingRect.top),

                                (int) Math.ceil(boundingRect.right),

                                (int) Math.ceil(boundingRect.bottom));

                    }

                }

            } while (parent != null);

        }

    }
複製代碼

其實父佈局中invalidateChild()方法定義在ViewGroup中,這裏咱們重點看56行到93行,也就是do{ } while{ }中,在79行不停地調用parent = parent.invalidateChildInParent(location, dirty),對於invalidateChildInParent方法其實這裏不用太關注,只須要知道它不停地返回其父佈局就能夠了,最終會返回根佈局ViewRootImpl,而後調用ViewRootImpl中invalidateChildInParent()方法:

@Override

public ViewParent invalidateChildInParent(int[] location, Rect dirty) {

    checkThread();

    if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);

    if (dirty == null) {

        invalidate();

        return null;

    } else if (dirty.isEmpty() && !mIsAnimating) {

        return null;

    }

    if (mCurScrollY != 0 || mTranslator != null) {

        mTempRect.set(dirty);

        dirty = mTempRect;

        if (mCurScrollY != 0) {

            dirty.offset(0, -mCurScrollY);

        }

        if (mTranslator != null) {

            mTranslator.translateRectInAppWindowToScreen(dirty);

        }

        if (mAttachInfo.mScalingRequired) {

            dirty.inset(-1, -1);

        }

    }

    invalidateRectOnScreen(dirty);

    return null;

}
複製代碼

第三行看到方法 checkThread():

void checkThread() {

    if (mThread != Thread.currentThread()) {

        throw new CalledFromWrongThreadException(

                "Only the original thread that created a view hierarchy can touch its views.");

    }

}
複製代碼

這個方法很簡單,可是也很是重要,它就是在判斷當前線程是不是主線程,若是不是拋出異常,也就是開篇第二個例子中拋出的異常信息。

以上:咱們得知,當執行TextView.setText()方法時,首先會執行 invalidate()方法,進而會獲得ViewRootImpl,而後會執行到checkThread()方法來判斷當前線程是不是主線程。

經過以上分析咱們能夠得出兩個結論:

  1. Android系統經過checkThread()方法來阻止開發者在子線程中更新UI
  2. checkThread()方法定義在ViewRootImpl中

到這裏咱們就能夠解釋開篇第一個例子中爲何在子線程中也能夠更新UI,是由於在Activity的onCreate()方法中ViewRootImpl對象尚未建立,那麼也就不可能執行checkThread()方法,所以但是達到在子線程中更新UI的目的。在第二個例子中增長了Thread.sleep(2000),當2秒過去時ViewRootImpl對象已經建立完畢了,所以也就不能在子線程中更新UI了。

最後我再給出第三個例子:

@Override

protected void onResume() {

    super.onResume();

    new Thread(new Runnable() {

        @Override

        public void run() {

            tv.setText("非UI線程更新TextView2");

        }

    }).start();

}
複製代碼

問:

  1. 第三個例子,將onCreate()改成onResume(),可否實如今子線程中更新UI?
  2. ViewRootImpl對象到底在何時建立的?
  3. requestLayout(), invalidate()的做用分別是什麼以及兩個的區別

公衆號:程序員喵大人(專一於Android各種學習筆記、面試題以及IT類資訊的分享。)

相關文章
相關標籤/搜索