让你的EditText删除表情比微信更高效--记一次android性能分析优化实战

return false;
}
SpData[] spDatas = getSpDatas();
for (SpData spData : spDatas) {
if (selectionStart == spData.end) {
Editable editable = getText();
editable.delete(spData.start, spData.end);
return true;
}

}
return false;
}

SpData中保存了表情对应的文本的开始位置和结束位置,直接使用Editable.delete()删除

问题定位

粗略定位

先打Log粗略定位下问题,把自己觉得可能会造成卡顿的地方都加了log,发现卡顿的罪魁祸首就是editable.delete(spData.start, spData.end);这一行

精确定位

再准备顺藤摸瓜找到卡顿的真正元凶,但是代码跳着跳着就到SpannableStringBuilderTextView这两个超大的类里去了,在哪卡的还不知道自己就绕晕了,只能靠性能检测工具先具体定位到问题再进一步分析了

这里用到了AndroidStudio3.0自带的Android Profiler,具体的用法可以看AndroidStudio3.0 Android Profiler分析器

FlameChart

先通过火焰图看看最耗时的调用栈是哪一条

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

图上可知ChangeWatcher.onSpanChanged()->ChangeWatcher.reflow()->DynamicLayout.reflow()->StaticLayout.generate()这条调用栈最为耗时

CallChart

再看看调用顺序图

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

  • ChangeWatcher.onSpanChanged()被调用了多次,会多次调用DynamicLayout.reflow()
  • DynamicLayout.reflow()中会调用多次StaticLayout.generate()

有一点疑问,我看DynamicLayout源码,每次reflow()应该只会调用一次StaticLayout.generate()而且都是在主线程,CallChat却显示了多次,而且调用次数没看出啥规律,不知道有没有大神可以帮我解下惑

BottomUp

其实通过上面两步基本已经定位到问题了,再在BottomUp的表格中确认一下

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

StaticLayout.generate()中有这样一段代码,这下实锤了

if (spanned == null) {
spanEnd = paraEnd;
int spanLen = spanEnd - spanStart;
measured.addStyleRun(paint, spanLen, fm);
} else {
spanEnd = spanned.nextSpanTransition(spanStart, paraEnd,
MetricAffectingSpan.class);
int spanLen = spanEnd - spanStart;
MetricAffectingSpan[] spans =
spanned.getSpans(spanStart, spanEnd, MetricAffectingSpan.class);
spans = TextUtils.removeEmptySpans(spans, spanned, MetricAffectingSpan.class);
measured.addStyleRun(paint, spans, spanLen, fm);
}

问题分析

TextView这块相关代码比较复杂就不一行行分析了直接说结论

  • ChangeWatcher实现了SpanWatcher接口,它是用来监听TextView中Span发生变化的
  • 当从中间删除一个表情,被删除表情后面的所有的ImageSpan位置都发生了变化,每个ImageSpan变化都会触发一次ChangeWatcher.onSpanChanged()->ChangeWatcher.reflow()->DynamicLayout.reflow()->StaticLayout.generate()这样的调用栈

这就是为什么要从中间删除才会卡顿,从最后删不会的原因

解决问题

通过以上的结论可以知道,要解决从中间删除表情卡顿的关键在于如何让ChangeWatcher.onSpanChanged()不多次调用

第一阶段方案

之前文章中提到过SpanWatcher继承于NoCopySpan接口,在产生一个新的Spannable对象时NoCopySpan不会被复制,而ChangeWatcher则实现了SpanWatcher,所以它也不会被复制,灵光一闪一个解决方案出来了

private boolean onDeleteEvent() {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
if (selectionEnd != selectionStart) {
return false;
}
SpData[] spDatas = getSpDatas();
for (int i = 0; i < spDatas.length; i++) {
SpData spData = spDatas[i];
if (selectionStart == spData.end) {
Editable editable = getText();
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(editable);
spannableStringBuilder.delete(spData.start, spData.end);
GifTextUtil.setText(this, spannableStringBuilder);
setSelection(spData.start);
return true;
}

}
return false;
}

  • 之前是直接删除
  • 新的方案是先取出文本内容,复制给新的SpannableStringBuilder,在设置到输入框之前删除表情,因为此时新的SpannableStringBuilder中并不包含ChangeWatcher所以不会多次调用ChangeWatcher.onSpanChanged()
  • 删除表情后再将SpannableStringBuilder设置给EditText
  • 最后设置光标位置

完成这一系列操作之后demo一跑,删除果然变流畅了,当时心里那个高兴啊,竟然做个功能可以比微信实现的还好那么一点

输入法问题

然而总是帅不过三秒。没过一会就发现了新的问题。

  • 百度输入法只能一个个删除表情,而不能长按一溜删下来(搜狗是可以的。。。)

刚战完微信又来个百度输入法,写个表情输入功能咋跟打游戏里的boss一样呢。本来自信满满要找出百度输入法的bug,但是从来没接触过输入法相关的开发工作,跑了跑google的输入法的sample还发现官方的输入法一样有问题,又挣扎了几下翻了翻源码,最终还是无功而返

