去年二月项目中有一个跳转到一个页面,如果这个页面超过200个表情,打开页面会变得非常卡(加载2~4s才能打开)的一个问题。
这套逻辑是很久之前的Legacy code,不知道经过了几个人手...但直到现在才发现问题...
Whatever,问题出现了肯定要解决,先分析这段Legacy code的具体逻辑...
加载表情步骤是
1.使用以下正则来匹配String中表情。
String string = "\\[(.+?)\\]";
2.将表情匹配出来后,根据匹配后字符找到id,使用id在项目中自带的表情resource中查找到具体表情的png。
3.使用SpannableString,采用ImageSpan将这些png依次嵌入SpannableString文本中。
4.setText(SpannableString)来展示。
一开始以为是接口请求较慢问题,后来监测发现不管多少个表情,接口Http Req/Resp都基本稳定在50ms以内。
看来只能是项目中代码的问题。
由于项目中整个加载解析逻辑不仅仅只有表情,还有解析股票代码,链接,@某人的跳转等等。暂时无法从庞杂的代码中分析出到底那处占用了时间。
遂想到使用TraceView来分析到底哪个方法执行过长。
第一次优化
第一次TraceView发现,是正则的一个matcher方法在表情多的(200+)时候,执行了上w次。正是这个方法的总计incl cpu(?)执行时间超过了3000ms。
那必然是正则匹配里的处理逻辑有问题。查看了一下代码,在匹配时有多处正则匹配逻辑,重点Debug 对表情的正则匹配逻辑。
private static void dealSpannableString(Context context, SpannableString spannableString, Pattern patten, int start) {
Matcher matcher = patten.matcher(spannableString);
while (matcher.find()) {
String key = matcher.group();
if (matcher.start() < start) {
continue;
}
int id = Expressions.getIdAsName(key);
if (id != 0) {
Drawable drawable = context.getResources().getDrawable(id);
// drawable.setBounds(0, 0, UIUtils.Dp2Px(context, 30),
// UIUtils.Dp2Px(context, 30));
drawable.setBounds(0, 0, drawable.getIntrinsicWidth() * 3 / 5, drawable.getIntrinsicHeight() * 3 / 5);
// ImageSpan imgSpan = new ImageSpan(drawable);
int end = matcher.start() + key.length();
// spannableString.setSpan(imgSpan, matcher.start(), end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
spannableString.setSpan(new VerticalImageSpan(drawable), matcher.start(), end, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
if (end < spannableString.length()) { //如果整个字符串还未验证完,则继续。。
dealSpannableString(context, spannableString, patten, end);
}
break;
}
}
}
跟踪代码,才发现解析这里使用了递归。dealSpannableString,又调用了自身。
然而这个递归是毫无必要的,200个表情在第一次递归时,递归剩下的199,第二次递归剩下的198...
直接导致解析N个表情所需的正则匹配次数为(n+1)*n/2。然而其实真正需要匹配的次数应该只是n。所以N=200时,其实方法执行次数上扩大了100倍,总计需要3000+ms,其实应该只需要30ms左右。
去掉递归之后:
while (matcher.find()) {
String key = matcher.group();
int id = Expressions.getIdAsName(key);
if (id != 0) {
Drawable drawable = context.getResources().getDrawable(id);
drawable.setBounds(0, 0, drawable.getIntrinsicWidth() * 3 / 5, drawable.getIntrinsicHeight() * 3 / 5);
ImageSpan imgSpan = new CenterVerticalEmojiExpressionImageSpan(drawable);
spannableString.setSpan(imgSpan, matcher.start(),
matcher.end(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
运行,发现确实打开页面速度得到了明显提升。
但是在反复切换打开页面时,感觉似乎还是有一点迟钝,虽然没有3~5秒这么夸张,但是有时会卡顿1秒钟左右。
第二次优化
找到一个打开延迟有1秒左右的页面进行测试。这个页面文本内容比较丰富,有表情,有需要解析跳转的股票代码和链接,还有@某人的标记。
找不到头绪,依然采用TraceView。
看了看这1秒内到底又是哪些个方法执行最多。
看了数据发现... 竟然是setText()。
Debug之~
终于发现原来在for循环匹配股票代码,链接,@标识的时候,setText写到了for循环体内,而且是每块解析的for循环体内,导致这些数据一多,setText执行了非常多次。。。
而setText其实还算是一个比较重量级的方法,里面还调用了
requestLayout();
invalidate();
等等。
修改为将在for循环体内将spannableString拼接起来,在for循环外只进行一次setText。
运行之... hmmmm ... 页面跳转变得非常流畅,感觉不到迟钝了。
总结
性能优化还是要熟练掌握工具的使用哇~ 一开始看TraceView的分析结果,有点不敢相信,总感觉是不是测错了。一个matcher方法,一个setText方法,怎么可能呢?
仔细分析发现还真的是这两个方法。看来数据真的是不会撒谎。(TraceView的结果图就不补了,仅仅是对去年二月的优化做次回顾,懒得重新复现上图了)