Review the past and learn the new: Explore the Android UI drawing and refreshing process

1. Description:

  1. Based on previous understanding, we know that the drawing of ui will eventually go to scheduleTraversals in Android’s ViewRootImpl to send and receive vsync signals for drawing, in ViewRootImpl The main thread will also be detected, which is what we call an exception when the sub-thread updates the UI.

  2. Like our commonly used refresh ui, invalidate, and requestLayout methods, (according to my previous understanding, after ViewRootImpl is initialized and added, in the child thread Refreshing the UI will definitely crash: as shown below)

2. Question: invalidate will definitely cause an abnormal crash?

2.1. Example: Sub-thread updates TextView text (note here is TextView, why is it instead of ImageView, because my background is the TextView I use, and when using it I found invalidate, and requestLayout method difference)

One day I used a sub-thread in onResume to update a piece of code in TextView, and found that no exception crash was thrown. The code is as follows:

 override fun onResume() {
        super.onResume()
        mBind.btTest.setOnClickListener{
            lifecycleScope.launch(Dispatchers.IO) {
                mBind.btTest.text = "Sub-thread click to change: ${Thread.currentThread().name}"
            }
        }
    }

I wonder why? , look at the code: Step by step debug: TextView control:

1.、
public final void setText(CharSequence text) {
    setText(text, mBufferType);
}
2.,
public void setText(CharSequence text, BufferType type) {
    setText(text, type, true, 0);

    if (mCharWrapper != null) {
        mCharWrapper.mChars = null;
    }
}
3.,
private void setText(CharSequence text, BufferType type,
                     boolean notifyBefore, int oldlen) {
     ...omitted
    if (mLayout != null) {
        checkForRelayout();
    }
    ...omitted
    
}

The above mainly depends on the checkForRelayout() in the third step to detect whether redrawing is required. The method is as follows

@UnsupportedAppUsage
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 & amp; & amp; mMaxWidth == mMinWidth))
             & amp; & amp; (mHint == null || mHintLayout != null)
             & amp; & amp; (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
        // Static width, so try making a new text layout.

        int oldht = mLayout.getHeight();
        int want = mLayout.getWidth();
        int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();

        /*
         * No need to bring the text into view, since the size is not
         * changing (unless we do the requestLayout(), in which case it
         * will happen at measure).
         */
        makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
                      mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
                      false);
        
        //1. Detect the display type of text, which is our too long ellipses.
        if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
            // In a fixed-height view, so use our new text layout.
            if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                     & amp; & amp; mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                autoSizeText();
                invalidate();
                return;
            }

            // Dynamic height, but height has stayed the same,
            // so use our new text layout.
            if (mLayout.getHeight() == oldht
                     & amp; & amp; (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                autoSizeText();
                invalidate();
                return;
            }
        }

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

From the if (mEllipsize != TextUtils.TruncateAt.MARQUEE) condition in the above checkForRelayout() method, we know that it is true because we have not set mEllipsize = marquee Effect, so the invalidate() method is used and then directly return truncates, and the requestLayout() method behind is not used. As for The difference between requestLayout() and invalidate() I won’t go into it

2.2. Analysis of requestLayout method

Based on previous knowledge, I know that calling the requestLayout() method will crash. As for why calling the requestLayout() method will crash?

Let’s look at the requestLayout() method first, and pause for a while to follow up with invalidate():

requestLayout() methodThe code is as follows:

