TextView預渲染研究

轉自:http://ragnraok.github.io/textview-pre-render-research.html

Android中的TextView是整個framework中最複雜的控件之一,負責Android中顯示文本的大部分工作,framwork中的許多控件也直接或者間接的繼承於TextView,例如Button,EditText等。其內部實現也相當複雜,單論代碼行數來說,android-22中TextView有足足9509行,另外,TextView中許多操作都非常繁重,例如setText操作,需要設置SpanWatcher,或者需要重現創建一個SpannableString,還需要根據情況重新創建Text Layout,這些操作加起來之後令一次setText操作非常耗時。爲了提升TextView的渲染效率,最近研究了一下預渲染的方法,接下來給大家講解一下原理。

TextView渲染基本原理

首先來介紹下TextView的基本渲染原理,總的來說,TextView中負責渲染文字的主要是這三個類:

  1. BoringLayout 
    主要負責顯示單行文本,並提供了isBoring方法來判斷是否滿足單行文本的條件。

  2. DynamicLayout 
    當文本爲Spannable的時候,TextView就會使用它來負責文本的顯示,在內部設置了SpanWatcher,當檢測到span改變的時候,會進行reflow,重新計算佈局。

  3. StaticLayout 
    當文本爲非單行文本,且非Spannable的時候,就會使用StaticLayout,內部並不會監聽span的變化,因此效率上會比DynamicLayout高,只需一次佈局的創建即可,但其實內部也能顯示SpannableString,只是不能在span變化之後重新進行佈局而已。

另外,以上三個類都繼承於Layout類,在此類中統一負責文本的具體繪製,在Layout.draw方法中,會對文本一行一行的進行渲染:

TextLine tl = TextLine.obtain();

// Draw the lines, one at a time.
// The baseline is the top of the following line minus the current line's descent.
for (int i = firstLine; i <= lastLine; i++) {
      ....
      Directions directions = getLineDirections(i);
      if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTabOrEmoji) {
         // XXX: assumes there's nothing additional to be done
          canvas.drawText(buf, start, end, x, lbaseline, paint);
      } else {
          tl.set(paint, buf, start, end, dir, directions, hasTabOrEmoji, tabStops);
          tl.draw(canvas, x, ltop, lbaseline, lbottom);
      }
}
TextLine.recycle(tl);

可以看出來對於Spannble,或者包含emoji的文本的話,實際渲染操作是交給了TextLine去繪製,否則直接使用canvas.drawTextTextLine負責單行復雜文本的繪製,其中Spannable, Emoji之類的繪製邏輯都包含在裏面,TextLine的繪製邏輯也並非十分高效,這裏後續將會繼續說明其應該如何優化。

TextLayoutCache

Canvas在drawText的時候,如果需要每次都計算字體的大小,邊距等之類的話,就會非常耗時,導致drawText時間會拉的很長,爲了提高效率,android在4.0之後引入了TextLayoutCache,使用LRU Cache緩存了字形,邊距等數據,提升了drawText的速度,在4.4中,這個cache的大小是0.5M,全局使用,並且會在Activity的configurationChangedonResumelowMemoryupdateVisibility等時機,會調用Canvas.freeTextLayoutCache來釋放這部分內存。由於這部分的cache是系統底層控制的,我們無法做具體的控制。

TextView的預渲染優化

從TextView的渲染原理來看,如果只是單純的顯示文本,我們根本不需要另外設置SpanWatcher來監聽span的變化,因此我們可以直接使用BoringLayout或者StaticLayout來直接顯示文本內容,但是BoringLayout只能顯示單行文本,因此這裏最好的選擇是直接用StaticLayout

我們選擇了自定義View,並希望最終有這樣的一個接口:

public class StaticLayoutView extends View {
    private Layout layout = null;
    public void setLayout(Layout layout) {
        this.layout = layout;
        requestLayout();
    } 
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        canvas.save();
        if (layout != null) {
            layout.draw(canvas, null, null, 0);
        }
        canvas.restore();
    }
}

我們可以直接通過設置這個view的Layout來繪製文本,並在onDraw方法中直接使用這個Layout對象來繪製文本。在這裏我們摒棄了setText方法,直接通過Layout來繪製文本,而這裏的Layout對象,我們可以通過預先創建之後才設置進去(這裏可以放到單獨的一個線程中創建),這樣對比起普通TextView的setText方法,我們減少了setText中的許多消耗,可以大幅度的提升效率。

StaticLayout的創建非常簡單,只需要給定文本,寬度等就能直接創建。另外,爲了預先填充TextLayoutCache,我們也可以在創建完StaticLayout對象之後,預先在一個dummy canvas中draw出來:

StaticLayout layout = new StaticLayout(TestSpan.getSpanString(i), textPaint, hardCodeWidth, alignment, 1.0f, 0f, true);
layout.draw(dummyCanvas);

性能對比

接下來我們測試一下具體的性能,這裏的testcase放到了Github上:StaticLayoutView

testcase的內容爲,在一個ListView中,顯示300個Item,每個item都是一段純文本,裏面全都是包含有大量ImageSpan的SpannableString,進行兩邊的對比,一邊是直接使用StaticLayout,一邊是使用普通的TextView,並且這300段文本不全相同,長度不同,隨機生成,在StaticLayout的testcase中,StaticLayout都是預先在另外一個線程創建好之後才設置進去的,另外SpannableString也是預先生成好的。

另外,在這裏爲了模擬真實app繁重的後臺工作,另外創建了3個線程,不停在做浮點預算以嘗試搶佔CPU資源。

測量性能的指標爲,ListView連續向下滾動,測量其平均幀率爲多少,分別測量五次,計算其平均值,最終性能測試結果如下:

這裏測試的機器是MX3,左側是直接使用StaticLayout的方案,右側是系統的默認方案,Y軸是FPS,可以看出來,使用優化之後的方案,幀率提升了許多。

References

Improving Comment Rendering on Android 
這篇文章介紹了Instagram如何優化他們的TextView渲染的效率,這也是這裏優化方法的來源,Instagram也是直接使用StaticLayout並通過預先創建Layout的方法來減少了ListView滾動過程中的掉幀率,並且效果非常顯著。這篇文章算是給出了這裏的原理解析以及一個簡單的實現


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章