作者:唯鹿
連結:https://juejin.im/post/5cb3e62ae51d456e2809fb72
概述
PrecomputedText 如字面意義一樣,是用來預先計算文字的。它的誕生也是因為計算文字是一個耗時操作,它需要根據字號、字型、樣式、換行等去計算,並且這個計算時間隨著文字數量的增加而增加。如果這時顯示的串列中恰好是這種多行的文字,那麼滑動起來豈不是會掉幀,影響著使用者體驗。比如微博這類的產品,串列就非常的複雜。
其實在Android 4.0 中底層就有引入TextLayoutCache來解決這個問題,每個測量過的文字都被新增到快取中,下次需要相同的文字時,可以從快取中獲取,不用在測量。不過快取大小隻有0.5 MB。並且在沒有快取之前,我們的首次滑動還是UI執行緒耗時的。
為瞭解決這類問題,Android 9.0中添加了PrecomputedText 。
據說測量的耗時減少了95%,具體對比可以參看文末的連結。
使用方法
-
compileSdkVersion為28以上,appcompat庫28.0.0或androidx appcompat 1.0.0以上
-
使用AppCompatTextView來替換TextView
-
使用setTextFuture 替換 setText 方法
程式碼如下:
Future future = PrecomputedTextCompat.getTextFuture(
“text”, textView.getTextMetricsParamsCompat(), null);
textView.setTextFuture(future);
當然如果你使用kotlin,那麼利用拓展方法會更加酸爽。
fun AppCompatTextView.setTextFuture(charSequence: CharSequence){
this.setTextFuture(PrecomputedTextCompat.getTextFuture(
charSequence,
TextViewCompat.getTextMetricsParams(this),
null
))
}
// 一行呼叫
textView.setTextFuture(“text”)
實現原理
其實PrecomputedText實現原理很簡單,就是將耗時的測量放到了非同步去執行。
@UiThread
public static Future getTextFuture(@NonNull CharSequence charSequence, @NonNull PrecomputedTextCompat.Params params, @Nullable Executor executor) {
PrecomputedTextCompat.PrecomputedTextFutureTask task = new PrecomputedTextCompat.PrecomputedTextFutureTask(params, charSequence);
if (executor == null) {
Object var4 = sLock;
synchronized(sLock) {
if (sExecutor == null) {
sExecutor = Executors.newFixedThreadPool(1);
}
executor = sExecutor;
}
}
executor.execute(task);
return task;
}
透過呼叫consumeTextFutureAndSetBlocking方法的future.get()阻塞計算執行緒來獲取計算結果,最終setText到對用的TextView上。
@UiThread
public static Future getTextFuture(@NonNull CharSequence charSequence, @NonNull PrecomputedTextCompat.Params params, @Nullable Executor executor) {
PrecomputedTextCompat.PrecomputedTextFutureTask task = new PrecomputedTextCompat.PrecomputedTextFutureTask(params, charSequence);
if (executor == null) {
Object var4 = sLock;
synchronized(sLock) {
if (sExecutor == null) {
sExecutor = Executors.newFixedThreadPool(1);
}
executor = sExecutor;
}
}
executor.execute(task);
return task;
}
新的問題
在看PrecomputedText時,在Github上找到了一個相關的Demo,這其中發現使用後造成了負最佳化。
這個例子中,一個item上有三個AppCompatTextView並且字號都很小,導致一螢幕可以看到十段左右的文字,當然使用了PrecomputedText最佳化後,onBindViewHolder方法的執行時間大大的縮短了,但是卻檢測到了新的問題。
https://github.com/CherryPerry/PrecomputedTextDemo
首先我們要瞭解滑動串列的速度越快,那麼單位時間內測量繪製的內容也就越多。我對使用前後進行了三種速度的測試,分別是慢速(1s滑動1次,力度小)、中速(1s滑動2次,力度中)、快速(1s滑動3次,力度大)得到了下麵的結論。(純手工滑動,真的累。。。)
具體的Systrace結果圖我就不全部展示了,這裡展示一下中速滑動前後結果。
代表Animation和Input的淺綠色豎條增高了。
最終的統計如下:
問題/速度 | 慢速 | 中速 | 快速 |
---|---|---|---|
Scheduling delay | 4 -> 46 | 5 -> 39 | 8 -> 17 |
Long View#draw() | 18 -> 12 | 37 -> 30 | 50 -> 48 |
Expensive measure/layout pass | 1 -> 0 | 0 | 0 |
Scheduling delay 就是一個執行緒在處理一塊運算的時候,在很長一段時間都沒有被CPU排程,從而導致這個執行緒在很長一段時間都沒有完成工作。
可以看到使用PrecomputedText後,Scheduling delay 問題會有一定機率激增,甚至更加嚴重。對比發現激增點都是因為dequeueBuffer 這裡等待時間過長,比如下麵 dequeueBuffer 的片段cpu實際執行了0.119ms,但是總耗時了10.035ms。
其實仔細觀察,dequeueBuffer 在一開始就已經執行完成,但是卻處在等待cpu排程來執行下一步的地方。這裡其實就是等待SurfaceFlinger執行導致的。如下圖:
這裡的耗時,會使通知 CPU 延遲,導致的Scheduling delay 。具體為何高機率觸發這類問題的原因這裡還不清楚。猜測是文字本身很複雜,一段文字中不同字號、顏色、樣式,並且頁面上同時存在十多個這樣的段落。這樣的話就短時間內會有十多次執行緒的切換來實現文字的非同步測量,勢必會有效能影響。
我後面將文字字型設定大了以後,發現這個問題就好多了。
所以PrecomputedText的使用還是需要根據場景來使用,否則會矯枉過正。
總結
-
不要濫用PrecomputedText,對於一兩行文字來說並沒有很大的提升,反而會造成不必要的Scheduling delay,建議文字200個字元以上使用。
-
不要在TextViewCompat.getTextMetricsParams()方法後修改textview的屬性。比如設定字號放到前面執行。
-
PrecomputedTextCompat在9.0以上使用PrecomputedText最佳化,5.0~9.0使用StaticLayout最佳化,5.0以下的不做處理。
-
如果您已禁用RecyclerView的預取(Prefetch),則PrecomputedText無效。如果您使用自定義LayoutManager,請確保它實現 collectAdjacentPrefetchPositions()以便RecyclerView知道要預取的專案。因此ListView 無法享受到PrecomputedText帶來的效能最佳化。
具體kotlin示例可以看 PrecomputedTextCompatExample ,裡面也有使用協程的最佳化方案。
https://github.com/satoshun-android-example/PrecomputedTextCompatExample
我也寫了一個對應的java示例
https://github.com/simplezhli/RecyclerViewExtensionsDemo
效果如下:
normal
PrecomputedText future
PrecomputedText coroutine
最後如果對你有幫助,希望點贊支援!!
參考
-
Use Android Text Like a Pro
-
What is new in Android P?—?PrecomputedText
-
Prefetch Text Layout in RecyclerView
朋友會在“發現-看一看”看到你“在看”的內容