欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Android 系统稳定性 - OOM(一)

程序员文章站 2022-07-04 10:57:51
2.1.1 什么是内存溢出 2.1.2 为什么会有内存溢出 android 主要应用在嵌入式设备当中,而嵌入式设备由于一些众所周知的条件限制,通常都不会有很高的配置,特别是内存比较有限。如果我们编...

2.1.1 什么是内存溢出

2.1.2 为什么会有内存溢出

android 主要应用在嵌入式设备当中,而嵌入式设备由于一些众所周知的条件限制,通常都不会有很高的配置,特别是内存比较有限。如果我们编写的代码当中有太多的对内存使用不当的地方,难免会使得我们的设备运行缓慢,甚至是死机。为了能够使安全且快速的运行,android 的每个应用程序都运行在单独的进程中,这个进程是由 zygote 进程孵化出来的,每个应用进程中都有且仅有一个实例。如果程序在运行过程中出现了内存泄漏的问题,只会影响自己的进程,不会直接影响其他进程。

java虽然有自己的垃圾回收机制,但并不是说用java编写的程序就不会内存溢出了。java程序运行在虚拟机中,虚拟机初始化时会设定它的堆内存的上限值,在android中这个上限值默认是“16m”,而你可以根据实际的硬件配置来调整这个上限值,调整的方法是在系统启动时加载的某个配置文件中设置一个系统属性:

dalvik.vm.heapsize=24m

当然也可以设置成更大的值(例如“32m”)。这样android中每个应用进程的dalvikvm实例的堆内存上限值就变成了24mb,也就是说一个应用进程中可以同时存在更多的java数据对象了。有一些大型的应用程序(例如游戏)运行时需要比较多的内存,heapsize太小的话根本无法运行,此时就需要考虑调整heapsize的大小了。heapsize的大小是同时对整个系统生效的,原生代码中无法单独的调整某一个java进程的heapsize(除非我们自己修改,不过我们从来没这么做过)。

当代码中的缺陷造成内存泄漏时,泄漏的内存无法在虚拟机gc的时候被释放,因为这些内存被一些数据对象占用着,而这些数据对象之所以没有被释放,可以归结为两类情况:

a) 被强引用着

例如被一个正在运行的线程、一个类中的static变量强引用着,或者当前对象被注册进了framework中的一些接口中。

b) 被jni中的指针引用着

framework中的一些类经常会在java层创建一个对象,同时也在c++层创建一个对象,然后通过jni让这两个对象相互引用(保存对方的地址),binderproxy对象就是一个很典型的例子,在这种情况下,java层的对象同样不会被释放。

当泄漏的内存随着程序的运行越来越多时,最终就会达到heapsize设定的上限值,此时虚拟机就会抛出outofmemoryerror错误,内存溢出了。

2.2 容易引起内存泄漏的常见问题

2.2.1 cursor对象未正确关闭

关于此类问题其实已经是老生常谈了,但是由于android应用源码中的缺陷和使用的场合比较复杂,所以还是会时常出现这类问题。

1. 问题举例

cursor cursor = getcontentresolver().query(…);

if (cursor.movetonext()) {

… …

}

2. 问题修正

cursor cursor = null;

try {

cursor = getcontentresolver().query(…);

if (cursor != null && cursor.movetonext()) {

… …

}

} catch (exception e) {

… …

} finally {

if (cursor != null) {

cursor.close();

}

}

3. 引申内容

(1) 实际在使用的时候代码的逻辑通常会比上述示例要复杂的多,但总的原则是一定要在使用完毕cursor以后正确的关闭。

(2) 如果你的cursor需要在activity的不同的生命周期方法中打开和关闭,那么一般可以这样做:

在oncreate()中打开,在ondestroy()中关闭;

在onstart() 中打开,在onstop() 中关闭;

在onresume()中打开,在onpause() 中关闭;

即要在成对的生命周期方法中打开/关闭。

(3) 如果程序中使用了cursoradapter(例如music),那么可以使用它的changecursor(cursor cursor)方法同时完成关闭旧cursor使用新cursor的操作。

(4) 至于在cursor.close时需不需要try…catch(cursor非空时),其实在close时做的工作就是释放资源,包括通过binder跨进程注销contentobserver时已经捕获了remoteexception异常,所以其实可以不用try…catch。

(5) 关于deactive和close,deactive不等同于close,看他们的api comments就能知道,如果deactive了一个cursor,说明以后还是会用到它(利用requery方法),这个cursor会释放一部分资源,但是并没有完全释放;如果确认不再使用这个cursor了,一定要close。

(6)除了cursor有时我们也会对database对象做操作,例如要修正mediaprovider中的一个attachvolume方法,在每次检测到attach的是一个external的volume时就重新建立一个,而不是采用以前的,那么在remove旧的数据库对象的时候不要忘记关闭它。

4. 影响范围

如果没有关闭cursor,在测试次数足够多的情况下,就会出现:

(1) 内存泄漏

