码迷,mamicode.com
首页 > 移动开发 > 详细

Android OOM 系列

时间:2016-04-10 17:38:06      阅读:528      评论:0      收藏:0      [点我收藏+]

标签:

2016-04-07

OOM手记

最近组里改友盟上的BUG,NullReference——防空,非UI线程访问UI控件?ClassCastException,Dialog在Activity之后dismis/show...各种问题慢慢解决了,因为大多就是业务逻辑的处理有问题。最后就剩下一个OOM的大头,因为大多数异常不是对应的具体的业务代码,都是系统类抛出的,所以统一放在最后解决:设计良好的app通常常驻内存的也就不过5个Activity,也少有Activity会占用很大的内存,因为历史代码的原因——大家都不是非常清楚——所以就果断猜测是内存泄漏,导致app长期运行后,就开始接近OOM的高压线啦。

移动端应用的特点

其实作为手机应用,通常也就是用户和后台服务的交互入口,设计之初就需要考虑内存占用,CPU占用、App多次随进程kill和reCreate等问题,适当的缓存,异步线程和本地文件存储等都是很好的设计方法。
作为业务的一大部分代码,我们都在和服务器数据做处理,那么有以下情况是必须考虑的:

  1. 网络错误,小概率的服务器错误数据这样的情况,必须提供network_error那样的提示页面,并且一个页面通常内置下拉刷新、点击刷新这样的重试操作。
  2. 数据解析,最常见的就是json的解析,因为是客户端应用,而难免的是我们期望的数据会丢失,比如状态传递错误,字段没传递、格式有问题等。在解析这些json数据的时候,应该做好无效数据的交互提醒,不能因为少个字段就让程序挂掉,或者展示出奇奇怪怪的内容。
  3. 进程重建,因为Android系统会对后台程序做暂时性回收,但是再次打开app的时候还是会继续跟进程序的任务栈情况来恢复各个页面,那么选择SharedPreference来存储一些登录信息就是必须的做法。 总之,网络和数据解析是客户端代码重点需要注意的地方,做到全面优雅的处理,是系统稳定和良好体验的基础。

常见的OOM

OOM就是内存不够用了。可能大家提到OOM就会想到Bitmap图片加载的一些问题,其实目前这类关于图片加载的框架——ImageLoader、xUtils里的处理已经很OK了,除非你的App正是图片类的功能要求苛刻,普通app即便遇到图片加载带来的内存溢出,定位和解决都不难。
就我们程序来看,OOM集中在以下2方面:

  1. 一次性要求很大内存
    因为程序之前关于日志的上传和一些本地数据的清理操作都涉及不确定大小的文件的读取——最初假设多次少量——但是一些特殊原因后,就成了单次大量——毕竟是Android 应用,内存的使用是有严格限制的,那么如果一些功能涉及到一次性大量内存的占用——就必须仔细设计了。不过这类问题的出现也会定位到具体的代码——发现问题,解决问题也就有依有据了。

  2. 内存泄漏
    不用的内存还无谓的占据着,这显然是代码不够正确。通常我们把重心放在了完成业务功能的编写,然而,在移动端,Java程序的内存泄漏带来的问题更加严重——很多泄漏最终都是导致Activity无法释放,而Activity通常关联一大堆UI对象,尤其是图片——一旦泄漏,那么app运行一段时间就很容易嗝屁了——本来Android应用的内存就限制几十MB,几十甚至十几个Activity的无法回收,就让app内存只高不下——OOM导致崩溃是迟早的。

工欲善其事必先利其器

以下就总结一些实际项目中的内存泄漏的问题的解决。网上关于内存泄漏的常见原因和原理分析很多,这里就直接进入“记录解决问题的过程”这样的情景。
要解决OOM,那么第一步就是分析内存泄漏的根源,走代码几乎是不现实的,还好,我们可以直接运行程序查看内存对象的占用来发现那些“有问题”的Activity和其它对象。
目前Eclipse和Android Studio都由这方面的工具。
Eclipse有MAT的插件,DDMS中也有可以针对进程dump出hprof文件的工具。这里使用Android Studio和MAT来分析内存问题。
工具:

  1. Memory Analyzer Tool
    MAT可以单独下载使用,这里从Eclipse官网下载MemoryAnalyzer-1.5.0独立工具。

  2. Android Studio 1.5以上的版本有Android Monitor工具窗口,可以dump java heap后分析对应的.hprof文件。可以在Android Studio的Captures窗口中把hprof文件转换为标准格式后,通过MAT来查看。
    MAT和Android Studio都是免安装的,AS新版本可以直接导入原来的配置,十分方便。

Android Monitor使用

