Android应用性能优化实践

November 15, 2015

CSDN移动将持续为您优选移动开发的精华内容,共同探讨移动开发的技术热点话题,涵盖移动应用、开发工具、移动游戏及引擎、智能硬件、物联网等方方面面。如果您想投稿,或寻求《近匠》报道,请发送邮件至tangxy#csdn.net(请把#改成@)。 


本文出自:UDI COHEN,作者:Udi Cohen译文出自:开发技术前线,译者:Zijian Wang

几周前,我在Droidcon NYC上有过一次关于Android性能优化的演讲。我在这个演讲中花费了大量的时间,因为我想通过真实的例子展现性能问题,以及我是通过什么样的工具去发掘这些问题的。因为时间原因,在演讲中我不得不舍弃一半的内容。在这篇文章中,我会总结在演讲中我所讨论的所有内容,并且给出实例。( 点击链接观看演讲视频,需自备梯子)

现在,我们来逐一讨论我在演讲中提及的一些重点内容,希望我的阐述足够的清晰。首先,在我进行性能优化的时候我遵循如下原则:

原则

每当我遇到性能问题,或者尝试发现性能问题的时候,我会遵循如下原则:

  • 坚持性能测试——不要用你的眼睛去优化性能。也许在你盯着同一个动画看了几次之后,你会开始相信它运行得越来越流畅了。数据不会说谎。在优化你的代码前后,使用我们将要介绍的一系列工具,去多次地测试你的App到底性能几何。
  • 使用低端设备——如果你想要你想暴露你应用的性能问题,低端设备往往会更加的容易。性能强大的设备往往不会太在意你应用上面的一些优化问题,且不是所有用户都在使用这些旗舰设备。
  • 权衡——性能的优化始终围绕着权衡这两个字。你在某一个点上的优化可能会造成另一点上出现问题。在很多情况下,你会花大量的时间寻找并解决这些问题,但造成这些问题的原因也可能使因为例如bitmaps的质量,或是你没有使用正确的数据结构去存储你的数据。所以你要时刻准备好作出一定的牺牲。

Systrace

Systrace是一个非常好但却有可能被你忽视的工具,这是因为开发者们往往不确定Systrace能够为他们提供什么样的信息。

Systrace会展示一个运行在手机上程序状况的概览。这个工具提醒了我们手机其实是一个可以在同一时间完成很多工作的电脑。在最近的一次SDK更新中,这个工具在数据分析能力上得到了提升,用以帮助我们寻找性能问题之所在。

下面让我们来看看Systrace长什么样子:


你可以通过Android Device Monitor Tool或命令行来生成Systrace文件,想了解更多猛戳此处

在视频中,我向大家介绍了Systrace中不同区域的功能。当然最有趣的还是Alerts和Frames两栏,它们展示了通过手机来的数据而生成出来的可视化分析结果。让我们来选择最上方的alerts瞧瞧:


这个警告指出了,有一个View#draw()方法执行了比较长的时间。我们可以在下面看到问题的描述,链接,甚至是相关的视频。下面我们看Frames这一行,可以看到这里展示了被绘制出来的每一帧,并且用绿、黄、红三颜色来区分它们在绘制时的性能。我们选一个红色帧来瞅瞅:


在最下方,我们看到了与这一帧所相关的一些警告。在这三个警告中,有一个是我们上面所提到的(View#draw())。接下来我们在这一帧处放大并在下方展开“Inflation during ListView recycling”这条警告:


我们可以看到警告部分的总耗时,32毫秒,远高于了我们对保障60fps所需的16毫秒绘制时间。同时还有更多的ListView每个条目的绘制时间,大约是6毫秒每个条目,总共五个。而Description描述项中的内容会帮助我们理解问题,甚至提供问题的解决方案。回到我们上一张图片,我们可以在“inflate”这一个块区处放大,并且观察到底是哪些View在被填充过程中耗时比较严重。

下面是另外一个渲染过慢的实例:


在选择了某一帧之后,我们可以按“m”键来高亮这一帧,并且在上方看到了这一部分的耗时,如图,我们看到了这一阵的绘制总共耗时超过19毫秒。而当我们展开这一帧唯一的一个警告时,我们发现了“Scheduling delay”这条错误。

Scheduling delay(调度延迟)的意思就是一个线程在处理一块运算的时候,在很长一段时间都没有被分配到CPU上面做运算,从而导致这个线程在很长一段时间都没有完成工作。我们选择这一帧中最长的一块,从而得到更加详细的信息:


在红框区域内,我们看到了“Wall duration”,他代表着这一区块的开始到结束的耗时。之所以叫作“Wall duration”,是因为他就像是墙上的一个时钟,从线程的一开始就为你计时。

但是,CPU Duration一项中显示了实际CPU在处理这一区块所消耗的时间。

很显然,两个时间的差距还是非常大的。整个区块耗时18毫秒,而在这之中CPU只消耗了4毫秒的时间去运算。这就有点奇怪了,所以我们应该看一下在这整个过程之中,CPU去干吗了。


可以看到,所有四个线程都非常的繁忙。

选择其中的一个线程会告诉我们是哪个程序在占用他,在这里是一个包名为com.udinic.keepbusyapp的程序。在这里,由于另外一个程序占用CPU,导致了我们的程序未能获得足够的CPU资源。

但是这种情况其实是暂时的,因为被其他后台应用占用CPU的情况并不多见(- -),但仍有其他应用的线程或是主线程占用CPU。而Traceview也只能为我们提供一个概览,他的深度是有限的。所以要找到我们app中到底是什么让我们的CPU繁忙,我们还要借助另一个工具——Traceview。

Traceview

Traceview是一个性能测试工具,展示了所有方法的运行时间。下面让我们来瞅瞅它是啥样的:


这个工具可以从Android Device Monitor中打开也可以通过代码打开。更多的消息信息请看这里

下面让我们来看看每一列的含义:

  • Name——方法名,以及他们在上面图表中所对应的颜色。
  • Inclusive CPU Time——CPU在处理这个方法以及所有子方法(如被他调用的所有方法)的总耗时。
  • Exclusive CPU Time——CPU在处理这一个单独方法的总耗时。
  • Inclusive/Exlusive Real Time——从方法的开始执行到执行结束的总耗时,和Systrace中的“Wall duration”类似。
  • Calls+Recursion——这个方法被调用的次数,以及被递归调用的次数。
  • CPU/Real time per Call——在处理这个方法时的CPU耗时的平均值以及实际耗时的平均值。另外的列展示了这个方法所有调用的累计耗时。

我打开一个滑动不太顺滑的应用。开启记录,滑动一点后停止记录。展开getView()方法,如下图:


这个方法被调用了12次,每次CPU会消耗3毫秒左右,但是每次调用的总耗时却高达162毫秒!绝对有问题啊!

而看看这个方法的children,我们可以看到这其中的每个方法在耗时方面是如何分布的。Thread.join()方法战局了98%的inclusive real time。这个方法在等待另一个线程结束的时候被调用。在Children中另外一个方法就是Tread.start()方法,而之所以整个方法耗时很长,我猜测是因为在getView()方法中启动了线程并且在等待它的结束。

但是这个线程在哪儿?

我们在getView()方法中并不能看到这个线程做了什么,因为这段逻辑不在getView()方法之中。于是我找到了Thread.run()方法,就是在线程被创建出来时候所运行的方法。而跟随这个方法一路向下,我找到了问题的元凶。


我发现了BgService.doWork()方法的每次调用花费了将近14毫秒,并且有四十个这东西!而且getView()中还有可能调用多次这个方法,这就解释了为什么getView()方法执行时间如此之长。这个方法让CPU长时间的保持在了繁忙状态。而看看Exclusive CPU time,我们可以看到他占据了80%的CPU时间!此外,根据Exclusive CPU time排序,可以帮我们更好的定位那些耗时很长的方法,而他们很有可能就是造成性能问题的罪魁祸首。

关注这些耗时方法,例如getView(),View#onDraw()等方法,可以很好的帮助我们寻找为什么应用运行缓慢的原因。但有些时候,还会有一些其他的东西来占用宝贵的CPU资源,而这些资源如果被运用在UI的绘制上,也许我们的应用会更加流畅。Garbage Collector垃圾回收机制会不时的运行,回收那些没用的对象,通常来讲这不会影响我们在前台运行的程序。但如果GC被运行的过于频繁,他同样可以影响我们应用的执行效率。而我们该如何知道回收的是否过于频繁了呢…