我们先简单的看一下cursor的结构,这样会更好理解。数据库操作涉及到服务端的contentprovider和客户端程序,客户端通常会通过contentresolver.query函数查询并获取一个结果集的cursor对象。而这个cursor对象实际上也只是一个代理,因为要考虑到客户端和服务端在不同进程的情况,所以cursor的使用本身也是利用了binder机制的,而客户端和服务端的数据共享是利用共享内存来实现的,如下图所示。


Android 系统稳定性 - OOM(一)

客户端和服务端使用的cursor经过了层层封装,显得十分臃肿,但它们的工作其实可以简单的从控制流和数据流两个方面来看。在控制流方面,客户端为了能和远端的服务端通信,使用实现了ibulkcursor接口的bulkcursorproxy和cusortobulkcursoradapter对象,例如要获取结果集数据时,客户端通过bulkcursoryproxy.onmove函数调用到cursortobulkcursoradapter.onmove函数,然后再调用到sqlitecursor.onmove函数来填充数据的。在数据流方面,服务端的sqlitecursor将从数据库中查询到的结果集写入到共享内存中,然后binder调用返回到客户端,客户端就可以从共享内存中获取到想要的数据了。客户端的控制流和数据流的访问由bulkcursortocursoradapter负责,服务端则是分别由cursortobulkcursoradapter和sqlitecursor负责。

如果cursor没有正常关闭,那么客户端和服务端的cursorwindow对象和申请的那块共享内存都不会被回收,尽管其他相关的java对象可能由于没有强引用而被回收,但是真正占用内存的通常是存放结果集数据的共享内存。大量的cursor没有关闭的话,你可能会看到以下类型的异常信息:

创建新的java对象时发现没有足够的内存,抛出内存溢出错误:outofmemoryerror

创建新的cursorwindow时无法申请到足够的内存,可能的异常信息有:
runtimeexception: no memory for native window object
illegalstateexception: couldn’t init cursor window
cursorwindow heap allocation failed
failed to create the cursorwindow heap

(2) 文件描述符泄漏

当然有可能很幸运,每次查询的结果集都很小,做几千次查询都不会内存溢出,但是android的linux内核还有另外一个限制,就是文件描述符的上限,这个上限默认是1024。

文件描述符本身是一个整数,用来表示每一个被进程所打开的文件和socket,第一个打开的文件是0,第二个是1,依此类推。而linux给每个进程能打开的文件数量设置了一个上限,可以使用命令“ulimit -n”查看。另外,操作系统还有一个系统级的限制。

每次创建一个cursor对象,都会向内核申请创建一块共享内存,这块内存以文件形式提供给应用进程,应用进程会获得这个文件的描述符,并将其映射到自己的进程空间中。如果有大量的cursor对象没有正常关闭,可想而知就会有大量的共享内存的文件描述符无法关闭,同时再加上应用进程中的其他文件描述符,就很容易达到1024这个上限,一旦达到,进程就挂掉了。

提示:可以到系统的“/proc/进程号/fd”目录中查看进程所有的文件描述符。

(3) gref has increased to 2001

先说明一下“死亡代理”的概念。利用binder做进程间通信时,允许对binder的客户端代理设置一个deathrecipient对象,它只有一个名为binderdied的函数。当binder的服务端进程死掉了,binder驱动会通知客户端进程,最终回调deathrecipient对象的binderdied函数,客户端进程可以借此做一些清理工作。

需要注意的是,“死亡代理”的概念只对进程间通信有效,对进程内通信没有意义;另外,binder的客户端和服务端的概念是相对的,例如bulkcursorproxy是cursortobulkcursoradapter的客户端,而后者又有一个icontentobserver的客户端,其对应的服务端在bulkcursortocursoradapter的getobserver函数中创建。这里需要关注的就是在cursortobulkcursoradapter对象被创建时,会同时将该对象注册为icontentobserver的客户端对象的“死亡代理”,代码如下:

cursortobulkcursoradaptor的内部类contentobserverproxy的构造函数中

public contentobserverproxy(icontentobserver remoteobserver, deathrecipient recipient) {

super(null);

mremote = remoteobserver;

try {

//此处的recipient就是cursortobulkcursoradapter对象

remoteobserver.asbinder().linktodeath(recipient, 0);

} catch (remoteexception e) {

}

}

“死亡代理”对象的引用会被native层的binder代理对象的mobituaries集合引用,所以“死亡代理”对象及其关联对象由于被强引用而不会被垃圾回收掉,同时jni在实现linktodeath函数的过程中也创建了一些具有全局性的引用,被称作“global reference(简写为gref)”,每一个gref都会被记录到虚拟机中维护的一个“全局引用表”中。

eng模式下,jni全局引用计数(gref)有一个上限值为2000,如果大量cursor对象没有被正常关闭,服务端进程就会因为“死亡代理”对象的创建使得虚拟机中的全局引用计数增多,当超过2000时,虚拟机就会抛出异常,导致进程挂掉,典型的异常信息就是“gref has increased to 2001”。

