Text rendering on Android

Cuong Le
4 min readMar 30, 2018

TextView is one of the most complex widget in the entire framework. It is mostly responsible for displaying text on Android — other widgets also directly or indirectly inherit TextView such as Button, EditText... Its internal implementation is quite complex, the number of lines on the code alone, android-27 in TextView full 11968 lines. In addition, TextView’s operations are very heavy — for example, the setText method needs to set SpanWatcher, allocate a new Spannable, re-create Layout. This article will cover pre-rendering technique in order to improve TextView performance.

TextView basic principle

Every images displayed on Android are backed by Drawable . If we look at ImageView code— it’s simple class that contains a Drawable . So what do we have for TextView? something calledTextDrawable ?

Or Canvas.drawText()?

In reality, Android exposes a nice package named android.text.*. Inside we find a class Layout . As its name suggests, Layout contains all the logic to lay out text. There are three implementations for this class:

  1. BoringLayout displays a single line of text — provides isBoring method to determine whether the conditions of a single line are met.
  2. DynamicLayout recalculates itself as the text is edited. This is used by widgets to manipulate text layout like EditText.
  3. StaticLayout is a Layout that is not a single line, and can not be edited. This renders static text like TextView.

In the Layout.draw method, text is drawn one line at a time:

TextLine tl = TextLine.obtain();for (int lineNum = firstLine; i <= lastLine; lineNum++) {
....
Directions directions = getLineDirections(i);
if (directions == DIRS_ALL_LEFT_TO_RIGHT && !mSpannedText && !hasTab && !justify) {
canvas.drawText(buf, start, end, x, lbaseline, paint);
} else {
tl.set(paint, buf, start, end, dir, directions, hasTab, tabStops);
if (justify) {
tl.justify(right - left - indentWidth);
}
tl.draw(canvas, x, ltop, lbaseline, lbottom);
}
}

If text is instance of String or SpannedString, Android will directly use Canvas to draw. Otherwise, Android uses an internal class TextLine to handle complex text elements like Spannable, emoji. TextView ends up spending most of the time on measure and layout logic.

TextLayoutCache

When Canvas.drawText(), Android has to calculate font size, margins… It results in time-consuming operation during drawing phase. In order to improve efficiency, Android introduced TextLayoutCache since 4.0. Internally, it uses LRU caching font, margins, and other data. On Kitkat, the cache size is 0.5M, used globally. For Activity, configurationChanged, onResume, lowMemory, updateVisibility is the time to call Canvas.freeTextLayoutCache to release the memory. The cache is part of the system, we cannot control it directly. As of Lollipop, Google uses libminikin to render text — Layout.cpp can be found instead ofTextLayoutCache.cpp.

The class is mainly responsible for drawing fonts in hwui package is FontRenderer which holds a LRU fonts, a list of text texture cache.

CacheTexture contains texture data for a text that need to be drawn.

Texture of a word is obtained through Skia before it is copied to CacheTexture. Then texture’s size, uv coordinate and corresponding CacheTexture are saved in CachedGlyphInfo. At drawing phase, only large texture cache is uploaded to GPU, meanwhile uv coordinate of the text is uploaded as well. In this way, text can be drawn in batch, which avoids uploading and rendering single word texture each time. Performance thereby can be greatly improved.

TextView pre-rendering optimization

TextView core principle, when simple text is displayed, it’s unnecessary to set up a SpanWatcher to monitor changes. We can directly use BoringLayout or StaticLayout.


public
class TextLayoutView 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();
}
}

Since we know we are going to show these texts after we download them, a simple improvement we made was pre-create the layouts. Despite of being able to cache the text.Layout, the initial drawing time is still relatively long. Most of these times were taken by measuring text advances and generating text glyphs. To surpass this issue, we take advantage of TextLayoutCache mentioned above. We need to warm up this cache before we draw the text on screen. The idea is virtually drawing this text on an off-screen canvas. This way we can warm up the TextLayoutCache on a background thread before we draw the text on screen.

Canvas canvas = picture.beginRecording(layout.width, layout.height)
layout.draw(canvas)
picture.endRecording()

Performance comparison

The test case is that a ListView shows 300 Items. Each item is a piece of plain text, which are all SpannableString contains a large number of ImageSpan for comparison on both sides, one side is the direct use of StaticLayout, while the use of ordinary TextView, and the 300 paragraphs of the text are not all the same, different lengths, randomly generated.

In addition, to simulate the real world — three threads are added doing some work in attempt to seize CPU resources.

Measurement performance indicators, ListView continuous scroll down to measure the average frame rate for how much were measured five times, calculate the average, the final performance test results are as follows:

References

Improving Comment Rendering on Android

--

--