What is new in Android P — PrecomputedText

Photo by Caspar Rubin on Unsplash (image source)

This is part of a series of articles about the new APIs available 
in Android 9.0:

  1. BiometricPrompt
  2. ImageDecoder and AnimatedImageDrawable
  3. PrecomputedText
The sample application with source code used in this article can be found here.
Code snippets are written in Kotlin.

How text is displayed

Every Android developer knows that in order to display text in an application we need to use the TextView widget. It is as simple as that:

textView.text = "Hello"
// or
textView.setText(R.string.hello_string)

But not every developer is aware of how complex and time consuming this process is.

Drawing text in an Android application can be divided into parts:

  1. Identifying the glyphs
    For each character in the text, proper graphical representations — glyph, needs to be found in the fonts set. One glyph can contain one or few characters.
  2. Positioning the glyphs
    All glyphs are positioned in order to create words.
  3. Measuring the words
    Each word is measured based on the target TextView parameters such as size, style, etc.
  4. Layouting the words 
    If text is longer than one line than proper line breaks needs to be identify. Performance of this process strongly depends on the breakStrategy and hyphenationFrequency parameters of the target TextView widget. For example: enabling hyphenation can increase layouting time 2.5 times.
  5. Drawing text in the TextView
    Whole text is draw on the TextView widget canvas.

Text cache and performance measuring

When we take a look at performance it will turn out that steps 1, 2 and 3 are the most expensive. That is why, in Android 4.0, TextLayoutCache was introduced. The idea is simple: each measured word is added to the LRU cache and the next time the same word needs to be displayed on the TextView with the same parameters (size, style, etc.) it can be taken from the cache and no further measurements are required. The cache size is 0.5 MB and it is owned by the system so applications can’t modify it directly.

In order to test how a text cache can improve drawing text performance I prepared a simple application with two buttons — “set text” and “clear text” and one TextView. The scenario was to display long, randomly generated text in the TextView, then clear it and display the same text again. The second operation should be performed faster because text is already in the text cache.

I’ve used Android Studio 3.2 and a built in Android Profiler tool in order to measure the setText(...) method execution time. The measurements were as follows:

In the second setText(...) method execution, words were loaded from the text cache — they were already measured. That is why this execution was 3 to 10 times faster. That is a very impressive performance boost.


Fun fact: In 2015, one optimization made to the Instagram app was based on the text cache. The idea was to virtually draw text on an off screen canvas in order to ‘warm up’ the cache. Then the actual text in the app was displayed faster because it was already measured.


Precomputing text on Android P

Text cache can improve the text displaying performance but it is a system component and can’t be controlled by the app. That is why in the new Android P framework Google prepared something even better — PrecomputedText. The object of this class contains both the text and the text layout measurements. So when this text is set on a TextView no further calculations are required and text is displayed faster.

In order to create a PrecomputedText object we need to pass both the String to display and metrics parameters of the target TextView. All the text and layout measurements will be calculated in the constructor so this object should be created on the background thread.

Example of use (worker thread is provided by Kotlin coroutines):

val mParams = textView.textMetricsParams
val ref = WeakReference(textView)
GlobalScope.launch(Dispatchers.Default) {
// worker thread
val pText = PrecomputedText.create("Hello", mParams)
GlobalScope.launch(Dispatchers.Main) {
// main thread
ref.get()?.let { textView ->
textView.text = pText
}
}
}

Based on the measurements it is clear that PrecomputedText is faster even than text cache, especially for long texts.

Normal and precomputed text performance on Pixel 2 with Android 9.0

It is worth mentioning that if we use a PrecomputedText object on a different TextView — it will be rejected and text layout measurements will be calculated again.

val mParams = textView.textMetricsParams
val pText = PrecomputedText.create(text, mParams)
// fast execution — no further text layout calculations:
textView.text = pText
// slow execution — text layout needs to be recaluclated:
differentTextView.text = pText

