Android TextView前レンダリング研究


AndroidのTextViewは、フレームワーク全体の中で最も複雑なコントロールの一つであり、Androidでテキストを表示する大部分の仕事を担当しています。frame workの多くのコントロールも直接または間接的にTextView、例えばButton、EditTextなどに継承されています。その内部実装もかなり複雑で、シングルコードの行数から言えば、android-22のTextViewはたっぷり9509行あります。また、Text Viewの多くの操作は非常に重いです。例えば、set Text操作はSpanWatchを設定する必要があります。またはSpannable Stringを再作成する必要があります。また、状況に応じてText Layoutを再作成します。これらの操作を合わせた後、一回のsetText操作は非常に時間がかかります。TextViewのレンダリング効率を高めるために、最近レンダリングの方法を検討しました。次に原理を説明します。
TextViewレンダリングの基本原理
まず、TextViewの基本的なレンダリング原理を紹介します。総じて言えば、TextViewの中でレンダリングする文字を担当するのは主にこの三つの種類です。
BoringLayout
主に1行のテキストを表示し、1行のテキストが満たされているかどうかを判断するためのisBoring方法を提供する。
DynamicLayout
テキストがSpannableの場合、TextViewはそれを使ってテキストの表示を担当し、内部にSpanWatchを設置し、spanの変化を検出するとreflowを行い、レイアウトを再計算します。
Static Layout
テキストが単行でないテキストで、Spannableでない場合はStatic Layoutを使用します。内部ではspanの変化を監視しませんので、効率的にはDynamicLayoutよりも高くなります。レイアウトの作成だけでいいですが、内部にはSpannable Stringが表示されます。
また、上記の三つのクラスは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);

Spannbaleに対して、またはemojiのテキストを含むと、実際の描画操作はTextLineに任せています。そうでなければ、直接にcanvas.drawTextを使って、TextLineは単行の複雑なテキストの描画を担当しています。その中にSpannable、Emojiなどの描画ロジックが含まれています。TextLineの描画ロジックもあまり効率的ではありません。ここではこれからも引き続きその最適化について説明します。
Text LayoutCache
Canvasは、drawTextの時に、毎回フォントの大きさや余白などを計算する必要があると、とても時間がかかります。drawTextの時間が長くなります。効率を上げるために、androidは4.0の後にTextLayoutCacheを導入して、LRU Cacheを使って字形、余白などのデータをキャッシュして、drawTextのスピードを上げます。4.4 cacheの大きさは0.5 cacheです。全体的にはActivityのconfigrationChend、onResume、lowMemory、udateVisibilityなどのタイミングで、Canvas.free TextLayoutCacheを呼び出してこのメモリをリリースします。この部分のcacheはシステムの下の階の制御ですので、具体的な制御はできません。
TextViewのプリレンダリング最適化
TextViewのレンダリング原理から見ると、単にテキストを表示するだけであれば、SpanWatchを別途設置してspanの変化をモニターする必要はありません。だから、私達は直接にBoringLayoutまたはStatic Layoutを使ってテキストの内容を直接表示できます。
私たちはカスタム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オブジェクトを直接使用してテキストを描きます。ここではset Text方法を捨てて、直接Layoutを通してテキストを描きます。ここのLayoutオブジェクトは、あらかじめ作成してから設定できます。これは普通のText ViewのsetTextメソッドよりも、setTextの多くの消費を減らして、効率を大幅に向上させることができます。
Static Layoutの作成はとても簡単で、テキストを与えられただけで、幅などが直接作成できます。また、TextLayoutCacheを事前に充填するために、Static Layoutオブジェクトを作成した後に、dummy canvasの中からdrawが出てきても良いです。
Static Layout layout=new Static Layout(TestSpan.get SpanString(i)、textPaint、hard CodeWidth、alignment、1.0 f、0 f、true)
layout.draw(dummyCanvas);
性能の比較
次に具体的な性能をテストします。ここのtestcaseはGithubに置いています。Static LayoutView
testcaseの内容は、一つのListViewに300個のItemが表示され、各itemは一段の純テキストであり、全部ImagespanのSpannable Stringが大量に含まれています。両方の対比を行います。一方はStatic Layoutを直接使用します。一方は普通のTextViewを使用しています。そして、この300段のテキストは全部同じではなくて、長さは違っています。Static Layoutは他のスレッドを作成してから設置されています。またSpannable Stringも予め生成されています。
また、ここでは実際のappの重いバックグラウンド作業をシミュレートするために、3つのスレッドを作成し、CPUリソースの乗っ取りを試みるために浮動小数点の予算を継続して行っている。
性能を測定する指標はListViewが連続的に下にスクロールし、その平均フレームレートを測定し、それぞれ5回測定し、その平均値を計算し、最終性能試験の結果は以下の通りである。

ここでテストするマシンはMX 3です。左側はStatic Layoutを直接使う方式です。右側はシステムのデフォルト方式です。Y軸はFPSです。最適化した方案を使って、フレームレートが多く上がりました。
References
Improving Comment Rendering on Android
この文章はInstagramがどのように彼らのTextViewのレンダリングの効率を最適化するかを紹介しています。これもここの最適化方法の源です。Instagramも直接Static Layoutを使って、Layoutを事前に作成する方法によってListViewのスクロール中のフレームレートを減少させ、効果は非常に顕著です。この文章はここの原理解析と簡単な実現を与えたと言える。
以上はAndroid TextViewに対して事前にレンダリングした資料を整理して、引き続き関連資料を補充します。ありがとうございます。