高频刷新 TextView 内容,如何规避性能问题?

161 阅读2分钟

在实际开发中,像计时器、状态监控面板等场景会频繁地调用 TextView.setText() 来刷新 UI,但高频刷新 TextView 其实是非常容易造成性能瓶颈。
本篇文章将从 Android 源码角度剖析 TextView.setText() 背后的行为逻辑,揭示它可能引发的 requestLayout() 和 invalidate() 操作。

setText() 为什么可能导致卡顿?

setText() 只是简单地换个文字,为什么会影响性能?关键点在于:

  • setText() 可能会触发 requestLayout(),导致整个 View 或 ViewGroup 重新测量和布局,频繁触发就会造成卡顿。

源码分析,TextView.setText() 背后的布局判断

private void setText(CharSequence text, BufferType type,
                     boolean notifyBefore, int oldlen) {
                     ...
                     if (mLayout != null) {
                        checkForRelayout();
                     }
                     ...
}
private void checkForRelayout() {
    // 如果我们有一个固定宽度,并且文本高度保持不变,或者 View 高度是固定的,
    // 就可以尝试直接更新文本布局而不重新测量整个 View。

    // 当mLayoutParams.width不是WRAP_CONTENT或者宽度固定
    if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
            || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
            && (mHint == null || mHintLayout != null)
            && (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);

        if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
            // 固定高度,仅刷新界面,不重新布局.
            if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
                    && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
                autoSizeText();
                invalidate();
                return;
            }

            // 高度是 wrap_content,但文本高度没变,也不需要 relayout
            if (mLayout.getHeight() == oldht
                    && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
                autoSizeText();
                invalidate();
                return;
            }
        }
        
        // 否则,文本高度发生了变化,且高度是 wrap_content,必须重新布局
        requestLayout();
        invalidate();
    } else {
        //宽度是动态的(wrap_content 或者 max ≠ min),必须重新生成布局并 requestLayout
        nullLayouts();
        requestLayout();
        invalidate();
    }
}

总结:什么情况下不会触发 requestLayout()
条件一:宽度是固定的
条件二:当前可用宽度大于 0
条件三:高度是固定的或者高度是 wrap_content,但文本内容高度没变
另外还有一个隐藏条件:如果是 跑马灯模式(MARQUEE),无论高度是否变化,都会执行 requestLayout(),因为需要特殊布局支持

布局建议

  1. 宽高尽量使用固定值或 match_parent
  2. 避免使用 ellipsize="marquee",除非确有需求