《Android应用性能优化》2——内存、CPU、性能测评

来源:互联网 发布:商桥2016软件下载 编辑:程序博客网 时间:2024/05/19 10:13
4、高效使用内存

4.1 说说内存

  Android设备的性能主要取决于以下三因素:

  1. CPU如何操纵特定的数据类型;
  2. 数据和指令需占用多少存储空间;
  3. 数据在内存中的布局

4.2 数据类型

  int和long数据使用了某些版本的快速排序算法排序,而short数据使用计数排序,它的算法复杂度是线性的。因此使用short类型,是一石二鸟之策:更少的内存消耗(2MB,而不是int的4MB或long的8MB),更快的运行速度

  • 处理大量数据时,使用可满足要求的最小数据类型。如,基于性能和空间的考量,选择short数组而不是int数组。若对精度要求不高(若需要使用FloatMath类),使用float而不是double
  • 避免类型转换。尽量保持类型一致,尽可能在计算中使用单一类型。
  • 若有必要取得更好的性能,推到重来,但要认真处理。

4.3 访问内存

  CPU本身也有开销,在此过程中会使用到两级缓存

  1. 一级缓存(L1)
  2. 二级缓存(L2)

  L1缓存的速度较快,但比L2小。例如,L1缓存可能是64KB(32KB的数据缓存、32KB指令缓存),而L2缓存可能是512KB

  当数据或指令在缓存中找不到时,即使缓存未命中。这时需从内存中取出数据或指令。缓存未命中有以下几种情况:

  1. 指令缓存读未命中;
  2. 数据缓存读未命中;
  3. 写未命中

  第一种缓存未命中最关键,因为CPU要一直等到从内存中读出指令,才可继续执行。第二种缓存未命中几乎和第一种同样重要,尽管CPU仍可能执行其他不依赖要读取数据的指令,但这种情况只会在CPU指令乱序时出现。最后一种缓存未命中的重要性最低,CPU通常可继续执行指令。你几乎无法控制写未命中,不过无须过于担心,只要关注前两种类型就好了,它俩是应该极力避免的缓存未命中情况。

  以下方法可减少指令缓存未命中的几率:

  1. 在Thumb模式下编译本地库。这不保证会使代码速度更快,因为Thumb指令比ARM指令执行得慢(可能要执行更多的指令)。如何用Thumb模式编译本地库的耕读信息参阅第2章
  2. 保持代码相对密集。虽不能保证密集的Java代码会产生密集的机器码,但这往往是可行的

  以下方法可减少数据缓存读未命中的几率:

  1. 在有大量数据存储在数组中时,使用尽可能小的数据类型
  2. 选择顺序访问而不是随机访问。最大限度地重用已在缓存中的数据,防止数据从缓存中清除后再次载入

  现在CPU都能够自动预取内存,以避免或说至少限制了缓存未命中情况的发生:

  如前所述,在应用性能的关键处使用这些技巧,这种代码通常只占一小部分。一方面,在Thumb模式下编译是一个简单的优化,并没有真正提高维护成本。另一方面,从长远来说,编写密集的代码可能会使事情变得更加复杂。没有一个放之四海而皆准的优化方法,要权衡各种优化手段。

  虽不必细到控制缓存数据的进进出出,但如何组织和使用数据最终会影响缓存的使用,进而影响性能。某些情况下,虽然会提高复杂性和维护成本,但还需将数据以特定方式排布,以提升缓存命中率。

4.5 垃圾收集

  Java的垃圾收集有两件非常重要的事情值得注意:

  1. 还是有可能出现内存泄露;
  2. 垃圾收集器会帮你管理内存,它做的不仅仅是释放不用的内存

   内存泄漏

  Android2.3定义了StrictMode类,它对检测潜在的内存泄漏有很大帮助。虽在Android2.3中,StrictMode的虚拟机策略只能检测SQLite对象(如游标)没有关闭时产生的泄漏,但在Android3.0及以上版本中,可检测以下潜在的泄漏:

  1. Acitivity泄漏;
  2. 其他对象泄漏;
  3. 对象没有关闭造成的泄漏(至于是哪些对象,可到Android文档中查看实现了Closeable接口的所有类)。

  引用

  Java定义了4种类型的引用:强软弱虚

  当使用缓存时,确保了解它使用的是什么类型的引用。例如,Android的LruCache使用强引用

  垃圾收集器的积极程度取决于实际实现

  垃圾收集

  垃圾收集可能会在不定的时间触发,你几乎无法控制它发生的时机。有时,你可通过System.gc()提醒一下Android,但垃圾收集的发生时间最终由Dalvik虚拟机决定。有以下5种情况会触发垃圾收集,这些可参考logcat中输出的消息。

  1. GC_FOR_MALLOC:发生在堆被占满不能进行内存分配时,在分配新对象之前必进行内存回收
  2. GC_CONCURRENT:发生在(可能是部分的)垃圾可供回收时,通常有很多对象可以回收
  3. GC_EXPLICIT:显示调用System.gc()产生的垃圾收集
  4. GC_EXTERNAL_ALLOC:Honeycomb及以上版本不会出现(一切都已在堆中分配)
  5. GC_HPROF_DUMP_HEAP:发生在创建HPROF文件时

  垃圾收集要花费时间,减少分配/释放对象的数量可提高性能。在Android2.2和更早的版本中尤其如此,因为垃圾收集发生在应用的主线程,很可能降低响应速度和性能,造成恶劣影响。例如,在即时游戏中会出现丢帧,因为有太多时间花在垃圾收集上。Android2.3有了转机,垃圾收集工作转移到了一个单独的线程。在垃圾收集时,还是会对主线程有点影响(暂停5毫秒或更少),但比以前的Android版本好太多了。一次完整的垃圾收集花了超过50毫秒的情况并不少见。试想一下,一个每秒30帧的游戏平均在每一帧上要花33毫秒进行渲染和显示,这时若有垃圾回收,在Android2.3之前的系统中,游戏会大受影响。