在Android Studio中,菜单“View > Tool Windows > Android Monitor”打开Android Monitor窗口:
技术分享
说明:
通常我们都使用期中的Logcat窗口,这里点击到Memory选项卡:
在最上面的Device列表和进程列表中选择自己的设备和要观察的进程。如果没有关联到设备或者进程,可以重新连接,或者尝试调试运行程序。
蓝色部分是已分配内存,浅蓝色是剩余的(右上方有图标说明)。
Logcat下面竖直方向的四个按钮分别是:
*启用和禁用:开启和关闭对进程内存的跟踪记录。
*执行GC:在dump内存快照文件之前,记得GC一次,然后等几秒,这样可以忽略那些不可达对象,它们是可以回收的,不存在泄漏。
*生成Java堆栈快照文件:也就是生成hprof文件,点击后,等待几秒,会主动打开生成的hprof文件。
*开启/关闭allocation跟踪:会生成.alloc文件,关于线程和内存分配对象的分配内存的过程记录,有兴趣自己尝试下。
下面是.hprof文件的一个截图:

技术分享

最左上角可以选择app heap和Zygote heap,Zygote是虚拟机底层使用的进程,这里选择app heap分析自己进程虚拟机中的对象分配就可以了。
左边的视图是class的列表,通常我们只关心自己的类,那么随便选择一行后按D,然后在左上角有Search for:输入自己的类名可以筛选出对应名称的类。
Class List View可以快速找到分配最多的对象。
切换为Package Tree View 可以根据包名找到自己的类。就跟着自己的包名找,例如com.hxw.app.MyCalendar类。
选择一个类后,右边是它所有的内存实例。一些Activity打开关闭后就应该不存在了,可以手动把常用的Activity打开关闭个几次,然后观察dump下来的hprof文件,发现Activity的实例不是0个,就说明泄漏了。

技术分享
类名很多都是PrettyCalendar$1,这样的是匿名内部类。PrettyCalendar$DayCellViewHolder这样的是命名的内部类。
ShallowSize是当前类本身直接占据的内存,Retained Size是此类自身及它所强引用到的对象总计的占据。
在右边选择一个实例后,底部的Reference Tree显示了此对象的引用情况。这个列表不好分析,待会使用MAT再看。
技术分享
在我们开发的过程中,就可以打开Android Monitor来跟踪内存使用,时常去生成hprof文件来分析app的目前内存情况。
在Captures(View > Tool Windows > Captures)窗口中是生成的快照文件。
技术分享
可以选中要使用MAT查看的hprof文件,右键,选择导出为标准.hprof文件。
之后就可以使用MAT分析生成的内存快照了。

MAT使用

在Eclipse官网下载到最新的MAT,目前(2016/4/7)是1.5版本。
直接打开:
File > Open Heap Dump打开导出的.hprof文件。

技术分享
具体的使用可以参考官方网站,或者网上其它文章。个人是直接随便点点,很快就可以熟悉了。
点击Actions > Dominator Tree:

技术分享
第一行Class Name标题下,可以输入正则表达式来筛选出要查看的类。
例如:
技术分享
第一个就是要查看的Activity,右键:

技术分享
看到如下的引用视图:
技术分享
如果你的Activity存在内存泄漏,那么此刻,分析上面列出的引用,你肯定能找到是哪些对象使得Activity Destroy之后竟然其对象还无法被GC。
this$0 就是那些内部类对外部类对象的默认引用。
以上就是Android Monitor和MAT的使用,更多细节大家自己摸索。通过工具,可以知道内存中对象的分布情况,某个类占用的内存,对象个数,观察最多内存占用的对象,分析无用对象的引用情况...拥有这些信息后,再结合业务情况,一定能找出泄漏的根源,或者设计出更好的替代方案。
建议大家在平常开发过程中,记得使用Android Monitor随时记录app的内存堆快照,尽量在每个功能的开发时就发现并解决新功能是否带来内存问题。

OOM寻找

可以看工具来直接获得哪些对象占用大量内存,哪些Activity错误的拥有多个实例,不过这之前你需要自己来操作App来让这些对象发生泄漏。
其它的工具或许可以在用户或测试人员的使用过程中生成hprof,暂且不论。我们自己可以有针对性的对经常使用的页面自己多次打开后进行分析。
通常关注Activity和一些已知的自己的大对象,比如Application,一些单例类,复杂的数据类和工具类、自己的网络、db框架类(一旦出错,涉及好多类)等。

OOM的原因?

内存溢出的根本原因就是无用的对象,间接或直接被一些静态字段或者较长时间停驻内存的其它对象所引用,导致无法在GC时被回收。
最常见的情况就是对象静态字段或者单例模式的对象引用了无用的对象。例如Activity中的静态字段,Application对象,Media相关对象,工具类,单例对象、全局数据对象...
有些是自己代码不小心造成引用,另一些情况,我们被其它库,或者系统类实例——那些常驻内存的对象——“意外”强引用到了。
内存泄漏的对象,分好多种。操作App页面,从Activity开始是个不错的选择。

Activity的泄漏

