Android 性能优化(四)以内存优化实战

在上一篇《Android性能优化(三)以内存管理》中咱们对Android的内存管理有了必定的认识,本篇文章从实际出发对内存进行优化,主要包含如下部分:javascript

1. Memory Leak

内存泄漏:对于Java来讲,就是new出来的Object 放在Heap上没法被GC回收(内存中存在没法被回收的对象);内存泄漏发生时的主要表现为内存抖动,可用内存慢慢变少。html

1.1 Memory Monitor

AndroidStudio自带的Memory Monitor能够方便的观察堆内存的分配状况,而且能够粗略的观察有没有Memory Leak。java

频繁的内存抖动,可能存在内存泄漏

  • A:initiate GC 手动触发GC操做;
  • B:Dump Java Heap 获取当前的堆栈信息,生成一个.hprof文件,AndroidStudip会自动使用HeapViewer打开;通常用于操做以后检测内存泄漏的状况;
  • C:Start Allocation Tracking 内存分配追踪工具,用于追踪一段时间的内存分配使用状况,可以知道执行一些列操做后,有哪些对象被分配空间。通常用于追踪某项操做以后的内存分配,调整相关的方法调用来优化app性能与内存使用;
  • D:剩余可用内存;
  • E:已经使用的内存。

点击Memory Monitor的Dump Java Heap,会生成一个.hprof文件,AndroidStudio会自动使用HeapViewer打开。android

Hprof Viewer打开.hprof文件

左面板说明:git

  • Total Count 该类的实例个数
  • Heap Count 选定的Heap中实例的个数
  • Sizeof 每一个实例占用的内存大小
  • Shallow Size 全部该类的实例占用的内存大小
  • Retained Size 该类的全部实例可支配的内存大小

右面板说明:github

  • Instance 该类的全部实例对象(左侧Total Count为15,此处就有15个对象)
  • Depth 深度, GC Root点到该实例的最短链路数
  • Dominating Size 该实例可支配的内存大小

此处能够看出MainActivity存在了15个示例对象,怀疑此处有问题。编程

1.2 MAT

上述只是能够粗略的看出是否是有问题,而要知道问题出在哪里就须要借助MAT了。将生成的.hprof文件进行转换,而后使用MAT打开;缓存

格式转换命令:hprof-conv 原文件路径 转换后文件路径复制代码

MAT打开.hprof

注意下面的Actions:性能优化

  • Histogram能够列出内存中每一个对象的名字、数量以及大小。
  • Dominator Tree会将全部内存中的对象按大小进行排序,而且咱们能够分析对象之间的引用结构。
    通常使用最多的也是这两个功能。

Retained Heap表示这个对象以及它所持有的其它引用(包括直接和间接)所占的总内存微信

  • 使用Histogram:
  1. 点击Histogram并在顶部的Regex中输入MainActivity会进行正则匹配,会将包含“MainActivity”的全部对象所有列出了出来,其中第一行就是MainActivity的实例。
  2. 对着想查看的对象点击右键 -> List objects -> with incoming references 查看具体MainActivity实例。
  3. 对想要查看的对象实例点击右键-> Path To Gc Roots -> exclude weak reference(排除掉软引用)。

注意:
this$0前面的图标的左下角有个圆圈,这表明这个引用能够被Gc Roots引用到,因为MainActivity$LeakClass能被GC Roots访问到致使其不能被回收,从而它所持有的其它引用也没法被回收了,包括MainActivity,也包括MainActivity中所包含的其它资源。
此时咱们就找到了内存泄漏的缘由。

  • 使用Dominator Tree


使用上面Histogram的操做方式也能够找到泄漏的具体缘由,此处再也不累述。
注意: 每一个对象前的图标的圆圈,并不表明必定是致使内存泄漏的缘由,有些对象就是须要在内存中存活的,须要区别对待。

1.3 LeakCanary

LeakCanary是square出品的一个检测内存泄漏的库,集成到App以后便无需关心,在发生内存泄漏以后会Toast、通知栏弹出等方式提示,能够指出泄漏的引用路径,并且能够抓取当前的堆栈信息供详细分析。

2. Out Of Memory

2.1 Android OOM