4.7 内存少的时候

  你的应用并不能独占平台,它要于许多其他应用及系统作为一个整体来共享资源。因此,当内存不足以分配给所有程序的情况下,Android会要求应用及应用的组件(如Activity或Fragment)“勒紧腰带”。

  ComponentCallbacks接口定义了API onLowMemory(),它对所有应用组件都是相同的。当它被调用时,组件基本会被要求释放那些并不会用到的内存。通常情况下,onLowMemory()的实现将释放:

  • 缓存或缓存条目(如使用强引用的LruCache);
  • 可再次按需生成的位图对象;
  • 不可见的布局对象;
  • 数据库对象。

  删除对象应该小心翼翼,重新创建是需要开销的。若没有释放出足够的内存可能会导致Android系统更激进的行为(如杀死进程),谁也不能独善其身。若应用进程被杀掉了,用户下次使用又要从头开始。因此,应用不仅要表现出色,也要释放尽可能多的资源,这样的结果是多赢的,对你的应用和其他应用都有好处。在代码中使用推迟初始化是一个好习惯,它可以让你在实现onLowMemory()时几乎不用改动其他地方的代码。

4.8 总结

  内存在嵌入式设备上是稀缺资源。尽管今天的手机和平板电脑的内存越来越多,但这些设备也在运行越来越复杂的系统和应用。有效地使用内存,不仅可使应用在旧设备上运行时占用较少的内存,还可让程序跑得更快。请记住,应用对内存大需求是无止境的。

5、多线程和同步

  应用运行的Android环境版本决定了会派生哪些管家线程。例如,垃圾收集作为一个独立线程只出现杂Android2.3和其后版本中,Android2.2还没有即时编译器,Android2.1只生成了6个线程(而不是8个)

5.1 线程

  Thread对象,也就是Java定义的Thread类的实例,是自己带有调用栈的执行单位。

  优先级

  1. MIN_PRIORITY(1)
  2. NORM_PRIORITY(5)——默认优先级
  3. MAX_PRIORITY(10)

  改变线程优先级需谨慎,增加一个线程的优先级可能会加快这个线程的任务执行速度,但也会对其他线程造成负面影响,让它们无法及时获取到CPU资源,从而扰乱了整体的用户体验。若需这样做,可考虑使用优先级老化算法。

5.2 AsyncTask http://www.cnblogs.com/nathan909/p/5328082.html

  应用处理顺序:

  1. 在UI线程收到事件;
  2. 在非UI线程中处理相应事件;
  3. UI根据处理结果进行刷新

5.3 Handler和Looper http://www.cnblogs.com/nathan909/p/5284745.html

5.4 数据类型 http://www.cnblogs.com/nathan909/p/5292600.html

  我们已知两种产生线程的方法,使用Thread和AsyncTask类。若两个或多个线程访问相同的数据,就需确保数据类型支持并发访问。

5.7 Activity生命周期 http://www.cnblogs.com/nathan909/p/5309722.html

6、性能评测和剖析

6.1 时间测量

  System.currentTimeMillis()不建议采用:

  • 精度和准确度可能不够:有些方法返回时间为纳秒表示,但也并不意味着精度为纳秒级。实际精度取决于平台,设备之间可能会有所不同。同样System.currentTimeMillis()返回的毫秒数也不能保证毫秒的精度
  • 更改系统时间会影响结果:其返回值为UTC时间1970年1月1日00:00:00到现在的毫秒数

  System.nanoTime():只能用来测量时间间隔。其消耗的时间取决于具体设备和实现

  因为调度器最终负责调度在CPU上运行的线程,需测量的操作可能会被中断几次,来让出CPU时间给别的线程。因此,测量结果可能包括一些执行其他代码的时间,这可能会得出不正确的时间测量结果,产生误导

  Debug.threadCpuTimeNanos():只测量在当前线程中所花费的时间,结果更为准确

6.2 方法调用跟踪

  Traceview可得到各函数的耗时和时间占比等信息,能很快确定哪些地方不需进一步花精力研究

  因为启用跟踪时JIT编译器是禁用的,得到的结果可能有一定误导性。

  Traceview并不完美,但它可给出很好的启示,帮助找出实际中真正执行到的代码和可能是瓶颈的地方。当需要获取更好性能时,Traceview是帮助查处代码中需要改善之处的首选工具

 6.3 日志

  我们可使用log类打印信息到LogCat。除了Java传统的日志机制,如System.out.println(),Android还定义了6个日志等级,每个都有自己对应的方法:

  1. verbose(Log.v)
  2. debug(Log.d)
  3. info(Log.i)
  4. warning(Log.w)
  5. error(Log.e)
  6. assert(Log.wtf)

  如,调用Log.v(TAG,"my message")相当于调用Log.println(Log.VERBOSE, TAG, "my message")。

6.4 总结

  测量性能的方式很简单,但它是优化过程中关键的部分,Android提供了简易而强大的工具进行辅助。无论是跟踪Java还是本地方法,Traceview都是最有用的工具之一,但请记住,只有真正的设备上的实际测量才能给出准确的答案,因为Traceview会禁用Dalvik的JIT编辑器。虽然这些主题相当琐碎,但它们非常重要,作为一个Android Coder,Android提供的各种工具都应该了然于胸,信手拈来。此外,还要记得经常检查新工具、新版本SDK,看有没有提升剖析、测量和调试功能的新特性。先找瓶颈,再优化代码,做到物尽其用。

0 0