虽然没解决输入法的问题,不过也不是完全没有收获

case DO_SEND_KEY_EVENT: {
InputConnection ic = getInputConnection();
if (ic == null || !isActive()) {
Log.w(TAG, “sendKeyEvent on inactive InputConnection”);
return;
}
ic.sendKeyEvent((KeyEvent)msg.obj);
onUserAction();
return;
}

  • W/IInputConnectionWrapper: sendKeyEvent on inactive InputConnection连续删除时会出现这样的log,搜狗输入法也会出现,估计是百度输入法在出现这样的情况时就把删除按钮的触摸事件给中断了
  • 出现上面log的原因是因为InputConnection在setText()时需要被重新创建,而第二次删除时InputConnection可能还没创建好或者IInputConnectionWrapper没处于激活状态

完全版的解决方案

跟输入法死磕几天未果正愁着呢,突然想到谷歌在android 8.0发布的时候推出了一个Emoji表情库,Emoji出现在TextView中逃不出也用的是ImageSpan,想看看谷歌会不会也有从中间开始删除表情卡顿的feature,就去找了下这个库的demo,一跑发现demo中不管从末尾还是从中间删都不会卡。顿时燃起了解决这个问题的希望,看完代码才发现解决方案如此简单

之前定位到问题在于ChangeWatcher,但它是一个内部类,自己想的法子都是在外部怎么避免ChangeWatcher.onSpanChanged()被调用,谷歌直接简单粗暴的用反射获取了ChangeWatcher的Class对象,在setSpan()的时候发现如果是ChangeWatcher就把它包装在新的WatcherWrapper中,所有的操作都通过WatcherWrapper中转,就可以随心所欲控制onSpanChanged了

自定义一个Editable.Factory

  • 用反射获取了DynamicLayout.ChangeWatcher的Class对象
  • 将Class对象作为新的SpannableStringBuilder的构造参数传入

final class ImageEditableFactory extends Factory {

private static final Object sInstanceLock = new Object();
@GuardedBy(“sInstanceLock”)
private static volatile Factory sInstance;
@Nullable
private static Class<?> sWatcherClass;

@SuppressLint({“PrivateApi”})
private ImageEditableFactory() {
try {
String className = “android.text.DynamicLayout$ChangeWatcher”;
sWatcherClass = this.getClass().getClassLoader().loadClass(className);
} catch (Throwable var2) {
;
}

}

public static Factory getInstance() {
if (sInstance == null) {
Object var0 = sInstanceLock;
synchronized (sInstanceLock) {
if (sInstance == null) {
sInstance = new ImageEditableFactory();
}
}
}

return sInstance;
}

public Editable newEditable(@NonNull CharSequence source) {
return (Editable) (sWatcherClass != null ? SpannableBuilder.create(sWatcherClass, source)
: super.newEditable(source));
}
}

自定义一个SpannableStringBuilder

  • 定义一个WatcherWrapper将ChangeWatcher包装起来,所有之前对ChangeWatcher的调用都通过WatcherWrapper完成
  • 这里onSpanChanged就对ImageSpan特殊处理了,直接返回不调用ChangeWatcher.onSpanChanged
  • 覆盖SpannableStringBuilder的相关方法
  • 对和Span相关的方法特殊处理

贴上WatcherWrapper 的代码,自定义SpannableStringBuilder代码就不贴了,大家可以去项目里找com.sunhapper.spedittool.view.SpannableBuilder自己看

private static class WatcherWrapper implements TextWatcher, SpanWatcher {

private final Object mObject;
private final AtomicInteger mBlockCalls = new AtomicInteger(0);

WatcherWrapper(Object object) {
this.mObject = object;
}

@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
((TextWatcher) mObject).beforeTextChanged(s, start, count, after);
}

@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
((TextWatcher) mObject).onTextChanged(s, start, before, count);
}

@Override
public void afterTextChanged(Editable s) {
((TextWatcher) mObject).afterTextChanged(s);
}

@Override
public void onSpanAdded(Spannable text, Object what, int start, int end) {
if (mBlockCalls.get() > 0 && isImageSpan(what)) {
return;

最后

总而言之,Android开发行业变化太快,作为技术人员就要保持终生学习的态度,让学习力成为核心竞争力,所谓“活到老学到老”只有不断的学习,不断的提升自己,才能跟紧行业的步伐,才能不被时代所淘汰。

在这里我分享一份自己收录整理上述技术体系图相关的几十套腾讯、头条、阿里、美团等公司20年的面试题,把技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,这里以图片的形式给大家展示一部分。

还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!
g-uqobWYBG-1715392953358)]

[外链图片转存中…(img-KWoRbMBE-1715392953359)]

还有高级架构技术进阶脑图、Android开发面试专题资料,高级进阶架构资料 帮助大家学习提升进阶,也节省大家在网上搜索资料的时间来学习,也可以分享给身边好友一起学习。
《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》点击传送门,即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值