【宝贵经验】Android性能优化之内存优化实战

1. Memory Leak

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

1.1 Memory Monitor

AndroidStudio自带的Memory Monitor可以方便的观察堆内存的分配情况,并且可以粗略的观察有没有Memory Leak。

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

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打开。

Hprof Viewer打开.hprof文件

左面板说明:

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

右面板说明:

  • 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:
  • 点击Histogram并在顶部的Regex中输入MainActivity会进行正则匹配,会将包含“MainActivity”的所有对象全部列出了出来,其中第一行就是MainActivity的实例。 

  • 对着想查看的对象点击右键 -> List objects -> with incoming references 查看具体MainActivity实例。 

  • 对想要查看的对象实例点击右键-> 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进行不同的处理:


  1. @Override 
  2.     public void onTrimMemory(int level) {        super.onTrimMemory(level); 
  3.     }  

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的使用

  • 使用三方库加载图片一般不会出内存问题,但是需要注意图片使用完毕的释放,而不是被动等待释放。
  • 使用优化过的数据结构
  • 使用onTrimMemory根据不同的内存状态做相应处理

(4)Library的使用

  • 去掉无用的Library,对生成的Apk进行反编译查看使用到的Library,避免出现无用的Lib仍然被打进Apk;
  • 避免引入巨大的Library;
  • 使用Proguard进行混淆、压缩。

本文作者:佚名

来源:51CTO

时间: 2024-08-01 23:29:00

【宝贵经验】Android性能优化之内存优化实战的相关文章

android开发中的内存优化

一.Android应用程序内存优化 在开发Android App的过程中,经常会遇到内存方面的压力,比如OOM,或者频繁GC.本文不打算涵盖内存优化的所有方面,只是介绍一下我自己遇到的问题和解决方法. 1.确定频繁分配内存的代码路径 一般来说,频繁分配内存的路径可能会是绘制(draw)相关的方法,排版(layout)相关的方法,某些回调方法(特别是传感器回调方法).你可能会检查这部分代码,然后优化它.但是,内存分配可能发生在调用链的更下面,检查代码非常困难.这里推荐一个工具,DDMS下的Allo

安卓手机怎么内存优化?手机内存优化方法

1)在此我们在手机中安装"手机360杀毒"然后打开软件,我们点击右边的"三横"之后我们再找到"设置"点击进入,如下图所示.     2)然后我们在打开的界面点击"悬浮窗",会看到有一个"内存优化忽略名单"如图所示   开发 内存优化"> 3)然后我们点击"添加"然后我们再选中我们要内存优化忽略名单之后点击"确定"即可.(如下图所示)  

android,性能优化,内存优化管理,高级缓存

http://blog.csdn.net/liao3841054/article/details/7011757 这近做的项目老是出现内存溢出,项目一大,稍不注意就会出现这样 的问题.导致第二个版本框架重写,重要的还是继承体系过深,导致垃圾回收总是回收不了,最后导致内存沾满无法释放. 内存对于手机来说是非常重要的. 下面总结了我们在注意创建对象时的规则,以及怎么更好更快的实行GC回收,和怎么构建高速的对象cace缓冲. 1 避免循环遍历的创建对象,哪怕对象很小,也是要占资源的. 2 尽量使对象符

Android性能优化之利用强大的LeakCanary检测内存泄漏及解决办法_Android

前言: 最近公司C轮融资成功了,移动团队准备扩大一下,需要招聘Android开发工程师,陆陆续续面试了几位Android应聘者,面试过程中聊到性能优化中如何避免内存泄漏问题时,很少有人全面的回答上来.所以决定抽空学习总结一下这方面的知识,以及分享一下我们是如何检测内存泄漏的.我们公司使用开源框架LeakCanary来检测内存泄漏. 什么是内存泄漏? 有些对象只有有限的生命周期.当它们的任务完成之后,它们将被垃圾回收.如果在对象的生命周期本该结束的时候,这个对象还被一系列的引用,这就会导致内存泄漏

详解Android性能优化之内存泄漏_Android

综述 内存泄漏(memory leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存.那么在Android中,当一个对象持有Activity的引用,如果该对象不能被系统回收,那么当这个Activity不再使用时,这个Activity也不会被系统回收,那这么以来便出现了内存泄漏的情况.在应用中内出现一次两次的内存泄漏获取不会出现什么影响,但是在应用长时间使用以后,若是存在大量的Activity无法被GC回收的话,最终会导致OOM的出现.那么我们在这就来分析一下导致内存泄漏的常见因素并且如何

ANDROID内存优化(大汇总——中)

写在最前: 本文的思路主要借鉴了2014年AnDevCon开发者大会的一个演讲PPT,加上把网上搜集的各种内存零散知识点进行汇总.挑选.简化后整理而成. 所以我将本文定义为一个工具类的文章,如果你在ANDROID开发中遇到关于内存问题,或者马上要参加面试,或者就是单纯的学习或复习一下内存相关知识,都欢迎阅读.(本文最后我会尽量列出所参考的文章). OOM: 内存泄露可以引发很多的问题: 1.程序卡顿,响应速度慢(内存占用高时JVM虚拟机会频繁触发GC) 2.莫名消失(当你的程序所占内存越大,它在

Android内存优化之OOM

原文:http://www.csdn.net/article/2015-09-18/2825737/1 10月14日-16日,由CSDN和创新工场联合主办的MDCC 2015中国移动开发者大会将在北京新云南皇冠假日酒店隆重召开,现在抢注大会门票,即享多重好礼!在平台与技术iOS专场议题全方位揭秘之后,平台与技术Android专场也有新动作!与会讲师--腾讯Android应用开发工程师 胡凯围绕着"Android内存优化之OOM"进行了非常深度的技术分享.   腾讯Android应用开发

Android性能优化典范(一)

2015年伊始,Google发布了关于Android性能优化典范的专题,一共16个短视频,每个3-5分钟,帮助开发者创建更快更优秀的Android App.课程专题不仅仅介绍了Android系统中有关性能问题的底层工作原理,同时也介绍了如何通过工具来找出性能问题以及提升性能的建议.主要从三个方面展开,Android的渲染机制,内存与GC,电量优化.下面是对这些问题和建议的总结梳理. 1) Render Performance 大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能.从设计师

Android性能优化典范

2015新年伊始,Google发布了关于Android性能优化典范的专题,一共16个短视频,每个3-5分钟,帮助开发者创建更快更优秀的Android App.课程专题不仅仅介绍了Android系统中有关性能问题的底层工作原理,同时也介绍了如何通过工具来找出性能问题以及提升性能的建议.主要从三个方面展开,Android的渲染机制,内存与GC,电量优化.下面是对这些问题和建议的总结梳理. 0)Render Performance 大多数用户感知到的卡顿等性能问题的最主要根源都是因为渲染性能.从设计师