美文网首页
Android自定义表情删除时卡顿问题的定位与优化

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

作者: Allenlll | 来源:发表于2024-04-22 10:55 被阅读0次

    一、背景

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

    二、原因分析

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

    image.png

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

    image.png

    根据以上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的影响。WatcherWrapperspan变化事件进行监控,如果检测到是 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中的这些类来解决我们自定义表情的卡顿问题。

    四、解决方案

    通过对 EmojiEditableFactory 的深入分析,我们发现它在 EditTextView中优化了对表情 Span 的处理。为了解决自定义表情在中间删除时的卡顿问题,我们可以复制并修改 EmojiEditableFactory 类和 SpannableBuilder 类。在 SpannableBuilder 中,将isEmojiSpan方法替换为我们自定义表情 span的判断逻辑。然后在自定义的EditTextView中使用 EmojiEditableFactory,通过应用 setEditableFactory(EmojiEditableFactory.getInstance()); 方法来设置 EmojiEditableFactoryEditTextViewEditableFactory实例。这种操作优化了EditTextView,从而有效减少了自定义表情在中间删除时的卡顿现象。

    五、结语

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

    相关文章

      网友评论

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

          本文链接:https://www.haomeiwen.com/subject/fiuvxjtx.html