public void requestLayout() {
    if (mMeasureCache != null) mMeasureCache.clear();

    if (mAttachInfo != null & amp; & amp; mAttachInfo.mViewRequestingLayout == null) {
        // Only trigger request-during-layout logic if this is the view requesting it,
        // not the views in its parent hierarchy
        ViewRootImpl viewRoot = getViewRootImpl();
        if (viewRoot != null & amp; & amp; viewRoot.isInLayout()) {
            if (!viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        mAttachInfo.mViewRequestingLayout = this;
    }

    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;

    if (mParent != null & amp; & amp; !mParent.isLayoutRequested()) {
        mParent.requestLayout();
    }
    if (mAttachInfo != null & amp; & amp; mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

The requestLayout() method will recursively call the mParent.requestLayout() method until the requestLayout() in ViewRootImpl is found. code> method, and its method does thread detection as shown below: This is why the requestLayout() method will crash.

Verify the conjecture: Set the marquee attribute for TextView so that the above if (mEllipsize != TextUtils.TruncateAt.MARQUEE) does not hold, and do the following requestLayout() method, the code is as follows:

 override fun onResume() {
        super.onResume()
        mBind.btTest.ellipsize = TextUtils.TruncateAt.valueOf("MARQUEE")
        mBind.btTest.setOnClickListener{
            lifecycleScope.launch(Dispatchers.IO) {
                mBind.btTest.text = "Sub-thread click to change: ${Thread.currentThread().name}"
            }
        }
    }

Sure enough, it crashes after clicking:

2.3. Continue to analyze the invalidate() method, why it does not cause the textview update to crash

Look at the code in the View.java file

public void invalidate() {
    invalidate(true);
}
public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}
void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
    if (mGhostView != null) {
        mGhostView.invalidate(true);
        return;
    }

    if (skipInvalidate()) {
        return;
    }

    //Reset content capture caches
    mPrivateFlags4 & amp;= ~PFLAG4_CONTENT_CAPTURE_IMPORTANCE_MASK;
    mContentCaptureSessionCached = false;

    if ((mPrivateFlags & amp; (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
            || (invalidateCache & amp; & amp; (mPrivateFlags & amp; PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
            || (mPrivateFlags & amp; PFLAG_INVALIDATED) != PFLAG_INVALIDATED
            || (fullInvalidate & amp; & amp; isOpaque() != mLastIsOpaque)) {
        if (fullInvalidate) {
            mLastIsOpaque = isOpaque();
            mPrivateFlags & amp;= ~PFLAG_DRAWN;
        }

        mPrivateFlags |= PFLAG_DIRTY;

        if (invalidateCache) {
            mPrivateFlags |= PFLAG_INVALIDATED;
            mPrivateFlags & amp;= ~PFLAG_DRAWING_CACHE_VALID;
        }

        // Propagate the damage rectangle to the parent view.
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null & amp; & amp; ai != null & amp; & amp; l < r & amp; & amp; t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            p.invalidateChild(this, damage);
        }

        // Damage the entire projection receiver, if necessary.
        if (mBackground != null & amp; & amp; mBackground.isProjected()) {
            final View receiver = getProjectionReceiver();
            if (receiver != null) {
                receiver.damageInParent();
            }
        }
    }
}

The core code is the invalidateChild method in the invalidateInternal method in the third paragraph above

It calls back to the invalidateChild method in ViewGroup

Look at: invalidateChild as shown below: We know

if (attachInfo != null & amp; & amp; attachInfo.mHardwareAccelerated) The condition is true if attachInfo is not empty and hardware acceleration is enabled (from API 14 (3.0) onwards. Hardware acceleration is enabled by default). attachInfo is a series of information assigned when a view is attached to its parent window.

Therefore, the onDescendantInvalidated method used after the condition is established is as follows:

@CallSuper
public void onDescendantInvalidated(@NonNull View child, @NonNull View target) {
    /*
     * HW-only, Rect-ignoring damage codepath
     *
     * We don't deal with rectangles here, since RenderThread native code computes damage for
     * everything drawn by HWUI (and SW layer / drawing cache doesn't keep track of damage area)
     */

    // if set, combine the animation flag into the parent
    mPrivateFlags |= (target.mPrivateFlags & amp; PFLAG_DRAW_ANIMATION);

    if ((target.mPrivateFlags & amp; ~PFLAG_DIRTY_MASK) != 0) {
        // We lazily use PFLAG_DIRTY, since computing opaque isn't worth the potential
        // optimization in provides in a DisplayList world.
        mPrivateFlags = (mPrivateFlags & amp; ~PFLAG_DIRTY_MASK) | PFLAG_DIRTY;

        // simplified invalidateChildInParent behavior: clear cache validity to be safe...
        mPrivateFlags & amp;= ~PFLAG_DRAWING_CACHE_VALID;
    }

    // ... and mark inval if in software layer that needs to repaint (hw handled in native)
    if (mLayerType == LAYER_TYPE_SOFTWARE) {
        // Layered parents should be invalidated. Escalate to a full invalidate (and note that
        // we do this after consuming any relevant flags from the originating descendant)
        mPrivateFlags |= PFLAG_INVALIDATED | PFLAG_DIRTY;
        target = this;
    }

    if (mParent != null) {
        mParent.onDescendantInvalidated(this, target);
    }
}

The core of the above code is mParent.onDescendantInvalidated(this, target);, which is similar to requestLayout(). The method onDescendantInvalidated will call cyclically and recursively. mParent.onDescendantInvalidated(this, target); method until you find the onDescendantInvalidated(this, target) method in ViewRootImpl, but its method does not perform thread detection As shown below: This is why the invalidate method will not crash after hardware acceleration is turned on. As shown below: Go directly to scheduleTraversals drawing and refreshing. If you are interested, you can take a look:

What happens after hardware acceleration is turned off? Continue to look at the invalidateChild method

@Deprecated
@Override
public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null & amp; & amp; attachInfo.mHardwareAccelerated) {
        // HW accelerated fast path
        onDescendantInvalidated(child, child);
        return;
    }

    ViewParent parent = this;
    if (attachInfo != null) {
     
       ...

        do {
             ....
            parent = parent.invalidateChildInParent(location, dirty);
               
            ....
        } while (parent != null);
    }
}

The core of the above paragraph is parent = parent.invalidateChildInParent(location, dirty); method is the same as the while loop and keeps calling the invalidateChildInParent method until it finds ViewRootImpl invalidateChildInParent(int[] location, Rect dirty) method, as shown below, thread detection is performed internally

**Verify the guess and turn off hardware acceleration: android:hardwareAccelerated="false"** Sure enough, it crashed.

3. Summary

This is the problem I encountered: based solely on the conclusion that TextView can be updated in sub-threads, in general, if you want to avoid crashing, you have to bypass checkThreadViewRootImpl code> detection. As for the use of studying it, only by knowing the process of understanding the source code can we write better things.

Android learning notes

Android performance optimization: https://qr18.cn/FVlo89
The underlying principles of Android Framework: https://qr18.cn/AQpN4J
Android car version: https://qr18.cn/F05ZCM
Android reverse security study notes: https://qr18.cn/CQ5TcL
Android audio and video: https://qr18.cn/Ei3VPD
Jetpack family bucket article (including Compose): https://qr18.cn/A0gajp
OkHttp source code analysis notes: https://qr18.cn/Cw0pBD
Kotlin article: https://qr18.cn/CdjtAF
Gradle article: https://qr18.cn/DzrmMB
Flutter article: https://qr18.cn/DIvKma
Eight major bodies of Android knowledge: https://qr18.cn/CyxarU
Android core notes: https://qr21.cn/CaZQLo
Android interview questions from previous years: https://qr18.cn/CKV8OZ
The latest Android interview question set in 2023: https://qr18.cn/CgxrRy
Interview questions for Android vehicle development positions: https://qr18.cn/FTlyCJ
Audio and video interview questions: https://qr18.cn/AcV6Ap

syntaxbug.com © 2021 All Rights Reserved.