Android系统的每一个进程都有一个最大内存限制,若是申请的内存资源超过这个限制,系统就会抛出OOM错误。

  • Android 2.x系统,当dalvik allocated + external allocated + 新分配的大小 >= dalvik heap 最大值时候就会发生OOM。其中bitmap是放于external中 。
  • Android 4.x系统,废除了external的计数器,相似bitmap的分配改到dalvik的java heap中申请,只要allocated + 新分配的内存 >= dalvik heap 最大值的时候就会发生OOM(art运行环境的统计规则仍是和dalvik保持一致)

内存溢出是程序运行到某一阶段的最终结果,直接缘由是剩余的内存不能知足内存的申请,可是再分析间接缘由内存为何没有了:

  • 内存泄漏的存在可能致使可用内存愈来愈少;
  • 内存申请的峰值超过了系统时间点剩余的内存;(例如:某手机单个进程可用最大内存为192M,目前分配内存80M,此时申请5M内存,可是当前时间点整个系统可用内存只有3M,此时没有超出单个进程可用最大内存,可是OOM也会发生)

2.2 Avoid Android OOM

除了避免内存泄漏以外,根据《Manage Your App's Memory》,咱们能够对内存的状态进行监听,在Activity中覆写此方法,根据不一样的case进行不一样的处理:

@Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
    }复制代码

TRIM_MEMORY_RUNNING_MODERATE:你的应用正在运行而且不会被列为可杀死的。可是设备此时正运行于低内存状态下,系统开始触发杀死LRU Cache中的Process的机制。
TRIM_MEMORY_RUNNING_LOW:你的应用正在运行且没有被列为可杀死的。可是设备正运行于更低内存的状态下,你应该释放不用的资源用来提高系统性能。
TRIM_MEMORY_RUNNING_CRITICAL:你的应用仍在运行,可是系统已经把LRU Cache中的大多数进程都已经杀死,所以你应该当即释放全部非必须的资源。若是系统不能回收到足够的RAM数量,系统将会清除全部的LRU缓存中的进程,而且开始杀死那些以前被认为不该该杀死的进程,例如那个包含了一个运行态Service的进程。
当应用进程退到后台正在被Cached的时候,可能会接收到从onTrimMemory()中返回的下面的值之一:
TRIM_MEMORY_BACKGROUND: 系统正运行于低内存状态而且你的进程正处于LRU缓存名单中最不容易杀掉的位置。尽管你的应用进程并非处于被杀掉的高危险状态,系统可能已经开始杀掉LRU缓存中的其余进程了。你应该释放那些容易恢复的资源,以便于你的进程能够保留下来,这样当用户回退到你的应用的时候才可以迅速恢复。
TRIM_MEMORY_MODERATE: 系统正运行于低内存状态而且你的进程已经已经接近LRU名单的中部位置。若是系统开始变得更加内存紧张,你的进程是有可能被杀死的。
TRIM_MEMORY_COMPLETE: 系统正运行于低内存的状态而且你的进程正处于LRU名单中最容易被杀掉的位置。你应该释听任何不影响你的应用恢复状态的资源。

3. Memory Churn

Memory Churn内存抖动:大量的对象被建立又在短期内立刻被释放。
瞬间产生大量的对象会严重占用Young Generation的内存区域,当达到阀值,剩余空间不够的时候,也会触发GC。系统花费在GC上的时间越多,进行界面绘制或流音频处理的时间就越短。即便每次分配的对象占用了不多的内存,可是他们叠加在一块儿会增长Heap的压力,从而触发更多其余类型的GC。这个操做有可能会影响到帧率,并使得用户感知到性能问题。

Drop Frame Occur

常见的可能引起内存抖动的情形:

  • 循环中建立临时对象;
  • onDraw中建立Paint或Bitmap对象等;

例如以前使用过的有些下拉刷新控件的实现方式,在onDraw中建立Bitmap等多个临时大对象会致使内存抖动。

4. Bitmap

Bitmap的处理也是Android中的一个难点,固然使用第三方框架的话就屏蔽掉了这个难点。

  • Bitmap的内存模型
  • Bitmap的加载、压缩、缓存等策略
  • 版本的兼容等

关于Bitmap以后会写专门的一篇文章来介绍,此处能够参考《Handling Bitmaps》

