天天看点

短信列表滑动慢、卡顿---性能优化总结

前提

以前面试中,面试官问到我优化了那些点。我当时紧张,只回答了说一个日期格式化的优化。所以从这个经历来说,我个人对自己做过的优化没有总结,优化的方法没有仔细地考虑原因,以及这个优化会有什么其他的副作用。今天特意来总结,因为最近学习了JVM内存布局、并发、锁等方面的知识,通过这些知识来更好的理解当初的优化方法。当初优化通过工具分析到卡顿点,找出了优化方法,而没有分析为什么这样优化会达到效果。

短信应用是android原生应用,平台是MTK android o的代码。短信中存储了5000条短信(通过工具批量插入)。

问题:滑动(快慢)短信列表会出现帧率小于60fps.

分析

  1. 首先我们使用systrace工具,查看是否有卡顿现象。测试人员是通过高速摄影机来判断丢祯率。那么我们开发人员通过systrace工具来查看到底是在那个环节耗时比较多。
    短信列表滑动慢、卡顿---性能优化总结
    通过上面的图可以看出,短信应用在滑动时确实耗时偏高。我们查看其中一祯:
短信列表滑动慢、卡顿---性能优化总结

工具的分析结果是建议我们提高适配器的getView绑定数据的效率。从这个建议当中我们无法确切的知道问题出在哪行代码上,之所给我们一个大概的方向。通过systrace工具能够让开发人员检测界面是否有卡顿,给出一个指导意见。在特别小的差别面前,我们人的感知是无法区别的。

  1. Method Profiling 方法分析工具

    因为systrace无法定位到确切的代码。还好我们有Method Profiling 工具可以分析方法的执行情况。理论上可以通过打印时间戳来追踪耗时方法,但是这个方法比较费事。

    在android device monitor工具中点击“start Method Profiling”,开始检测->滑动短信列表->“stop Method Profiling”,

    短信列表滑动慢、卡顿---性能优化总结
    那么上面的图中,我们如何找到耗时方法呢?可以在过滤器中输入我们的包名,这样,我们的耗时类就在最上面了。
    短信列表滑动慢、卡顿---性能优化总结
    第一个耗时高的方法是Conversation.from();我们再点击进去,发现是Conversation中的内部类Cache.put方法耗时非常多。而Cache.put方法是间接调用了ConcurrentHashMap.put方法。HashMap添加元素的步骤是,根据key的hash散列码来放到桶中。如果桶中没有元素则将元素放到这里,如果有值,则比较是否是同一个元素,是则更新,否则在后面添加(分离链接法)。最终发现是equals方法耗时较多。那么我看一下Conversation的equals方法是如何实现的。
/*
     * The primary key of a conversation is its recipient set; override
     * equals() and hashCode() to just pass through to the internal
     * recipient sets.
     */
    @Override
    public synchronized boolean equals(Object obj) {
        try {
            Conversation other = (Conversation) obj;
            /** M: contains method of ConcurrentHashMap use equals to judge element existence.
             *  we take wap push message and cb message into seperate thread.
             *  they may be has the same recipients as the common thread, but different.
             */
            return (mRecipients.equals(other.mRecipients)) && (mType == other.mType);
        } catch (ClassCastException e) {
            return false;
        }
    }

           

这里面有synchronized关键字,那么是不是这个导致慢呢?首先在并发这块,synchronized关键字是重量级锁,一定要获取锁线程才能进入执行。即使单线程执行,也会在这个地方卡顿。所以我们尝试将synchronized关键字去除,看效果怎样。

  • 改了以后会有点效果,在contains()方法中从开始的占用85%的时间降低到61%.

    还没有达到我们的预期,还有一个方法(mRecipients.equals(other.mRecipients)) ,这个中我们看一下是如何实现equals方法的。

  • mRecipients是一个ContactList,ContactList继承自Arraylist,在ContactList的equals方法中,他循环遍历A中的接收者是否在B中,所以这个比较耗时。
  • 看它的注释中有 wap push message and cb message,wap push message这个在短信中已经没有了,而cb message我们已经分离了在另外一个apk中。所以我们可以把比较条件改为(mThreadId == other.mThreadId) ,通过测试,eqauls方法在contains()方法中从开始的占用61%的时间降低到15%.
短信列表滑动慢、卡顿---性能优化总结

那么优化后我们发现还有下面的问题。

短信列表滑动慢、卡顿---性能优化总结

Conversation.from()方法的耗时比较长。那么我们最终下去,看到底什么地方出了问题。

定位到有一行代码是删除字符串中的特色字符:

workingNumberOrEmail = workingNumberOrEmail.replaceAll(" ", "")
                    .replaceAll("-", "").replaceAll("[()]", "");
           

上面代码的意思是将字符串中的空格,横杠,小括号、中括号去除。但是它调用了三次replaceAll方法。所以我们能否让它调用一次来达到效果。replaceAll的第一个String参数是可以为正则表达式。而代码中没有用到这个特性。所以我们的可以如下修改。

workingNumberOrEmail = workingNumberOrEmail.replaceAll(
                   "[\\ ]*[\\-]*[\\[]*[\\(]*[\\)]*[\\]]*", "");
           

这样我们就完成了优化。

代码中另外还有一个点。程序中通过一个异步线程去更新联系人数据,任务放在runnable对象中,再将runnable对象发送给任务线程,但是任务线程通过ArrayList来存储,运行时从ArrayList中去除第一个元素,增加时直接调用add方法。源码:

private static class TaskStack {
             Thread mWorkerThread;
            private final ArrayList<Runnable> mThingsToLoad;
...
                              if (mThingsToLoad.size() > 0) {
                                    r = mThingsToLoad.remove(0);
...
             public void push(Runnable r) {
                synchronized (mThingsToLoad) {
                     mThingsToLoad.add(r);
...
           

而我们知道,ArrayList在添加元素时,将给数组扩容,然后将原数据拷贝到新空间中,再将新数据放在末尾。删除时申请性空间,将数组拷贝的新空间中。无论是删除还是添加,都需要进行整个数组的移动,所以会带来很大的计算量。其实在这种情况下我们可以使用queue的数据结构可以很好的解决这个数组拷贝的问题。比如android中Handler的消息就是通过队列来组织的(Handler消息)

3.

测试定位问题。

工具使用

卡顿原因分析。解决策略。

更好的方法

多线程、并发、锁

硬件加速