kunyashaw博客主页 关注kunyashaw新博客 关于kunyashaw 转到底部

63、加速android应用(转载)

本文转自 http://www.devtf.cn/?p=1097

 

原文链接 : Speed up your app
原文作者 : UDI COHEN
译文出自 : 开发技术前线 www.devtf.cn。未经允许,不得转载!
译者 : zijianwang90
校对者:
状态 : 完成

几周之前,我在Droidcon NYC上有过一次关于Android性能优化的演讲。

我在这个演讲中花费了大量的时间,因为我想通过真实的例子展现性能问题,以及我是通过什么样的工具去发掘这些问题的。因为时间原因,在演讲中我不得不舍弃一半的内容。在这篇文章中,我会总结在演讲中我所讨论的所有内容,并且给出实例

点此链接进入演讲视频

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

原则

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

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

Systrace

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

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

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

systrace-overview

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

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

systrace-alert

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

systrace-frame

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

systrace-frame-zoomin

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

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

systrace-2-frame

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

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

systrace-2-slice

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

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

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

systrace-2-cpu

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

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

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

Traceview

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

traceview-overview

这个工具可以从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()方法,如下图:

traceview-getview

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

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

但是这个线程在哪儿?

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

traceview-thread

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

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

内存调优 Memory Profiling

Android Studio在最近的更新中给予了我们更加强大的工具去分析性能问题。在底部Android选项中的Memory选项卡,会显示有多大的数据在什么时候被分配到了堆内存之中,他是长成这个样子的:

mem-graph

而当图表中出现一个小的下滑的时候,说明GC回收发生了,他清除了不必要的对象并且腾出了一定的堆空间。而在这张图表的左侧有两个工具供我们使用,Head dump和Allocation Tracker。

Heap dump

为了找出到底是什么正在占用我们的堆内存,我们可以使用左边的heap dump按钮。他会提供一个堆内存占用情况的快照,并且会在Android Studio中打开一个单独的报告界面。

heap-overview

在左侧,我们看到一个图标展示了堆中所有的实例,按照类进行分组。而对于每一个实例,会展示有多少个实例的对象被分配到堆中,以及他们的所占用的空间(Shallow size浅尺寸),以及这些对象在内存中仍然占用的空间,后者告诉了我们多少的内存空间将会被释放如果这些实例被释放。这个工具可以让我们直观的观察处内存是被如何占用的,帮助我们分析我们使用的数据结构和对象之间的关系,以便发现问题并使用更加高效的数据结构,解开和对象之间的关联,并且降低Ratained Memory的占用。而最终目的,就是尽可能的降低我们的内存占用。

回过头来看图表,我们发现MemoryActivity存在39个实例,这对于一个Activity来说有点奇怪。在右边选择其中的一个实例,会在下方看到所有的对这个实例的引用树状列表。

heap-reftree

其中一个是ListenersManager对象中的一个集合。而观察这个activity的其他实例,就会他们都因为这个对象而被保留在了内存之中。这也解释了为什么这些对象占用了如此多的内存:

heap-retained

这个现象就叫做“内存泄露”,我们的activity已经被销毁,但是他们的对象却因为始终被引用着而无法被垃圾回收。我们可以避免这种情况,例如确保这些对象再被销毁后不会被其他对象一直引用着。在我们这个例子中,在Activity被销毁后,ListernesManager并不需要保持着对这些对象的引用。所以解决办法就是在onDestroy()回调方法中移除这些引用。

内存泄露以及其他较大的对象会在堆中占据很多的控件,它们减少着可用内存的同时也频繁的造成垃圾回收。而垃圾回收又回造成CPU的繁忙,而堆内存并不会变得更大,最终就会导致更悲剧的结果发生:OutOfMemoryException内存溢出,并导致程序崩溃。

另外一个更先进的工具就是Eclipse Memory Analyzer Tool (Eclipse MAT):

eclipse-mat

这个工具可以做所有Android Studio可以做的,并且辨别可能出现的内存泄露,以及提供更加高级的搜索功能,例如搜索所有大于2MB的Bitmap实例,或是搜索所有空的Rect对象

另外一个很好的工具是LeakCanary,是一个第三方库,可以观察应用中的对象并且确保它们没有造成泄漏。而如果造成泄漏了,会有一个推送来提醒你在哪里发生了什么。

leakcanary

Allocation Tracker

我们可以在内存图表的左侧找到Allocation Tracker的启动和停止按钮。他会生成一个在一定时间内被生成的所有实例的报告,并且按照类分型分组:

alloc-class

或者按照方法分组:

alloc-method

同时它还能通过美观的可视化界面,告诉我们哪些方法或类拥有最多的实例。

利用这些信息,我们可以找到哪些占用过多内存,引发过多次垃圾回收且又对耗时非常敏感的方法。我们也可以利用这个工具找到很多短命的相同类的实例,从而可以考虑使用对象池的思想去尽量的减少过多的实例被创建。

常见内存小技巧

