前提
以前面试中,面试官问到我优化了那些点。我当时紧张,只回答了说一个日期格式化的优化。所以从这个经历来说,我个人对自己做过的优化没有总结,优化的方法没有仔细地考虑原因,以及这个优化会有什么其他的副作用。今天特意来总结,因为最近学习了JVM内存布局、并发、锁等方面的知识,通过这些知识来更好的理解当初的优化方法。当初优化通过工具分析到卡顿点,找出了优化方法,而没有分析为什么这样优化会达到效果。
短信应用是android原生应用,平台是MTK android o的代码。短信中存储了5000条短信(通过工具批量插入)。
问题:滑动(快慢)短信列表会出现帧率小于60fps.
分析
- 首先我们使用systrace工具,查看是否有卡顿现象。测试人员是通过高速摄影机来判断丢祯率。那么我们开发人员通过systrace工具来查看到底是在那个环节耗时比较多。 通过上面的图可以看出,短信应用在滑动时确实耗时偏高。我们查看其中一祯:
工具的分析结果是建议我们提高适配器的getView绑定数据的效率。从这个建议当中我们无法确切的知道问题出在哪行代码上,之所给我们一个大概的方向。通过systrace工具能够让开发人员检测界面是否有卡顿,给出一个指导意见。在特别小的差别面前,我们人的感知是无法区别的。
-
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.
测试定位问题。
工具使用
卡顿原因分析。解决策略。
更好的方法
多线程、并发、锁
硬件加速