Android性能优化之运算与工具

Intro to Compute and Memory Problems

Android中的Java代码会需要经过编译优化再执行的过程。代码的不同写法会影响到Java编译器的优化效率。例如for循环的不同写法就会对编译器优化这段代码产生不同的效率,当程序中包含大量这种可优化的代码的时候,运算性能就会出现问题。想要知道如何优化代码的运算性能就需要知道代码在硬件层的执行差异。

Slow Function Performance

如果你写了一段代码,它的执行效率比想象中的要差很多。我们需要知道有哪些因素有可能影响到这段代码的执行效率。例如:比较两个float数值大小的执行时间是int数值的4倍左右。这是因为CPU的运算架构导致的.

虽然现代的CPU架构得到了很大的提升,也许并不存在上面所示的那么大的差异,但是这个例子说明了代码写法上的差异会对运算性能产生很大的影响。

通常来说有两类运行效率差的情况:第1种是相对执行时间长的方法,我们可以很轻松的找到这些方法并做一定的优化。第2种是执行时间短,但是执行频次很高的方法,因为执行次数多,累积效应下就会对性能产生很大的影响。

修复这些细节效率问题,需要使用Android SDK提供的工具,进行仔细的测量,然后再进行微调修复。

Traceview Walkthrough

通过Android Studio打开里面的Android Device Monitor,切换到DDMS窗口,点击左边栏上面想要跟踪的进程,再点击上面的Start Method Tracing的按钮.

启动跟踪之后,再操控app,做一些你想要跟踪的事件,例如滑动listview,点击某些视图进入另外一个页面等等。操作完之后,回到Android Device Monitor,再次点击Method Tracing的按钮停止跟踪。此时工具会为刚才的操作生成TraceView的详细视图。

找出线程分布这个功能不实用

TraceView这个工具就是用来分析代码里方法的执行时间的。

Timeline Panel

(1)每一行表示一个线程 (2)每一种不同颜色代表一个不同的方法 (3)每一个bar对应一个方法执行过程,bar的宽度表示方法执行的时长 (4)对于缩小图,bar表示方法开始执行时间;对于放大图,每个方法对应的bar会扩大成一个有色的U型,U型的左边 表示方法开始时间,右边表示方法结束时间 (5)小技巧:双击上面的时间轴可以缩小图,鼠标选中线程的颜色部分轻微水平拉动可以放大图

补充(参考4): (1)颜色脉冲bar的高度表示cpu的利用率,高度越高表示cpu利用率越高 (2)白色gap空白块表示该线程目前没有占CPU,被其他线程占用 (3)黑块表示系统空闲(system idle)

Profile Panel

(1)表示执行的方法列表 (2)任意点击一个方法展开,Parent表示调用该方法的方法,Children表示该方法调用的方法 (3)颜色和Timeline中的颜色含义相同,且选中一个方法会在Timeline中同步高亮 (4)参数解释: Incl Cpu Time(%) -方法自身及该方法调用的所有子方法所占时间和(百分比) Excl Cpu Time(%) -方法自身所占时间(百分比) Incl Real Time(%) -类似于Incl Cpu Time(%) Excl Real Time(%) -类似于Excl Cpu Time(%) Calls + Recur Calls/Total -调用次数+递归调用次数。或某子方法被该父方法调用的次数/某子方法被所有方法调用的总次数。参考5。

Cpu Time/Call -方法调用一次所占Cpu Time。[Calls + Recur Calls/Total] [Cpu Time/Call] = [Incl Cpu Time] Real Time/Call -方法调用一次所占Real Time。[Calls + Recur Calls/Total] [Real Time/Call] = [Incl Real Time] 关于Cpu Time和Real Time的区别(参考6): Cpu Time 指的是方法执行所占用的cpu时间,不包括中间的等待时间 Real Time 指的是方法从开始执行到结束执行总的时间,包括中间的等待时间

产生trace logs的两种方式