和UI相关的对象都很耗内存,作为Android中的“页面”对象,Activity是灰常占用资源的。在Activity在onDestroy执行后,此Activity再也不会被系统使用,若Activity对象无法回收,内存泄漏是很严峻的。
发现这种问题倒也简单,如果想知道MyActivity是否存在泄漏,可以手动打开此页面2次以上,再关闭Activity,手动GC后,分析hprof文件看是否MyActivity对象依然存在。
Activity的泄漏肯定不是“有意为之”的,解决方法就是把错误的强引用都移除掉。通常,Activity对象执行onDestroy依然无法回收,原因就是Activity对象被显式设置给一些静态字段,常驻内存对象。或者, Activity被内部类引用(this&0引用),而内部类对象经过一系列的参数传递,最终被一些常驻内存对象(例如自己的类似AudioManager、NotificationManager类等。)所强引用。
关于Activity,有以下地方是引用的泄漏点:

  1. handler、context,内部类对象等持有Activity的引用,需要注意这些对象在Activity onDestroy断开和Activity的关联。
  2. Handler延迟消息。
  3. 线程在Activity销毁后执行,线程访问Activity方法,进而持有Activity对象。
  4. 网络请求,其它异步任务的执行,回调监听对象持有Activity引用。 如果有一个ActivityManager的对象管理所以的Activity对象集合,通常在onCreate中添加Activity,一定记着在onDestroy后移除对Activity的引用。 Activity的引用大多数都是内部类泄漏的,最常见的就是各种异步操作的回调对象。内部类持有Activity的this$0引用,可以考虑将内部类设置为static,避免内部类不需要访问Activity时还无意中引用到Activity对象,或者,注意内部类,以及异步任务在onDestroy之后停止操作,断开Activity实例的关联。 ##json数据的泄漏 网络请求的数据,包括图片和json,xml等。图片一般的框架使用三级缓存,不存在占用大量内存或长期占用内存的问题。json和xml数据,作为设计良好的程序,网络层和UI层是分离的。在解析完毕之后,通常内存中都是一些数据对象,原始的JSONObject和JSONArray对象只应该临时存在,处理完请求响应之后,就应该释放对它们的引用。 一些情况下,不是需要每次请求的数据,例如各种配置和字典,在请求完毕后可以直接缓存。对应功能的代码在运行时可以从本地获取,如SharedPreference,SQLite。

数据对象的泛滥

通常列表数据是另一项占内存的对象。可以的话应该使用分页,而且,列表+详情页是常见的功能,所谓master-detail page,例如百度贴吧中的帖子列表,列表项可以存一些数据的key信息,摘要字段。再进入详情页面后再请求具体的页面数据。这可以根据实际应用的对象占用内存情况作出选择,单次网络请求的流量和请求的次数需要权衡,那么,如果列表一次获取了20条完整的数据,可以考虑先解析出列表页面的摘要对象,之后进入详情页面再从本地缓存中取出实际数据。
字段繁多的大对象,数量庞大的对象,本地缓存是很值得考虑的一种方案。尤其是速度不会受到显著影响的情况下,一开始就节约内存是很好的习惯,牺牲一点点CPU,可以为以后更多的功能被加入应用的情况建立良好的内存基础。

不常用对象的缓存避免内存占用

偶尔使用的数据,存储在SharedPreference中,或者db中,自定义结构的文件中。像SharedPreference是很高效的,它在内存中有一个map的缓存,并同步数据到手机存储。可以通过类似的手段,建立起适应自己程序的缓存框架。无论如何,由于Android程序的“一后台就可能被暂时性回收”的特点,一些“持续性”状态(就像session,cookie那样)你需要保存在非内存中。

速度&空间

程序设计,性能的考量就是空间和速度2方面的权衡取舍,可以以少量速度的牺牲——0.1秒和0.2秒打开页面,都属于“合格”的范畴——换取更少的内存占用。
Android设备内存越来越大,速度也越来越强悍,在保证速度的前提下,尽可能的节省内存。因为,app有一个不断开发完善的过程,随着功能的逐渐庞大,不论是安装包本身,还是运行时占用的内存,都只会不断的增加。只在内存中保持最必要的对象,避免不必要对象的驻留。一些操作中,例如ViewPager、ListView、GridView、RecycleView等,使用中复用View和集合对象,避免不必要的瞬间对象创建和释放——重用原则。
合理的使用文件来代替内存,缓存避免网络请求。
总结下,就是复用和缓存。

杂七杂八的建议

像Handler和Context这样的对象被一传两传的最后的引用变得极其复杂,然后就忘了该什么时候释放。实际上,能用ApplicationConttext的地方,就用ApplicationContext,需要用Activity的地方,就严格声明为Activity,而且第一个获得此引用的对象,有责任感知对应的Activity的生命周期,并在合适的时候断开和Activity的强引用,或者随Activity一起变为自由对象。
异步操作的监听器是很多类之间的通信手段,那么一些对象常驻内存的,如观察者模式的单例,其它的异步请求的回调,对它们的监听注册一定需要取消注册,被注册者需要管理好监听对象,做好null情况的处理。
其它的数据库操作,语音文件,都记得关闭和取消回调对象。
如果自己的程序具备一定规模的成熟产品,代码的规划和反复的设计改良是必须的,可以考虑引入EventBus,ORM框架来实现程序内的复杂数据通信,SQLite相关对象的生命周期的管理,从整体设计上统一规避各种潜在的内存问题。

小结

Android Monitor和 MAT的使用,内存泄漏的常见类设计。如何节省内存。

Android OOM 系列

标签:

原文地址:http://www.cnblogs.com/everhad/p/5374550.html

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!