一、背景
在我们的实际应用中,用户在发布文本时,输入大量表情后尝试从中间删除时,会出现明显的卡顿问题。这种操作可能耗时长达2s
,导致用户体验受到严重影响。通过使用 Profiler
分析耗时的方法,我们找到了造成卡顿的原因,并参考了 emoji2
源码提出了解决方案。
二、原因分析
如图所示,当从中间删除一个表情时,耗时方法从SpannableStringBuilder.delete
执行到SpannableStringBuilder.sendSpanChanged
方法,SpannableStringBuilder.sendSpanChanged
方法调用了DynamicLayout$ChangeWatcher.onSpanChanged
,执行了很多次,并且每次调用非常耗时。
点击DynamicLayout$ChangeWatcher.onSpanChanged
方法后看一下对这个方法的分析,从下图中可以看出这个方法被调用了很多次。
根据以上Profiler
的分析,我们无法准确定位问题所在,因此我们决定测试系统表情的表现。测试结果显示,系统表情并没有出现卡顿的问题。因此,我们怀疑可能是我们的自定义表情尺寸过大,尝试压缩表情图标,但仍然出现卡顿现象。
通过分析 Profiler
的输出,我们发现有一个类与 emoji2
中的 androidx.emoji2.viewsintegration.EmojiKeyListener.onKeyDown
方法相关。emoji2
是官方推出的用于适配系统表情的库,我们猜测 emoji2
可能对系统表情进行了特殊优化和处理。查看 emoji2
的源码后,确实发现了对表情输入进行了特殊优化和处理。
三、emoji2的处理
emoji2
源码位于 这里。
emoji2
使用 EmojiSpan
来显示表情,不通过ImageSpan
绘制图片,而是将所有表情封装为字体,并利用 canvas.drawText
进行绘制。虽然系统表情不是图片,但每个表情都由 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
中用于创建可编辑的文本内容,控制 EditTextView
的文本编辑行为。这对于处理复杂的文本内容,如带有特殊格式、表情符号等内容非常有用。通过自定义EditableFactory
,可以优化 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 实现了 SpanWatcher 和 Textwatcher。当前,每次删除/添加操作都会向 DynamicLayout 报告每个 span 的更改。对于每次更改,DynamicLayout 都会执行一些昂贵的计算。例如,如果有 100 个 EmojiSpans,且第一个 span 被删除,DynamicLayout 会接到 99 次关于剩余 span 位置变化的通知。这会导致响应时间的严重延迟。
由于 "android.text.DynamicLayout$ChangeWatcher" 类不是公共类,EmojiEditableFactory 检查观察者是否在类路径中,如果是,则使用经过修改的 Spannable,从而减少对影响 EmojiSpans 的操作对 DynamicLayout 的调用总数。
请参阅 SpannableBuilder。
通过以上注释可以发现,EditableFactory
旨在解决 EmojiSpan
修改时耗时操作的问题。它通过自定义的 SpannableBuilder
来优化操作,从而提高了性能。
@Override
public Editable newEditable(@NonNull final CharSequence source) {
if (sWatcherClass != null) {
return SpannableBuilder.create(sWatcherClass, source);
}
return super.newEditable(source);
}
在 SpannableBuilder
中,通过自定义 WatcherWrapper
对象,能够在 span
发生变化时排除对 EmojiSpan
的影响。WatcherWrapper
对 span
变化事件进行监控,如果检测到是 EmojiSpan
的变化,则阻止 DynamicLayout$ChangeWatcher
对该 span
的触发,仅在编辑结束时通知 ChangeWatcher
。这种优化仅针对 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 上调用 setSpan 函数时,它会检查 mObject 是否是 DynamicLayout$ChangeWatcher 的实例。如果是的话,它会将 mObject 包装成另一个监听器(WatcherWrapper),该监听器实现了相同的接口。
在 WatcherWrapper 的函数在一个 span 更改事件中被触发时,它会检查该 span 是否为 EmojiSpan,并阻止 ChangeWatcher 对该 span 进行触发。WatcherWrapper 只在编辑结束时通知 ChangeWatcher 一次。重要的一点是,这种阻塞操作仅针对 EmojiSpans 应用。因此,任何其他 span 更改操作与框架中的操作方式相同。
上面是SpannableBuilder
类的注释,感兴趣的可以查看源码,通过以上源码的分析,emoji2
也是对系统表情的显示做了特殊的处理,我们可以利用emoji2
中的这些类来解决我们自定义表情的卡顿问题。
四、解决方案
通过对 EmojiEditableFactory
的深入分析,我们发现它在 EditTextView
中优化了对表情 Span
的处理。为了解决自定义表情在中间删除时的卡顿问题,我们可以复制并修改 EmojiEditableFactory
类和 SpannableBuilder
类。在 SpannableBuilder
中,将isEmojiSpan
方法替换为我们自定义表情 span
的判断逻辑。然后在自定义的EditTextView
中使用 EmojiEditableFactory
,通过应用 setEditableFactory(EmojiEditableFactory.getInstance());
方法来设置 EmojiEditableFactory
为 EditTextView
的 EditableFactory
实例。这种操作优化了EditTextView
,从而有效减少了自定义表情在中间删除时的卡顿现象。
五、结语
当遇到性能问题时,Profiler
是一个非常有用的工具,可以帮助我们深入分析和定位问题。在本文中,通过 Profiler
分析,我们找到了导致卡顿的原因,并通过 emoji2
源码找到了优化方案。希望这篇文章能对大家在解决类似问题时提供帮助,让大家在应用中更好地处理自定义表情的输入和删除,提高用户体验。
网友评论