以下是一些我写代码时候遵循的规律或是技巧:

  • 枚举在性能问题上一直是一个被经常讨论的话题。这里是一个讨论枚举的视频,指出了枚举所消耗的内存空间,这还有一段关于这个视频的讨论,当然其中存在着一些误导。但是回过头来,枚举真的比一般的常量更加占用空间吗?肯定的。但是这一定不好吗?未必。如果你在编写一个library库并且需要很强的类型安全性,那么也许可以使用枚举而非其他办法,例如@IntDef。而如果你只是有一堆的常量,使用枚举也许就不能么明智了。还是那句话,在你做决定之前一定要权衡与取舍。
  • 自动装箱 – 自动装箱是一个从原始数据类型到对象型数据的装箱过程(例如int到Integer)。每当一个原始类型数据被装箱到一个对象类型数据,一个新的对象就产生了(震惊吧。。)。所以如果发生了很多次的自动装箱,势必会加快GC的执行频率,而且自动装箱是很容易被我们忽视的。而解决办法,在使用这些类型的时候尽量一致,如果你在应用中完全使用原始数据类型,那么尽量避免他被无缘无故的自动封装。你可以使用我们上面提到的memery profiling工具去寻找这些过于大量的对象类型数据,也可以通过Traceview去寻找类似Integer.valueOf(),Long.valueOf()这样的方法来判断是否发生了大量不必要的自动封装。
  • HashMap vs ArrayMap / Sparse*Array – 既然提到了自动装箱的问题,那么使用HashMap的话,就需要我们使用对象类型作为键。而如果我们在整个应用中使用的都是基本数据类型的“int”,那么在我们使用HashMap时候就会发生自动装箱,而这时也许我们就可以考虑使用SparseIntArray。而假如我们仍然需要键为对象类型,那么我们可以使用ArrayMap。ArrayMap和HashMap很类似,但是在底层的实现原理却不尽相同,这也会让我们更加高效的使用内存,但要付出一定的性能代价。两种方法都会比HashMap更加节省内存空间,但是相比于HashMap,查询和增删的速度上会有一定的牺牲。当然,除非你具有至少1000条的数据源,否则在运行时也不会对速度造成太大的影响,这也是你使用他们替代HashMap的原因之一。
  • 注意Context – 在我们前面也看到了,Activity是非常容易造成内存泄露的。在Android中,最容易造成内存泄露的当属Activity。并且这些内存泄露会浪费大量的内存,因为他们持有着他们UI中所有的View,而这些View通常会占据很多的控件。在开发过程中的很多操作需要Context,而我通常也会使用Activity来传递。所以一定要搞清楚你对这个Activity做了什么。如果一个引用被缓存起来了,且这个对象的生命周期比你的Activity还要长,那么在我们解除这个引用之前,就会造成内存泄露了。
  • 避免非静态 – 当我们创建非静态内部类,并且初始化它的时候,在其内部会创建一个外部类的隐式引用。而如果内部类的生命周期比外部类还要长,那么外部类也同样会被保留在内存之中,尽管我们已经完全不需要它了。例如,在Activity内创建了一个继承自AsyncTask的内部类,完后在Activity运行的时候启动这个async task,再杀掉Activty。那么这时候这个async task会保持着这个Activity直到执行结束。而解决办法也很简单,不要这么做,尽量使用静态内部类。

监测GPU(GPU Profiling)

在Android 1.4中的一个全新工具,就是可以查看GPU绘制。

gpu-overview

每一条线意味着一帧被绘制出来,而每条线中的不同颜色又代表着在绘制过程中的不同阶段:

  • Draw (蓝色) 代表着View#onDraw()方法。在这个环节会创建/刷新DisplayList中的对象,这些对象在后面会被转换成GPU可以明白的OpenGL命令。而这个值比较高可能是因为view比较复杂,需要更多的时间去创建他们的display list,或者是因为有太多的view在很短的时间内被创建。
  • Prepare (紫色) – 在Lollipop版本中,一个新的线程被加入到了UI线程中来帮助UI的绘制。这个线程叫作RenderThread。它负责转换display list到OpenGL命令并且送至GPU。在这过程中,UI线程可以继续开始处理后面的帧。而在UI线程将所有资源传递给RenderThread过程中所消耗的时间,就是紫色阶段所消耗的时间。如果在这过程中有很多的资源都要进行传递,display list会变得过多过于沉重,从而导致在这一阶段过长的耗时。
  • Process (红色) – 执行Display list中的内容并创建OpenGL命令。如果有过多或者过于复杂的display list需要执行的话,那么这阶段会消耗较长的时间,因为这样的话会有很多的view被重绘。而重绘往往发生在界面的刷新或是被移动出了被覆盖的区域。
  • Execute (黄色) – 发送OpenGL命令到GPU。这个阶段是一个阻塞调用,因为CPU在这里只会发送一个含有一些OpenGL命令的缓冲区给GPU,并且等待GPU返回空的缓冲区以便再次传递下一帧的OpenGL命令。而这些缓冲区的总量是一定的,如果GPU太过于繁忙,那么CPU则会去等待下一个空缓冲区。所以,如果我们看到这一阶段耗时比较长,那可能是因为GPU过于繁忙的绘制UI,而造成这个的原因则可能是在短时间内绘制了过于复杂的view。

在Marshmallow版本中,有更多的颜色被加了进来,例如Measure/Layout阶段,input handling输入处理,以及一些其他的:

gpu-colors-marsh

在使用这些功能之前,你需要在开发者选项中开启GPU rendering(GPU呈现模式分析):

gpu-settings2

接下来我们就可以通过以下这条adb命令得到我们想要得到的所有信息:

 

 

我们可以自己收集这些信息并创建图表。这个命令也会打印出一些其他有用的信息,例如view层级中的层数,display lists的大小等等。在Marshmallow中,我们也会得到更多的信息:

gpu-adb

如果我们需要自动化测试我们的app,那么我们可以自己创建服务器去运行在特定节点执行这些命令(如列表滚动,重度动画等),并观察这些数值的变动。这可以帮助我们找出在哪里出现了性能的下降,并且产品上线之前找到问题的所在。我们也能够通过”framestats”关键字来找到更多更加精确的数据,这里有

posted @ 2015-11-10 14:15  kunyashaw  阅读(224)  评论(0编辑  收藏  举报
回到顶部