提示:全局引用计数的上限2000已经是一个比较大的值,正常情况下很难达到。android在eng模式下开启这项检查,就是为了能够在开发阶段发现native层的内存泄漏问题。在usr模式下这项检查会被禁用,此时如果有内存泄漏就只有等到抛出内存溢出错误或者文件描述符超出上限等其他异常时才能发现了。

cursor未正常关闭是导致gref越界的原因之一,后续会在其他章节中详细讨论。

2.2.2 释放对象的引用

内存的问题是bugzilla中的常客,经常会在不经意间遗留一些对象没有释放或销毁。

1. 静态成员变量

有时因为一些原因(比如希望节省activity初始化时间等),将一些对象设置为static的,比如:

private static textview mtv;

… …

mtv = (textview) findviewbyid(…);

而且没有在activity退出时释放mtv的引用,那么此时mtv本身,和与mtv相关的那个activity的对象也不会在gc时被释放掉,activity强引用的其他对象也无法被释放掉,这样就造成了内存泄漏。如果没有充分的理由,或者不能够清楚的控制这样做带来的影响,请不要这样写代码。

2. 正确注册/注销监听器对象

经常要用到一些xxxlistener对象,或者是xxxobserver、xxxreceiver对象,然后用registerxxx方法注册,用unregisterxxx方法注销。本身用法也很简单,但是从一些实际开发中的代码来看,仍然会有一些问题:

(1) registerxxx和unregisterxxx方法的调用通常也和cursor的打开/关闭类似,在activity的生命周期中成对的出现即可:

在 oncreate() 中 register,在 ondestroy() 中 unregitster;

在 onstart() 中 register,在 onstop() 中 unregitster;

在 onresume() 中 register,在 onpause() 中 unregitster;

(2) 忘记unregister

以前看到过一段代码,在activity中定义了一个phonestatelistener的对象,将其注册到telephonymanager中:

telephonymanager.listen(l,phonestatelistener.listen_service_state);

但是在activity退出的时候注销掉这个监听,即没有调用以下方法:

telephonymanager.listen(l,phonestatelistener.listen_none);

因为phonestatelistener的成员变量callback,被注册到了telephonyregistry中,telephonyregistry是后台的一个服务会一直运行着。所以如果不注销,则callback对象无法被释放,phonestatelistener对象也就无法被释放,最终导致activity对象无法被释放。

3. 适当的使用softreferenceweakreference

如果要写一个缓存之类的类(例如图片缓存),建议使用softreference,而不要直接用强引用,例如:

private final concurrenthashmap> mbitmapcache = new concurrenthashmap>();,>,>

当加载的图片过多,应用可用堆内存不足的时候,就可以自动的释放这些缓存的bitmap对象。

关于java中的强引用、软引用、弱引用和虚引用是一些比较重要的概念,在android开发中经常会用到。

2.2.3 构造 adapter 时,没有使用缓存的 convertview

以构造 listview 的 baseadapter 为例,在 baseadapter 中提供了以下方法:

public view getview(int position,view convertview,viewgroup parent)

来向 listview 提供每一个 item 所需要的 view 对象。初始时 listview 会从 baseadapter 中根据当前的屏幕布局实例化一定数量的 view 对象,同时 listview 会将这些 view 对象缓存起来 。当向上滚动listview 时,原先位于最上面的 list item 的 view 对象会被回收,然后被用来构造新出现的最下面的 listitem。这个构造过程就是由 getview()方法完成的,getview()的第二个形参 view convertview 就是被缓存起来的 list item 的 view 对象(初始化时缓存中没有 view对象则 convertview 是 null)。由此可以看出,如果我们不去使用 convertview,而是每次都在 getview()中重新实例化一个 view 对象的话,即浪费资源也浪费时间,也会使得内存占用越来越大listview 回收listitem 的 view 对象的过程可以查看:android.widget.abslistview类中的addscrapview(view scrap) 方法。

示例代码:

public view getview(int position,view convertview,viewgroup parent) {

view view = new xxx(…);

… …

return view;

}

修正示例代码:

public view getview(int position,view convertview,viewgroup parent) {

view view = null;

if (convertview != null) {

view = convertview;

populate(view,getitem(position));

} else {

view = new xxx(…);

}

return view;

}

2.2.4 bitmap 对象不再使用时调用 recycle()释放内存

有时我们会自己操作 bitmap 对象,如果一个 bitmap 对象比较占内存,当它不再被使用的时候,可以调用 bitmap.recycle()方法回收此对象的像素所占用的内存,但这不是必须的 ,视情况而定。可以看一下代码中的注释:

/**

* free up the memory associated with this bitmap’s pixels,and mark the

* bitmap as “dead”,meaning it will throw an exception if getpixels() or

* setpixels() is called,and will draw nothing. this operation cannot be

* reversed,so it should only be called if you are sure there are no

* further uses for the bitmap. this is an advanced call,and normally need

* not be called,since the normal gc process will free up this memory when

* there are no more references to this bitmap.

*/