编程开源技术交流,分享技术与知识

网站首页 > 开源技术 正文

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

wxchong 2024-08-25 16:44:54 开源技术 8 ℃ 0 评论

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);
        }

上面代码是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());方法来设置EmojiEditableFactoryEditTextViewEditableFactory实例。这种操作优化了EditTextView,从而有效减少了自定义表情在中间删除时的卡顿现象。

05

结语

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


作者:狐友张乙龙

来源-微信公众号:搜狐技术产品

出处:https://mp.weixin.qq.com/s/NNHtCZgcLsTpcrFQVghoEw

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表