(1)更精确方式:Debug.startMethodTracing(),Debug.stopMethodTracing(),生成.trace文件, 导出到PC目录adb pull /sdcard/*.trace /tmp, 然后DDMS -> Open file

(2)次精确方式:打开Tools->Android->Android Device Monitor。在Devices下选中进程,然后选中Start Method Profiling图标,然后在设备上进行可能存在性能问题的操作,然后点击Stop Method Profiling图标

关于分析方法

关于TraceView阿里的经典文章 :https://yq.aliyun.com/articles/20467

方法性能问题通常分为两类: (1)调用次数不多,但每次耗时长的方法 (2)自身耗时不长,但频繁调用的方法

  • 关于第一种,通常做法是先按Cpu Time/Call降序排序,然后看Incl Cpu Time的大小,综合起来越大的性能问题越严重

  • 关于第二种,通常做法是按Calls + Recur Calls/Total降序排序,然后看Incl Cpu Time的大小,综合起来越大的性能问题越严重

做完上面两种排序之后 ,找自己的应用里的包名,分析查看它的耗时时间。

参考文献:

(0) http://developer.android.com/intl/zh-cn/tools/performance/traceview/index.html (1) http://developer.android.com/intl/zh-cn/tools/debugging/debugging-tracing.html (2) http://jwzhangjie.cn/2015/07/14/android系统性能调优工具介绍/ (3) http://myeyeofjava.iteye.com/blog/2250801 (4) http://stackoverflow.com/questions/6476991/android-eclipse-traceview-i-just-dont-get-it (5) http://stackoverflow.com/questions/28125441/how-to-understanding-callsrecurcalls-total-in-android-traceview-tool/28149711#28149711 (6) http://stackoverflow.com/questions/15760447/what-is-the-meaning-of-incl-cpu-time-excl-cpu-time-incl-real-cpu-time-excl-re

Batching and Caching(批量预处理和缓存)

为了提升运算性能,这里介绍2个非常重要的技术,Batching与Caching。

Batching是在真正执行运算操作之前对数据进行批量预处理。

Blocking the UI Thread

提升代码的运算效率是改善性能的一方面,让代码执行在哪个线程也同样很重要。我们都知道Android的Main Thread也是UI Thread,它需要承担用户的触摸事件的反馈,界面视图的渲染等操作。这就意味着,我们不能在Main Thread里面做任何非轻量级的操作,类似I/O操作会花费大量时间,这很有可能会导致界面渲染发生丢帧的现象,甚至有可能导致ANR。防止这些问题的解决办法就是把那些可能有性能问题的代码移到非UI线程进行操作。

Container Performance(Android容器性能)

另外一个我们需要注意的运算性能问题是基础算法的合理选择,例如冒泡排序与快速排序的性能差异.

快速排序随着排序数量的增加会一直很稳定,而冒泡会随着排序数量的增加而快速花费更多的时间。

推荐的数据结构: ArrayMap 替代 HashMap ArraySet 替代 HashSet SparseArray 替代 HashMap SparseBooleanArray 替代 HashMap SparseIntArray 替代 HashMap SparseLongArray 替代 HashMap LongSparseArray 替代 HashMap

HashMap vs ArrayMap

HashMap 的位置在 java.util.HashMap 包中。

ArrayMap 的位置在 android.util.ArrayMap 和 android.support.v4.util.ArrayMap 包中。

它存在于 support.v4 包中,以便兼容较低的 Android 版本。

这里 是直接出自 Android 开发者频道的 youtube 视频,强烈建议你看一下。

ArrayMap 是一种通用的键->值映射数据结构,它在设计上比传统的 HashMap 更多考虑内存优化。 它使用两个数组来存储数据——一个整型数组存储键的哈希值,另一个对象数组存储键/值对。这样既能避免为每个存入 map 中的键创建额外的对象,还能试图更j积极地控制这些数组的长度的增加(因为增加长度只需拷贝数组中的键,而不是重新构建一个哈希表)。

需要注意的是,ArraryMap 并不适用于可能含有大量条目的数据类型。它通常比 HashMap 要慢,因为在查找时需要进行二分查找,增加或删除时,需要在数组中插入或删除键。对于一个最多含有几百条目的容器来说,它们的性能差异并不巨大,相差不到 50%。

HashMap

HashMap 基本上就是一个 HashMap.Entry 的数组(Entry 是 HashMap 的一个内部类)。更准确来说,Entry 类中包含以下字段:

一个非基本数据类型的 key 一个非基本数据类型的 value 保存对象的哈希值 指向下一个 Entry 的指针 当有键值对插入时,HashMap 会发生什么 ?

首先,键的哈希值被计算出来,然后这个值会赋给 Entry 类中对应的 hashCode 变量。 然后,使用这个哈希值找到它将要被存入的数组中“桶”的索引。 如果该位置的“桶”中已经有一个元素,那么新的元素会被插入到“桶”的头部,next 指向上一个元素——“桶”在本质上形成为链表。 现在,当你用 key 去查询值时,时间复杂度是 O(1)。

虽然时间上 HashMap 更快,但同时它也花费了更多的内存空间。

缺点:

自动装箱的存在意味着每一次插入都会有额外的对象创建。这跟垃圾回收机制一样也会影响到内存的利用。 HashMap.Entry 对象本身是一层额外需要被创建以及被垃圾回收的对象。 “桶” 在 HashMap 每次被压缩或扩容的时候都会被重新安排。这个操作会随着对象数量的增长而变得开销极大。 在Android中,当涉及到快速响应的应用时,内存至关重要,因为持续地分发和释放内存会出发垃圾回收机制,这会拖慢应用运行。

垃圾回收机制会影响应用性能表现

垃圾回收时间段内,应用程序是不会运行的,最终应用使用上就显得卡顿。

ArrayMap

ArrayMap 使用2个数组。它的对象实例内部有用来存储对象的 Object[] mArray 和 存储哈希值的 int[] mHashes。当插入一个键值对时:

键/值被自动装箱。 键对象被插入到 mArray[] 数组中的下一个空闲位置。 值对象也会被插入到 mArray[] 数组中与键对象相邻的位置。 键的哈希值会被计算出来并被插入到 mHashes[] 数组中的下一个空闲位置。 对于查找一个 key :

键的哈希值先被计算出来 在 mHashes[] 数组中二分查找此哈希值。这表明查找的时间复杂度增加到了 O(logN)。 一旦得到了哈希值所对应的索引 index,键值对中的键就存储在 mArray[2index] ,值存储在 mArray[2index+1]。 这里的时间复杂度从 O(1) 上升到 O(logN),但是内存效率提升了。当我们在 100 左右的数据量范围内尝试时,没有耗时的问题,察觉不到时间上的差异,但我们应用的内存效率获得了提高。

性能优化工具总结

  • Lint是用来分析静态代码的,StrictMode是用来分析动态代码的就是说当代码执行起来以后分析代码性能的。http://droidyue.com/blog/2015/09/26/android-tuning-tool-strictmode/index.html

  • TraceView是用来分析方法执行时长和CPU及各个线程的执行占用情况。

  • LeakCanary是在销毁的时机下,通过构造方法把对象的引用(Activity或者Fragment)传到WeakReference里面,然后创建一个与之相关联的队列,然后再调用GC,GC之后,如果它在这个回收的队列里,那么就说明它被回收了,如果没有在这个队列里说明它还没有被回收,可以初步怀疑为内存Leak。这个工具只是把可疑的给爆出来,并不一定是真的内存Leak。需要根据代码具体查看。

  • Memory Monitor App的内存分配情况,可以快速查看当打开和关闭某个页面后的内存变化情况,能够大概判断是否有可能存在内存Leak,这只是一个宏观上的判断,最好GC一下,看看它是否能够回到最初的水平。同时也能判断它是否有内存抖动(就是在短期内有大量的对象创建和释放,会影响UI,使之卡顿)。

  • Heap Viewer 能实时查看App分配的内存大小和空闲内存大小,它属于早期DDMS的一个工具,现在可以让Memory Monitor给代替掉。

  • Heap Snapshot 获取Java堆内存详细信息,可以分析出内存泄漏的问题,并可按照Package和Class排列,这个是生成hprof初步查看分配的第一步,这步可以看下它在内存中和堆内存中是否还有对象存在,如果有,它自身占用了多大,它所引用的对象又占用了多大。并且在这个时间段内,这个对象是否应该被回收掉,或者它应该只存在一个实例,可以通过它在内存中的占用情况看出它有没有问题。如果有问题,必要就要导出一个标准的.hprof文件交给MAT分析,MAT一定会把情况分析清楚。这个工具通常是用在内存分析上的第一步,看看一些对象的内存情况是否正常等等。它在AS下使用情况是最好的。

  • Allocation Tracker 追踪内存分配信息,按顺序排列,这样我们就能清晰看出来某一个操作的内存是如何一步一步分配出来的 可以根据Method和Allocator来查看。会生成一个轮胎图,炫酷,不过没啥用。

  • MAT 是大招 有在内存泄漏系列文章里有它的身影。

  • GPU Monitor 分析GPU的性能,实时查看绘制每一帧所花费的时间

  • System Information 这个功能很多人不知道,它可以查看Package Memory Activity的一些信息。

  • Hierarchy Viewer 不再说了

  • Tracer for OpenGL ES 这个是用来分析嵌入式系统的(ES)

  • Systrace Systrace是Android4.1中新增的性能数据采样和分析工具。它可帮助开发者收集Android关键子系统(如surfaceflinger、WindowManagerService等Framework部分关键模块、服务,View系统等)的运行信息,从而帮助开发者更直观的分析系统瓶颈,改进性能。

  • Method Tracer 这个是比TraceView好用很多的一个方法追踪工具。

参考: https://developer.android.com/studio/profile/index.html?hl=zh-cn

这里有所有关于AS的工具使用,极其实用。

从渲染绘制流程去优化

  1. CPU计算:它是把UI、控件、图片等等转换成Polygons和Texture。

  2. CPU将计算好的Polygons和Texture传递到GPU的时候也需要时间。

  3. GPU把Polygons和Texture进行格栅化。

上面三步是渲染绘制的基本流程。我们可以从这三个流程做分别对CPU和GPU做优化。

1.优化CPU的计算时间

CPU的优化,从减轻加工View对象成Polygons和Texture来下手View Hierarchy中包涵了太多的没有用的view,这些view根本就不会显示在屏幕上面,一旦触发测量和布局操作,就会拖累应用的性能表现。

1.如何找出里面没用的view呢?或者减少不必要的view嵌套。 工具:Hierarchy Viewer检测

优化: 1)当我们的布局是用的FrameLayout的时候,我们可以把它改成merge 可以避免自己的帧布局和系统的ContentFrameLayout帧布局重叠造成重复计算(measure和layout) 2)ViewStub:当加载的时候才会占用。不加载的时候就是隐藏的,仅仅占用位置。

三个圆点分别代表:测量、布局、绘制三个阶段的性能表现。 1)绿色:渲染的管道阶段,这个视图的渲染速度快于至少一半的其他的视图。 2)黄色:渲染速度比较慢的50%。 3)红色:渲染速度非常慢。

优化思想:查看自己的布局,层次是否很深以及渲染比较耗时,然后想办法能否减少层级以及优化每一个View的渲染时间。

  • include 标签共享布局

  • ViewStub标签实现延迟加载

  • merge标签减少布局层次

  • 尽量使用CompoundDrawble

  • 使用Lint

上面几步都是从布局和Java代码进行入手 ,这些优化都是对CPU的计算时间进行优化的,属于对CPU的优化。

2.CPU将计算好的Polygons和Texture传递到GPU的时候也需要时间

OpenGL ES API允许数据上传到GPU后可以对数据进行保存,做了缓存。

3.GPU进行格栅化

优化:尽量避免过度绘制(overdraw)

GPU如何优化:

1.背景经常容易造成过度绘制。

2.自定义控件如何处理过度绘制。 可以通过裁剪来处理。

实例分析:扑克牌

Last updated