TextView效能瓶頸,渲染優化,以及StaticLayout的一些用處
TextView高頻度繪圖下的問題
在一些場景下。比如介面上有大量的聊天並且活躍度高,內容包含了文字,emoji,圖片等各種資訊的複雜文字,採用TextView來展示這些內容資訊。就容易觀察到,聊天訊息在頻繁重新整理的時候,效能有明顯下降,GPU火焰圖抖動也更加頻繁。
效能瓶頸在那裡?
- 1 TextView有1w行程式碼,除了裡面包含的View的屬性和方法外,還包含如下內容,Spans,MovementMethod,InputConnection, TransformationMethod, Layout, Editor(你以為這應該寫在EditText裡面,google說我就不)。
- 2 純看程式碼還是很難看出來問題。找到問題的方法有2種。
google一下,在前人的基礎上找到途徑,參見連結:請自備梯子。另外,在你的聊天頁裡面出現textview處理的地方分析trace的時間,也能直接看到TextView的setText方法耗時很長,是一個明顯的瓶頸。
TextView的setText方法分析
- 1 原始碼如下:
public void setText(CharSequence text, BufferType type) {
setText(text, type, true, 0);
if (mCharWrapper != null) {
mCharWrapper.mChars = null;
}
}
private void setText(CharSequence text, BufferType type,
boolean notifyBefore, int oldlen) {
if (text == null) {
text = "";
}
// If suggestions are not enabled, remove the suggestion spans from the 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;
} else {
setHorizontalFadingEdgeEnabled(false);
mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
}
setEllipsize(TextUtils.TruncateAt.MARQUEE);
}
int n = mFilters.length;
for (int i = 0; i < n; i++) {
CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
if (out != null) {
text = out;
}
}
if (notifyBefore) {
if (mText != null) {
oldlen = mText.length();
sendBeforeTextChanged(mText, 0, oldlen, text.length());
} else {
sendBeforeTextChanged("", 0, 0, text.length());
}
}
boolean needEditableForNotification = false;
if (mListeners != null && mListeners.size() != 0) {
needEditableForNotification = true;
}
if (type == BufferType.EDITABLE || getKeyListener() != null ||
needEditableForNotification) {
createEditorIfNeeded();
mEditor.forgetUndoRedo();
Editable t = mEditableFactory.newEditable(text);
text = t;
setFilters(t, mFilters);
InputMethodManager imm = InputMethodManager.peekInstance();
if (imm != null) imm.restartInput(this);
} else if (type == BufferType.SPANNABLE || mMovement != null) {
text = mSpannableFactory.newSpannable(text);
} else if (!(text instanceof CharWrapper)) {
text = TextUtils.stringOrSpannedString(text);
}
if (mAutoLinkMask != 0) {
Spannable s2;
if (type == BufferType.EDITABLE || text instanceof Spannable) {
s2 = (Spannable) text;
} else {
s2 = mSpannableFactory.newSpannable(text);
}
if (Linkify.addLinks(s2, mAutoLinkMask)) {
text = s2;
type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
/*
* We must go ahead and set the text before changing the
* movement method, because setMovementMethod() may call
* setText() again to try to upgrade the buffer type.
*/
mText = text;
// Do not change the movement method for text that support text selection as it
// would prevent an arbitrary cursor displacement.
if (mLinksClickable && !textCanBeSelected()) {
setMovementMethod(LinkMovementMethod.getInstance());
}
}
}
mBufferType = type;
mText = text;
if (mTransformation == null) {
mTransformed = text;
} else {
mTransformed = mTransformation.getTransformation(text, this);
}
final int textLength = text.length();
if (text instanceof Spannable && !mAllowTransformationLengthChange) {
Spannable sp = (Spannable) text;
// Remove any ChangeWatchers that might have come from other TextViews.
final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
final int count = watchers.length;
for (int i = 0; i < count; i++) {
sp.removeSpan(watchers[i]);
}
if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();
sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE |
(CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
if (mEditor != null) mEditor.addSpanWatchers(sp);
if (mTransformation != null) {
sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
}
if (mMovement != null) {
mMovement.initialize(this, (Spannable) text);
/*
* Initializing the movement method will have set the
* selection, so reset mSelectionMoved to keep that from
* interfering with the normal on-focus selection-setting.
*/
if (mEditor != null) mEditor.mSelectionMoved = false;
}
}
if (mLayout != null) {
checkForRelayout();
}
sendOnTextChanged(text, 0, oldlen, textLength);
onTextChanged(text, 0, oldlen, textLength);
notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
if (needEditableForNotification) {
sendAfterTextChanged((Editable) text);
}
// SelectionModifierCursorController depends on textCanBeSelected, which depends on text
if (mEditor != null) mEditor.prepareCursorControllers();
}
2 從上面的原始碼中我們可以看到下面幾個問題:
A. 程式碼很長(好重要的感悟啊!!!)
B. 判斷type的時候,在一些條件下會重新構造Spannable,至於構造Editable咋們就無視掉。
C.對text移除ChangeWatcher(繼承TextWatcher,SpanWatcher),重新新增ChangeWatcher3 我們需要一樣什麼樣的TextView,在這個大批量重新整理的情況下。我們要的只是一個高效率展示覆雜文字的TextView,不需要Editable,也不需要ChangeWatcher。更重要的我們如果能先計算好佈局,省去settext這個複雜過程,直接給View呼叫這樣效能提升會更快。
解決方法:我們需要的是一個靜態的文字佈局
- 1 Android提供了文字佈局的基類android.text.Layout,注意看Layout的類註釋:
/**
* A base class that manages text layout in visual elements on
* the screen.
* <p>For text that will be edited, use a {@link DynamicLayout},
* which will be updated as the text changes.
* For text that will not change, use a {@link StaticLayout}.
*/
public abstract class Layout {
註釋中提到:對於可編輯的文字,使用DynamicLayout,當文字變化的時候會執行update。而對於靜態文字,推薦使用StaticLayout.
- 2 翻過來看TextView怎麼做的了?
TextView包含了3種佈局:
a. BoringLayout (單行純文字文字)
b. Staticlayout (多行復雜文字)
c. DynamicLayout (多行可編輯複雜文字)
從TextView的設計上來說,這個設計應該能滿足我們的多行靜態複雜文字的展示啊(判斷完邏輯採用Staticlayout啊!!!),但是閱讀下原始碼,你會發現,TextView構造一個layout,只要文字中有Spannable的出現,TextView就會構造DynamicLayout來負責文字展示。
整個TextView選擇layout的原始碼如下:
protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
boolean useSaved) {
Layout result = null;
if (mText instanceof Spannable) { //!!!!!!!注意看這裡!!!!
result = new DynamicLayout(mText, mTransformed, mTextPaint, wantWidth,
alignment, mTextDir, mSpacingMult, mSpacingAdd, mIncludePad,
mBreakStrategy, mHyphenationFrequency,
getKeyListener() == null ? effectiveEllipsize : null, ellipsisWidth);
} else {
if (boring == UNKNOWN_BORING) {
boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
if (boring != null) {
mBoring = boring;
}
}
if (boring != null) {
if (boring.width <= wantWidth &&
(effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad);
}
if (useSaved) {
mSavedLayout = (BoringLayout) result;
}
} else if (shouldEllipsize && boring.width <= wantWidth) {
if (useSaved && mSavedLayout != null) {
result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
} else {
result = BoringLayout.make(mTransformed, mTextPaint,
wantWidth, alignment, mSpacingMult, mSpacingAdd,
boring, mIncludePad, effectiveEllipsize,
ellipsisWidth);
}
}
}
}
if (result == null) {
StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
0, mTransformed.length(), mTextPaint, wantWidth)
.setAlignment(alignment)
.setTextDirection(mTextDir)
.setLineSpacing(mSpacingAdd, mSpacingMult)
.setIncludePad(mIncludePad)
.setBreakStrategy(mBreakStrategy)
.setHyphenationFrequency(mHyphenationFrequency);
if (shouldEllipsize) {
builder.setEllipsize(effectiveEllipsize)
.setEllipsizedWidth(ellipsisWidth)
.setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
}
// TODO: explore always setting maxLines
result = builder.build();
}
return result;
}
StaticLayout表示不服,Spannable我也能處理,而且我簡單效率高。Google為什麼這麼設計,可能是因為TextView作為Button, CheckedTextView, EditText, Switch,ToggleButton等的基類表示壓力山大吧!有那位同學給我解析下為啥TextView要把EditText這個子類關於edit相關的活自己幹!
找到了解決問題的方式,下面就是純擼程式碼了
- 1 自定義StaticLayoutView
public class StaticLayoutView extends View {
private Layout layout;
private int width ;
private int height;
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public StaticLayoutView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public StaticLayoutView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public StaticLayoutView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public StaticLayoutView(Context context) {
super(context);
}
public void setLayout(Layout layout) {
this.layout = layout;
if (this.layout.getWidth() != width || this.layout.getHeight() != height) {
width = this.layout.getWidth();
height = this.layout.getHeight();
requestLayout();
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
canvas.save();
if (layout != null) {
layout.draw(canvas, null, null, 0);
}
canvas.restore();
}
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (layout != null) {
setMeasuredDimension(layout.getWidth(), layout.getHeight());
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
}
}
- 2 StaticLayout [父類Layout]的一個建構函式
/**
* Subclasses of Layout use this constructor to set the display text,
* width, and other standard properties.
* @param text the text to render
* @param paint the default paint for the layout. Styles can override
* various attributes of the paint.
* @param width the wrapping width for the text.
* @param align whether to left, right, or center the text. Styles can
* override the alignment.
* @param spacingMult factor by which to scale the font size to get the
* default line spacing
* @param spacingAdd amount to add to the default line spacing
*
* @hide
*/
protected Layout(CharSequence text, TextPaint paint,
int width, Alignment align, TextDirectionHeuristic textDir,
float spacingMult, float spacingAdd) {
a. 構造CharSequence,平時我們對複雜文字的處理,就是通過spannerString來構造,比如某段用什麼顏色,某段用什麼size
b. 構造TextPaint,可以設定整個文字的字型size,color等(當然spannerString來分段控制更好),也可以來設定字型的type等
c. 輸入其他引數,構造完整個StaticLayout後,呼叫StaticLayoutView的
setLayout方法就完成了整個繪製,這個構造的過程當然也可以放在子執行緒來做。
- 3 細節問題
細節永遠是最麻煩的事情,比如你專案有預設設定的複雜文字的顏色,文字size,陰影等等,不像textview天生就提供textsize,textcolor,shadow這些屬性,你的選擇有2個。第一,自己構造這些屬性,第二,圍繞你的CharSequence和textpaint構造更多方便實現你業務ui效果的方法。
- 4 StaticLayout的用途
a.文中高頻度大量textview重新整理優化。
b.一個textview顯示大量的文字,比如一些閱讀app。
c. 在控制元件上畫文字,比如一個ImageView中心畫文字。
d. 一些排版效果,比如多行文字文字居中對齊等。
相關文章
- 效能測試瓶頸調優
- 用 pprof 找出程式碼效能瓶頸
- 實用技巧:快速定位Zuul的效能瓶頸Zuul
- Android效能優化(4):UI渲染機制以及優化Android優化UI
- 突破效能瓶頸,實現流程自動化
- 漫談前端效能 突破 React 應用瓶頸前端React
- Oracle效能優化方法論的發展之四:基於資源瓶頸分析的優化方法論Oracle優化
- 流量高峰時期的效能瓶頸有哪些、以及如何來解決
- SQL Server 資料庫 最佳化 效能瓶頸SQLServer資料庫
- 前端不止:Web效能優化–關鍵渲染路徑以及優化策略前端Web優化
- 執行在 CCV2 環境上的 Angular 伺服器端渲染應用的效能瓶頸分析Angular伺服器
- 效能測試瓶頸之CPU問題分析與調優
- React渲染效能優化React優化
- VUE 渲染效能優化Vue優化
- 如何正確定義效能瓶頸
- 利用PerfDog分析遊戲效能瓶頸遊戲
- Chrome執行時效能瓶頸分析Chrome
- Redis效能瓶頸揭秘:如何最佳化大key問題?Redis
- 效能課堂-TPS 瓶頸精準定位
- LightDB資料庫效能瓶頸分析(一)資料庫
- JVM 效能調優實戰之:一次系統效能瓶頸的尋找過程JVM
- 各種儲存效能瓶頸場景的分析與最佳化手段
- 效能測試-服務端瓶頸分析思路服務端
- 2020.10.6 效能課堂筆記-cpu 瓶頸分析筆記
- I/O已經不再是效能瓶頸
- 高併發下log4j的效能瓶頸
- 五個容易錯過的 PostgreSQL 查詢效能瓶頸SQL
- 伺服器IO瓶頸對MySQL效能的影響伺服器MySql
- 徹底瞭解渲染引擎以及幾點關於效能優化的建議優化
- 收集的一些React hooks的效能優化以及閉包陷阱問題ReactHook優化
- 使用 sar 和 kSar 來發現 Linux 效能瓶頸Linux
- 六、Android效能優化之UI卡頓分析之渲染效能優化Android優化UI
- 人到中年了的瓶頸
- React 效能優化 - 避免重複渲染React優化
- 使用 content-visibility 優化渲染效能優化
- 用資料說話,億級海量資料分析效能瓶頸如何破?
- AndroidAPP效能優化的一些思考AndroidAPP優化
- 效能之殤:從馮·諾依曼瓶頸談起
- 擴充套件jwt解決oauth2 效能瓶頸套件JWTOAuth