美文网首页
Memory of a TraceView practice

Memory of a TraceView practice

作者: mrz_233333 | 来源:发表于2018-03-21 10:46 被阅读0次

    去年二月项目中有一个跳转到一个页面,如果这个页面超过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的结果图就不补了,仅仅是对去年二月的优化做次回顾,懒得重新复现上图了)

    相关文章

      网友评论

          本文标题:Memory of a TraceView practice

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