Android自定义表情删除时卡顿问题的定位与优化

a4ecd6a925612ef59ac2babf4d5b5298.jpeg

c216a55c661c8832567e3dcc96831820.gif

本文字数:2001

预计阅读时间:15分钟

01

背景

在我们的实际应用中,用户在发布文本时,输入大量表情后尝试从中间删除时,会出现明显的卡顿问题。这种操作可能耗时长达2s,导致用户体验受到严重影响。通过使用 Profiler 分析耗时的方法,我们找到了造成卡顿的原因,并参考了 emoji2 源码提出了解决方案。

02

原因分析

如图所示,当从中间删除一个表情时,耗时方法从SpannableStringBuilder.delete执行到SpannableStringBuilder.sendSpanChanged方法,SpannableStringBuilder.sendSpanChanged方法调用了DynamicLayout$ChangeWatcher.onSpanChanged,执行了很多次,并且每次调用非常耗时。

a4a09f72c5f0b1a2fd18b1f9c1798d0c.jpeg

点击DynamicLayout$ChangeWatcher.onSpanChanged方法后看一下对这个方法的分析,从下图中可以看出这个方法被调用了很多次。d013c8f11bea20ac1eeb1eda308be9a7.jpeg

根据以上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);
        }

上面代码是WatcherWrapperonSpanChanged的代码,我们可以参考这个方法,只需要把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 EditTextViewEditableFactory 实例。这种操作优化了 EditTextView,从而有效减少了自定义表情在中间删除时的卡顿现象。

05

结语

当遇到性能问题时,Profiler 是一个非常有用的工具,可以帮助我们深入分析和定位问题。在本文中,通过 Profiler 分析,我们找到了导致卡顿的原因,并通过 emoji2 源码找到了优化方案。希望这篇文章能对大家在解决类似问题时提供帮助,让大家在应用中更好地处理自定义表情的输入和删除,提高用户体验。
  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值