01
背景
在我们的实际应用中,用户在发布文本时,输入大量表情后尝试从中间删除时,会出现明显的卡顿问题。这种操作可能耗时长达2s,导致用户体验受到严重影响。通过使用Profiler分析耗时的方法,我们找到了造成卡顿的原因,并参考了emoji2源码提出了解决方案。
02
原因分析
如图所示,当从中间删除一个表情时,耗时方法从SpannableStringBuilder.delete执行到SpannableStringBuilder.sendSpanChanged方法,SpannableStringBuilder.sendSpanChanged方法调用了DynamicLayout$ChangeWatcher.onSpanChanged,执行了很多次,并且每次调用非常耗时。
点击DynamicLayout$ChangeWatcher.onSpanChanged方法后看一下对这个方法的分析,从下图中可以看出这个方法被调用了很多次。
根据以上Profiler的分析,我们无法准确定位问题所在,因此我们决定测试系统表情的表现。测试结果显示,系统表情并没有出现卡顿的问题。因此,我们怀疑可能是我们的自定义表情尺寸过大,尝试压缩表情图标,但仍然出现卡顿现象。
通过分析 Profiler 的输出,我们发现有一个类与 emoji2 中的 androidx.emoji2.viewsintegration.EmojiKeyListener.onKeyDown 方法相关。emoji2 是官方推出的用于适配系统表情的库,我们猜测 emoji2 可能对系统表情进行了特殊优化和处理。查看 emoji2 的源码后,确实发现了对表情输入进行了特殊优化和处理。
03
emoji2的处理
emoji2源码位于https://github.com/androidx/androidx/tree/androidx- main/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中的这些类来解决我们自定义表情的卡顿问题。
04
解决方案
通过对EmojiEditableFactory的深入分析,我们发现它在EditTextView中优化了对表情Span的处理。为了解决自定义表情在中间删除时的卡顿问题,我们可以复制并修改EmojiEditableFactory类和SpannableBuilder类。在SpannableBuilder中,将isEmojiSpan方法替换为我们自定义表情span的判断逻辑。然后在自定义的EditTextView中使用EmojiEditableFactory,通过应用setEditableFactory(EmojiEditableFactory.getInstance());方法来设置EmojiEditableFactory为EditTextView的EditableFactory实例。这种操作优化了EditTextView,从而有效减少了自定义表情在中间删除时的卡顿现象。
05
结语
当遇到性能问题时,Profiler是一个非常有用的工具,可以帮助我们深入分析和定位问题。在本文中,通过Profiler分析,我们找到了导致卡顿的原因,并通过emoji2源码找到了优化方案。希望这篇文章能对大家在解决类似问题时提供帮助,让大家在应用中更好地处理自定义表情的输入和删除,提高用户体验。
作者:狐友张乙龙
来源-微信公众号:搜狐技术产品
出处:https://mp.weixin.qq.com/s/NNHtCZgcLsTpcrFQVghoEw
本文暂时没有评论,来添加一个吧(●'◡'●)