Precomputing text on older devices

On pre Android P devices we can also improve displaying text performance by using PrecomputedTextCompat. However this component works based on the TextLayoutCache so it has its limitations (cache size is 0.5 MB). ‘Precompute’ in this case means that the measured words are put into the system text cache so when setting precomputed text on a TextView we do not have guarantee that its words are still in the cache. PrecomputedTextCompat class is available from Android API 14.

Example of use:

val mParams = TextViewCompat.getTextMetricsParams(textView)
val ref = WeakReference(textView)
GlobalScope.launch(Dispatchers.Default) {
// worker thread
val pText = PrecomputedTextCompat.create("Hello", mParams)
GlobalScope.launch(Dispatchers.Main) {
// main thread
ref.get()?.let { textView ->
TextViewCompat.setPrecomputedText(textView, pText)
}
}
}

Measurements are very similar to the one which was presented before — first and second setText(...) method execution time. That is because the same mechanism is used in both cases — the text cache.

Precomputed text in RecyclerView

Text precomputation makes sense only if it is performed asynchronously on the background thread. When performed on the main thread it will still block the UI. So it should be used only in cases when we know the text before it needs to be displayed on the UI. The most common cases are:

  • fetching text from the backend
  • text is not visible in initial screen state but only after user interaction (button click, swipe, etc..)
  • displaying text in RecyclerView

The first and second cases are quite obvious, so we will focus on the last one.

RecyclerView by default prefetches its items. While the UI thread is idle between the frames LayoutManager is queried for items to inflate/bind views outside of its viewport. It improves the performance and when we scroll down the recycler view every new item appearing on the screen it is already measured, layouted and ready to display. So if this item contains static text what actually happen is that this text is measured before it appear on the screen. So no matter how long this text is we can improve the performance by moving its layout measurement part into background thread using PrecomputedText (or its comat version for older devices).

Setting text in RecyclerView adapter the normal way:

onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
holder.itemView.textView.text = "Hello"
}

Setting text in RecyclerView adapter using PrecomputedText:

onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView = holder.itemView.textView
val mParams = textView.textMetricsParams
val ref = WeakReference(textView)

GlobalScope.launch(Dispatchers.Default) {
// worker thread
val pText = PrecomputedText.create("Hello", mParams)
GlobalScope.launch(Dispatchers.Main) {
// main thread
ref.get()?.let { textView ->
textView.text = pText
}
}
}
}

Setting text in RecyclerView adapter using PrecomputedTextCompat::

onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val textView = holder.itemView.textView
val mParams = TextViewCompat.getTextMetricsParams(textView)
val ref = WeakReference(textView)
  GlobalScope.launch(Dispatchers.Default) {
// worker thread
val pText = PrecomputedTextCompat.create("Hello", mParams)
GlobalScope.launch(Dispatchers.Main) {
// main thread
ref.get()?.let { textView ->
TextViewCompat.setPrecomputedText(textView, pText)
}
}
}
}

RecyclerView text performance

I’ve also prepared sample app in order to measure the normal and precomputed text performance in RecyclerView. The scenario was that adapter provides 100 items or random text items — each has about 1.5k characters).

I’ve tested it both on Nexus 5 with Android 6.0 and Pixel 2 with Android 9.0. Testes subject was the time which the UI thread needs to measure and display the text. So for normal text it was only time of onBindViewHolder(...) method execution and for precomputed text both onBindViewHolder(...) and setText(...) method execution time — because in this case the set text process was asynchronous).

The results was as follows:

And again, precomputed text is the winner — it uses less time of the UI thread.

Conclusion

Performance is a very important part of every Android application. If it can be improved in any way it definitely should be done. New precomputed text can help with that and thanks to its compat version it will be noticeable on older devices too. I encourage you to try it in your own projects.

If you’ve found this article useful please don’t hesitate to share it and if you have any further questions, feel free to comment below.