5. Program Advice

5.1 节制地使用Service

内存管理最大的错误之一就是让Service一直运行。在后台使用service时,除非它须要被触发并执行一个任务,不然其余时候Service都应该是中止状态。另外须要注意Service工做完毕以后须要被中止,以避免形成内存泄漏。

系统会倾向于保留有Service所在的进程,这使得进程的运行代价很高,由于系统没有办法把Service所占用的RAM空间腾出来让给其余组件,另外Service还不能被Paged out。这减小了系统可以存放到LRU缓存当中的进程数量,它会影响应用之间的切换效率,甚至会致使系统内存使用不稳定,从而没法继续保持住全部目前正在运行的service。

建议使用JobScheduler,而尽可能避免使用持久性的Service。还有建议使用IntentService,它会在处理完交代给它的任务以后尽快结束本身。

5.2 使用优化过的集合

Android API当中提供了一些优化事后的数据集合工具类,如SparseArray,SparseBooleanArray,以及LongSparseArray等,使用这些API可让咱们的程序更加高效。传统Java API中提供的HashMap工具类会相对比较低效,由于它须要为每个键值对都提供一个对象入口,而SparseArray就避免掉了基本数据类型转换成对象数据类型的时间。

5.3 谨慎对待面向抽象

开发者常常把抽象做为好的编程实践,由于抽象可以提高代码的灵活性与可维护性。然而,抽象会致使一个显著的开销:面向抽象须要额外的代码(不会被执行到),一样会被咨映射到内存中,耗费了更多的时间以及内存空间。所以若是面向抽象对你的代码没有显著的收益,那你应该避免使用。

例如:使用枚举一般会比使用静态常量要消耗两倍以上的内存,在Android开发当中咱们应当尽量地不使用枚举。

5.4 使用nano protobufs序列化数据

Protocol buffers是Google为序列化数据设计的一种语言无关、平台无关、具备良好扩展性的数据描述语言,与XML相似,可是更加轻量、快速、简单。若是使用protobufs来实现数据的序列化及反序列化,建议在客户端使用nano protobufs,由于一般的protobufs会生成冗余代码,会致使可用内存减小,Apk体积变大,运行速度减慢。

5.5 避免内存抖动

垃圾回收一般不会影响应用的表现,可是短期内屡次的垃圾回收会消耗掉界面绘制的时间。系统花费在GC上的时间越多,进行界面绘制或流音频处理的时间就越短。一般内存抖动会致使屡次的GC,实践中内存抖动表明了一段时间内分配了临时对象。

例如:在For循环中分配了多个临时对象,或在onDraw()方法中建立了Paint、Bitmap对象,应用产生了大量的对象;这会很快耗尽young generation的可用内存,致使GC发生。

使用Analyze your RAM usage中的工具找出代码里内存抖动的地方。考虑把操做移出内部循环,或者将其移动到基于工厂的分配结构中。

5.6 移除消耗内存的库、缩减Apk的大小

查看Apk的大小,包括三方库和内嵌的资源,这些都会影响应用消耗的内存。经过减小冗余、非必须或大的组件、库、图片、资源、动画等,均可以改善应用的内存消耗。

5.7 使用Dagger 2进行依赖注入

若是您打算在应用程序中使用依赖注入框架,请考虑使用Dagger 2。 Dagger不使用反射来扫描应用程序的代码。 Dagger的编译时注解技术实现意味着它不须要没必要要的运行时成本。而使用反射的其它依赖注入框架一般经过扫描代码来初始化过程。 此过程可能须要显着更多的CPU周期和RAM,并可能致使应用程序启动时明显的卡顿。

备注:以前的文档是不建议使用依赖注入框架,由于实现原理是使用反射,而进化为编译时注解以后,就再也不有反射带来的影响了。

5.8 谨慎使用第三方库

不少开源的library代码都不是为移动端而编写的,若是运用在移动设备上,并不必定适合。即便是针对Android而设计的library,也须要特别谨慎,特别是在你不知道引入的library具体作了什么事情的时候。例如,其中一个library使用的是nano protobufs, 而另一个使用的是micro protobufs。这样一来,在你的应用里面就有2种protobuf的实现方式。这样相似的冲突还可能发生在输出日志,加载图片,缓存等等模块里面。另外不要为了1个或者2个功能而导入整个library,若是没有一个合适的库与你的需求相吻合,你应该考虑本身去实现,而不是导入一个大而全的解决方案。

