一、背景
在我們的實際應用中,用戶在發(fā)布文本時,輸入大量表情后嘗試從中間刪除時,會出現(xiàn)明顯的卡頓問題。這種操作可能耗時長達2s,導致用戶體驗受到嚴重影響。通過使用 Profiler分析耗時的方法,我們找到了造成卡頓的原因,并參考了 emoji2源碼提出了解決方案。
二、原因分析
如圖所示,當從中間刪除一個表情時,耗時方法從SpannableStringBuilder.delete執(zhí)行到SpannableStringBuilder.sendSpanChanged方法,SpannableStringBuilder.sendSpanChanged方法調(diào)用了DynamicLayout$ChangeWatcher.onSpanChanged,執(zhí)行了很多次,并且每次調(diào)用非常耗時。

點擊DynamicLayout$ChangeWatcher.onSpanChanged方法后看一下對這個方法的分析,從下圖中可以看出這個方法被調(diào)用了很多次。

根據(jù)以上Profiler的分析,我們無法準確定位問題所在,因此我們決定測試系統(tǒng)表情的表現(xiàn)。測試結(jié)果顯示,系統(tǒng)表情并沒有出現(xiàn)卡頓的問題。因此,我們懷疑可能是我們的自定義表情尺寸過大,嘗試壓縮表情圖標,但仍然出現(xiàn)卡頓現(xiàn)象。
通過分析 Profiler的輸出,我們發(fā)現(xiàn)有一個類與 emoji2中的 androidx.emoji2.viewsintegration.EmojiKeyListener.onKeyDown方法相關。emoji2 是官方推出的用于適配系統(tǒng)表情的庫,我們猜測 emoji2 可能對系統(tǒng)表情進行了特殊優(yōu)化和處理。查看 emoji2 的源碼后,確實發(fā)現(xiàn)了對表情輸入進行了特殊優(yōu)化和處理。
三、emoji2的處理
emoji2 源碼位于 這里。
emoji2 使用 EmojiSpan來顯示表情,不通過ImageSpan繪制圖片,而是將所有表情封裝為字體,并利用 canvas.drawText進行繪制。雖然系統(tǒng)表情不是圖片,但每個表情都由 EmojiSpan 繪制,最終在 TypefaceEmojiRasterizer類中完成渲染。
/**
* Draws the emoji onto a canvas with origin at (x,y), using the specified paint.
*
* @param canvas Canvas to be drawn
* @param x x-coordinate of the origin of the emoji being drawn
* @param y y-coordinate of the baseline of the emoji being drawn
* @param paint Paint used for the text (e.g. color, size, style)
*/
public void draw(@NonNull final Canvas canvas, final float x, final float y,
@NonNull final Paint paint) {
final Typeface typeface = mMetadataRepo.getTypeface();
final Typeface oldTypeface = paint.getTypeface();
paint.setTypeface(typeface);
// MetadataRepo.getEmojiCharArray() is a continuous array of chars that is used to store the
// chars for emojis. since all emojis are mapped to a single codepoint, and since it is 2
// chars wide, we assume that the start index of the current emoji is mIndex * 2, and it is
// 2 chars long.
final int charArrayStartIndex = mIndex * 2;
canvas.drawText(mMetadataRepo.getEmojiCharArray(), charArrayStartIndex, 2, x, y, paint);
paint.setTypeface(oldTypeface);
}
EditableFactory類在 EditTextView 中用于創(chuàng)建可編輯的文本內(nèi)容,控制 EditTextView的文本編輯行為。這對于處理復雜的文本內(nèi)容,如帶有特殊格式、表情符號等內(nèi)容非常有用。通過自定義EditableFactory,可以優(yōu)化 EditTextView中的文本編輯性能,提高用戶體驗。可以看一下emoji2中自定義的EmojiEditableFactory中的注釋:
/**
* EditableFactory used to improve editing operations on an EditText.
* <p>
* EditText uses DynamicLayout, which attaches to the Spannable instance that is being edited using
* ChangeWatcher. ChangeWatcher implements SpanWatcher and Textwatcher. Currently every delete/add
* operation is reported to DynamicLayout, for every span that has changed. For each change,
* DynamicLayout performs some expensive computations. i.e. if there is 100 EmojiSpans and the first
* span is deleted, DynamicLayout gets 99 calls about the change of position occurred in the
* remaining spans. This causes a huge delay in response time.
* <p>
* Since "android.text.DynamicLayout$ChangeWatcher" class is not a public class,
* EmojiEditableFactory checks if the watcher is in the classpath, and if so uses the modified
* Spannable which reduces the total number of calls to DynamicLayout for operations that affect
* EmojiSpans.
EditableFactory 用于改進 EditText 上的編輯操作。
EditText 使用 DynamicLayout,該布局通過 ChangeWatcher 附加到正在編輯的 Spannable 實例。ChangeWatcher 實現(xiàn)了 SpanWatcher 和 Textwatcher。當前,每次刪除/添加操作都會向 DynamicLayout 報告每個 span 的更改。對于每次更改,DynamicLayout 都會執(zhí)行一些昂貴的計算。例如,如果有 100 個 EmojiSpans,且第一個 span 被刪除,DynamicLayout 會接到 99 次關于剩余 span 位置變化的通知。這會導致響應時間的嚴重延遲。
由于 "android.text.DynamicLayout$ChangeWatcher" 類不是公共類,EmojiEditableFactory 檢查觀察者是否在類路徑中,如果是,則使用經(jīng)過修改的 Spannable,從而減少對影響 EmojiSpans 的操作對 DynamicLayout 的調(diào)用總數(shù)。
請參閱 SpannableBuilder。
通過以上注釋可以發(fā)現(xiàn),EditableFactory旨在解決 EmojiSpan修改時耗時操作的問題。它通過自定義的 SpannableBuilder來優(yōu)化操作,從而提高了性能。
@Override
public Editable newEditable(@NonNull final CharSequence source) {
if (sWatcherClass != null) {
return SpannableBuilder.create(sWatcherClass, source);
}
return super.newEditable(source);
}
在 SpannableBuilder 中,通過自定義 WatcherWrapper對象,能夠在 span發(fā)生變化時排除對 EmojiSpan的影響。WatcherWrapper對 span變化事件進行監(jiān)控,如果檢測到是 EmojiSpan的變化,則阻止 DynamicLayout$ChangeWatcher對該 span的觸發(fā),僅在編輯結(jié)束時通知 ChangeWatcher。這種優(yōu)化僅針對 EmojiSpan操作,而其他span的更改與框架中的操作方式保持一致。
/**
* Prevent the onSpanChanged calls to DynamicLayout$ChangeWatcher if in a replace operation
* (mBlockCalls is set) and the span that is added is an EmojiSpan.
*/
@Override
public void onSpanChanged(Spannable text, Object what, int ostart, int oend, int nstart,
int nend) {
if (mBlockCalls.get() > 0 && isEmojiSpan(what)) {
return;
}
// workaround for platform bug fixed in Android P
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P) {
// b/67926915 start cannot be determined, fallback to reflow from start instead
// of causing an exception.
// emoji2 bug b/216891011
if (ostart > oend) {
ostart = 0;
}
if (nstart > nend) {
nstart = 0;
}
}
((SpanWatcher) mObject).onSpanChanged(text, what, ostart, oend, nstart, nend);
}
上面代碼是WatcherWrapper中onSpanChanged的代碼,我們可以參考這個方法,只需要把isEmojiSpan方法換成我們自己表情span的檢測就可以了。感興趣的讀者可以查看相關源碼。
/**
* When setSpan functions is called on EmojiSpannableBuilder, it checks if the mObject is instance
* of the DynamicLayout$ChangeWatcher. if so, it wraps it into another listener mObject
* (WatcherWrapper) that implements the same interfaces.
* <p>
* During a span change event WatcherWrapper’s functions are fired, it checks if the span is an
* EmojiSpan, and prevents the ChangeWatcher being fired for that span. WatcherWrapper informs
* ChangeWatcher only once at the end of the edit. Important point is, the block operation is
* applied only for EmojiSpans. Therefore any other span change operation works the same way as in
* the framework.
*
*/
當在 EmojiSpannableBuilder 上調(diào)用 setSpan 函數(shù)時,它會檢查 mObject 是否是 DynamicLayout$ChangeWatcher 的實例。如果是的話,它會將 mObject 包裝成另一個監(jiān)聽器(WatcherWrapper),該監(jiān)聽器實現(xiàn)了相同的接口。
在 WatcherWrapper 的函數(shù)在一個 span 更改事件中被觸發(fā)時,它會檢查該 span 是否為 EmojiSpan,并阻止 ChangeWatcher 對該 span 進行觸發(fā)。WatcherWrapper 只在編輯結(jié)束時通知 ChangeWatcher 一次。重要的一點是,這種阻塞操作僅針對 EmojiSpans 應用。因此,任何其他 span 更改操作與框架中的操作方式相同。
上面是SpannableBuilder類的注釋,感興趣的可以查看源碼,通過以上源碼的分析,emoji2也是對系統(tǒng)表情的顯示做了特殊的處理,我們可以利用emoji2中的這些類來解決我們自定義表情的卡頓問題。
四、解決方案
通過對 EmojiEditableFactory 的深入分析,我們發(fā)現(xiàn)它在 EditTextView中優(yōu)化了對表情 Span 的處理。為了解決自定義表情在中間刪除時的卡頓問題,我們可以復制并修改 EmojiEditableFactory 類和 SpannableBuilder 類。在 SpannableBuilder 中,將isEmojiSpan方法替換為我們自定義表情 span的判斷邏輯。然后在自定義的EditTextView中使用 EmojiEditableFactory,通過應用 setEditableFactory(EmojiEditableFactory.getInstance()); 方法來設置 EmojiEditableFactory為 EditTextView 的 EditableFactory實例。這種操作優(yōu)化了EditTextView,從而有效減少了自定義表情在中間刪除時的卡頓現(xiàn)象。
五、結(jié)語
當遇到性能問題時,Profiler是一個非常有用的工具,可以幫助我們深入分析和定位問題。在本文中,通過 Profiler分析,我們找到了導致卡頓的原因,并通過 emoji2源碼找到了優(yōu)化方案。希望這篇文章能對大家在解決類似問題時提供幫助,讓大家在應用中更好地處理自定義表情的輸入和刪除,提高用戶體驗。