6. Other

6.1 谨慎使用LargeHeap属性

能够经过在manifest的application标签下添加largeHeap=true的属性来为应用声明一个更大的heap空间(能够经过getLargeMemoryClass()来获取到这个更大的heap size阈值)。然而,声明获得更大Heap阈值的本意是为了一小部分会消耗大量RAM的应用(例如一个大图片的编辑应用)。不要轻易的由于你须要使用更多的内存而去请求一个大的Heap Size。只有当你清楚的知道哪里会使用大量的内存而且知道为何这些内存必须被保留时才去使用large heap,使用额外的内存空间会影响系统总体的用户体验,而且会使得每次gc的运行时间更长。在任务切换时,系统的性能会大打折扣。另外, large heap并不必定可以获取到更大的heap。在某些有严格限制的机器上,large heap的大小和一般的heap size是同样的。

6.2 谨慎使用多进程

多进程确实是一种能够帮助咱们节省和管理内存的高级技巧。若是你要使用它的话必定要谨慎使用,由于绝大多数的应用程序都不该该在多个进程当中运行的,一旦使用不当,它甚至会增长额外的内存而不是帮咱们节省内存;同时须要知晓多进程带来的缺点。这个技巧比较适用于那些须要在后台去完成一项独立的任务,和前台的功能是能够彻底区分开的场景。

这里举一个比较适合去使用多进程技巧的场景,好比说咱们正在作一个音乐播放器软件,其中播放音乐的功能应该是一个独立的功能,它不须要和UI方面有任何关系,即便软件已经关闭了也应该能够正常播放音乐。若是此时咱们只使用一个进程,那么即便用户关闭了软件,已经彻底由Service来控制音乐播放了,系统仍然会将许多UI方面的内存进行保留。在这种场景下就很是适合使用两个进程,一个用于UI展现,另外一个则用于在后台持续地播放音乐。

6.3 实现方式可能存在的问题:例如启动页闪屏图,show完毕以后应该释放掉Bitmap。

一些实现方式看起来没有问题实现了功能可是实际上可能对内存形成了影响。我在使用Heap Viewer查看Bitmap对象时发现了一张只需下载不该该被加载的图。

使用HeapViewer可直接查看Bitmap

内存中出现的不该该被加载的图

经过查阅代码,发现问题出在:此处下载图片做为另外一个模块的使用图,可是下载的方法居然是使用图片加载器加载出来Bitmap而后再保存到本地;并且保存以后也没有将Bitmap对象释放掉。

与之相似的还有:首页闪屏图展现以后,Bitmap对象应该及时释放掉。

6.4 使用try catch进行捕获

对高风险OOM代码块如展现高清大图等进行try catch,在catch块加载非高清的图片并作相应内存回收的处理。注意OOM是OutOfMemoryError,不能使用Exception进行捕获。

7. Summary

内存优化的套路:

  1. 解决全部的内存泄漏

    • 集成LeakCanary,能够方便的定位出90%的内存泄漏问题;
    • 经过反复进出可疑界面,观察内存增减的状况,Dump Java Heap获取当前堆栈信息使用MAT进行分析。
    • 内存泄漏的常见情形可参照《Android 内存泄漏分析心得》
  2. 避免内存抖动

    • 避免在循环中建立临时对象;
    • 避免在onDraw中建立Paint、Bitmap对象等。
  3. Bitmap的使用

    • 使用三方库加载图片通常不会出内存问题,可是须要注意图片使用完毕的释放,而不是被动等待释放。
  4. 使用优化过的数据结构

  5. 使用onTrimMemory根据不一样的内存状态作相应处理
  6. Library的使用
    • 去掉无用的Library,对生成的Apk进行反编译查看使用到的Library,避免出现无用的Lib仍然被打进Apk;
    • 避免引入巨大的Library;
    • 使用Proguard进行混淆、压缩。

参考:

欢迎关注微信公众号:按期分享Java、Android干货!

欢迎关注
相关文章
相关标签/搜索