节省你的内存

来源:互联网 发布:廖承宇直播软件 编辑:程序博客网 时间:2024/05/16 12:24

我刚开始接触手机开发的时候,一昧地认为让程序跑的越快就越好,完全忽略内存是否够不够用,而事实上,手机的CPU速度总是比我们想象的要快,而内存的容量总是比我们想象的要少。忽略内存的使用情况有时候好比一条贪吃的蟒蛇,企图一次吞下大象;有时候好比消化系统出现了问题,只进不出,俗称“BM”。

 

在 android 开发中这两种情况都有可能发生,当我们批量加载图片时,有可能因为同时写入内存的字节过多,导致内存不够用,出现OOM(Out OfMemeory)——蟒蛇把肚皮撑破了;当我们进行数据库查询返回 Cursor 时,有可能因为忘记使用完 Cursor 后将其关闭释放资源而导致内存泄漏——消化不良了。

 

以下分别对这两种情况进行说明并提出一些需要注意的地方以避免发生OOM。


1 小心特殊指针Cursor——在大仓库寻找绣花针的烦恼

做 C/C++ 开发的童鞋经常苦恼于内存的管理,尤其是指针,它给我们带来高效率的同时也带来了不小的麻烦,所谓“花无百日红”,无论你多么仔细,忘记释放指针几乎是难免的事,而寻找这些隐患有时就如同大海捞针一般。


虽然在 Java 中省去了这些所谓的麻烦,但是在 android 开发中又引入了一个游标机制——Cursor,它抓住内存资源不放的行径就如同 C/C++ 指针一般厚颜无耻,忘记释放Cursor 在数据库查询中经常出现,要想避免这样的情况出现没有别的捷径,我们只能时刻提醒自己:

 

谁产生了Cursor谁就要负责释放它

 

这条原则是如此的眼熟,简直像极了C/C++ 中的指针管理以及 Object-c 中的 retain count 管理。

 

记住一些Cursor管理的经验:

 

  • 在 finally 中释放 Curosr——“It’s safe now”,引人入胜的电影情节总不会让这句台词得逞

一旦你能全程操控 Cursor 的生命周期,请记住:一定要在 finally 中释放 cursor,请看下面的伪代码块:


public void foo() {Cursor c = query to get a cursor;//use the cursorif (something occured) {return or throw exception;}//in the end you thoughtif (c != null) {c.close();}}



在 foo 方法中,你原本以为在函数的结尾释放了 cursor,就此平安无事了,可未曾料到前面的语句中有返回或者异常的出现,而此时 cursor 将被忘记释放。也许你认为这样的错误太过低级,稍微留意就不会出现,这里只是简化了实例,一旦程序逻辑复杂到一定程度,则这样的低级错误往往很难避免,解决的办法就是,只要你全程管理了一个 cursor,就将其植入 try{}finally块中,并在 finally 中释放 cursor:


public void foo() {Cursor c = query to get a cursor;if (c != null) {try {//use the cursorif (something occured) {return or throw exception;}} finally {c.close();}}}


这样,无论你的 try 中有多少可能的返回或异常出现,finally 中的语句总会在最后执行,从而也可避免在 try 中的每一处退出程序的地方都进行cursor的检查释放,省去很多麻烦和隐患。

 

  • CursorAdapter 之殇——孩子不能依赖父母太甚

当我们用 CursorAdapter 为列表产生数据时,我们无法全程对 Cursor 进行管理,于是无法引用前一条规则,事实上CursorAdapter 动态实现了对老 cursor 的释放:

 

当我们对 Cursor 进行更新后都需要通过 CursorAdapter.changeCursor(c); 方法将新的 cursor 传入CursorAdapter 中以便重新生成新的数据并更新列表。而在 changeCursor() 方法中就实现了对前一个 cursor 的释放(请参考 CursorAdapter 源代码:http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.2.2_r1/android/widget/CursorAdapter.java#CursorAdapter.changeCursor%28android.database.Cursor%29)。

 

这样看起来好像 CursorAdapter 已经帮我们安排好了一切,它对 Cursor 的管理是如此的完美,就好比富二代的父母安排孩子上学、工作,让孩子误认为一切都有父母,一切依赖父母,一旦失去依靠则无法生存。

 

你肯定发现了,CursorAdapter 虽然帮我们释放了所有老的 cursor,可最后一个 cursor呢?我们往往记得有更新时需要调用 changeCursor(),让我们产生 changeCursor() 只在我们需要更新的时候才需要用到的错觉。

 

CursorAdapter第一原则:用完CursorAdapter 之后调用CursorAdapter.changeCursor(null); 释放最后一个 cursor。

 

第一原则适用于 CursorAdapter 本身及其所有子类,如:ResourceCursorAdapter。

 

此外,还需要注意 cursor 的释放时机:

 

CursorAdapter 第二原则:调用CursorAdapter.changeCursor(null); 的次数和 多次查询产生的 Cursor个数相等。

 

千万不要忘记在你的 Activity 中反复执行了多少次同样的查询,每次查询都会重新生成一个新的 Cursor,那么释放次数也应和生成的 Cursor 次数相等。这些反复查询有可能是你自己进行的,也有可能是 API 内部进行的,总之,要小心地检查,不要遗漏。

 

  • 避免过早地释放 Cursor——嘿,服务员,我还没吃完呢

在餐厅你可能会遇到这样的尴尬,饭吃到一半,出去接了个电话或者上了趟WC,回来一看,碗筷被收走了……

 

这里还要补充一下 cursor 的释放时机,我们知道滞后释放会引起内存泄漏,那么过早释放就会引起空引用的致命异常,就好比前面提到的餐厅事件。

 

记住以下两条经验:

 

1.   释放Cursor 之前一定要确保它在当前线程中永远不会再被使用到

 

2.   尽量不用一个 Cursor 变量引用多个 Cursor 生成来源,否则会让 cursor 的管理变得混乱,很容易导致错误释放。


 

2 小心载入Bitmap——贪吃蟒蛇的悲剧

       在应用中载入图片是 android 开发中的普遍需求,无论是网络下载的图片还是从外存加载的图片,小则几K,大则几M,若一次加载图片不多,几乎无需考虑图片对内存的占用问题,然而这样就安然无事了吗?考虑以下几种情况:

 

1.    没错,你一次只需加载一张图片,而某时你的程序突然加载了一张超过10M的大图(系统给一个进程分配的内存上限是10M);

 

2.    你加载的每一张图片确实很小,但是你需要批量加载,比如在 GridView中,每次加载的图片大概在三屏60张左右,假设平均一张图片超过200K,请算算需要多少内存?

 

看看我们的图片是不是像极了一条贪吃的大蟒蛇,如果不加提防,一旦它饥饿起来会疯狂吞噬你的内存!那如何解决呢?两个原则:

 

1.   缩小图片,控制图片大小;

2.   手动分配内存使其不超过上限,重复利用分配的内存给图片——需要时给,不需要就收回。

 

2.1 控制单张图片的大小——一起来瘦身吧

第一种情况相对好解决,只需控制图片大小,具体来说加载图片之前先加载其宽高值并计算出图片字节大小,然后缩小其规格,使其字节大小不超过甚至远小于内存上限即可。图片字节大小的计算公式如下:

 

BitSize = width X height X BPP

 

width和 height 分别表示图片的宽和高,单位为像素,BPP(bit per pix)表示单位像素所占的字节位数。以下是一个生成图片缩略图的例子:


static final int MAX_SIZE = 1024 * 1024 * 8; //每张加载图片所占内存限制 1MBBitmapFactory.Options options = new BitmapFatory.Options();options.InJustDecodeBounds = true; //只加载边界,无需占用内存BitmapFatory.decodeFile(path, options);//android默认采用ARGB_8888颜色模式,即BPP = 32位int srcSize = options.outHeight * options.outWidth * 32;if (srcSize > MAX_SIZE) {int scale = Math.ceil((double)MAX_SIZE / (double)srcSize);options. InJustDecodeBounds = false;//严格来说应该是scale的二次开方,但我们尽量缩小options.inSampleSize = scale; //如果对颜色质量要求不高可选择此项,能将BBP 置为更小的16位每像素options.inPreferredConfig = Bitmap.Config.ARGB_4444;bitmap = BitmapFatory.decodeFile(path, options); //重新解码加载图片}


几点注意:

1.    一般来说在不太苛刻的情况下,控制好你所需要的图片宽高值就行了,无需计算出图片的字节数;

 

2.    使用BitmapFactory解码图片时尽量不要使用 decodeResource 方法,而要使用 decodeStream 或者 decodeFile 方法,decodeFile 其实也是调用了 decodeStream,因为 decodeStream 调用了底层的 native 方法进行解码,需要更少的额外内存;

 

3.    如果对图片颜色质量有要求,则不要改变 inPreferredConfig选项。

 

2.2 合理分配可用内存——不要占着茅坑不…

对于大批量加载图片的情况,仅仅依靠对单张图片瘦身还是不够的,就算图片再小,理论上当加载总量超过一个阈值时还是会出现内存不够用的情况,这样的情况很容易出现在列表类View中快速加载图片的时候。

 

比如在 GridView 中加载图片时,为了避免多次生成图片,通常需要将已经生成的图片放入 HashMap 缓存中以便重复利用,如果对缓存不加限制,那么缓存容量将直线上升,相反可用内存容量也将直线下降,直至内存耗尽。

 

那么,如何避免内存被耗尽呢?简言之,就是——需要时就给,不需要时及时收回。对操作系统原理很熟悉的童鞋一定马上想到了操作系统的内存分配策略,那我们就在应用层次模仿系统层次的内存管理吧。

 

这里以类FIFO策略和LRU策略分别加以说明。

 

  • 类FIFO内存分配策略——嗯,不要自欺欺人,其实我们都喜新厌旧

FIFO的概念我想不用多说,关键是如何构建一个这样的有序列表,让我们存入其中的bitmap 是按照生成的先后顺序进行排列的?也许你觉得这有什么难的,顺序加入 List 容器不就 OK 了吗?的确,可是别忘了,这样一来复用的时候我怎么知道取哪一个呢?没错,别忘了每一个bitmap 的唯一标识:URI(也许是文件路径,也许是URL)。

 

看来还得麻烦 HashMap,而且必须是有序的 HashMap,java util 中给我们提供了这样的HashMap——LinkedHashMap。

 

首先,让我们来看看类FIFO策略的具体运作流程吧,如下图所示:




图中的 Hard Cache 可以用LinkedHashMap实现,以“path - bitmap”元素对存入。之所以叫类FIFO策略是因为该策略不是严格的FIFO,从图中可以看出,新的 bitmap 进入有序缓存(Hard Cache)是按照时间顺序排列的,但是出去的bitmap并不总是最老的那个,为了让 Hard Cache 时刻保持最新,我们将复用过一次的 bitmap 都丢到 Soft Cache 中,此外, Hard Cache 的容量是有限的,当超过容量上限则将最老的一个 bitmap 也丢进 Soft Cache。

 

Soft Cache 是什么呢?我们可以将其视为二级缓存,一般用可同时读写的ConcurrentHashMap 实现,也以“path - bitmap”元素对存入,只不过其中的 bigmap 采用 SoftReference 弱引用,这些优先级最低的 bitmap 将随时被系统回收,同时它还起到备用作用,当从 Hard Cache 中取不到所要的 bitmap 时,可以再次从 Soft Cache 中遍历到,如果该 bitmap 存在且未被回收的话。若两者没有,则重新生成(下载或从外存中获取)。

 

下图描述了从缓存中读取 bitmap 的过程:

 


总结一下,类FIFO策略的关键是:新进入的比后进入的优先级高,复用过的和最后进入的可以被系统回收

 

该种策略的实现方法请参考 Tim Bray 的安卓开发博客(需要翻墙):http://android-developers.blogspot.com/2010/07/multithreading-for-performance.html 

或者看我转载的博文:Multithreading For Performance

  • LRU内存分配策略——家里最近不用的杂物太多,又占地方,当废品卖掉吧

往下阅读之前,请大家抽空复习下操作系统的LRU内存分配策略,虽然在这里不需要大家手动实现它,但熟悉一下总是好的,能让你明白该种策略的价值所在。

 

LRU的核心是对于最近最少使用到的内存块将其释放,以便可以让新进事务利用。AndroidAPI 提供了一个实现了 LRU 算法的 HashMap——LruCache,实际上是 LruCache 引用了一个 LinkedHashMap 的实例,并通过 LRU 算法实现对 LinkedHashMap 的缓存操作。大家在 bitmap 的缓存中可以直接采用 LruCache 来实现。

 

最简单的LruCache 缓存 bitmap 的方式就是只需要一个独立的 LruCache,分配给固定的内存空间,使其按照 LRU 策略对缓存的 bitmap 实施淘汰,如下图所示:

 



当然,如果想有效缓存更多的图片,你还可以选择采用一级LruCache 和二级类FIFO Cache 相结合的方法,或者 一级LruCache 与 二级Soft Cache 相结合的方法,如下图所示:

 

总结一下,LRU 策略的关键是:采用LRU 策略对 bitmap 进行缓存淘汰,LRU 策略采用 android API 中的 LruCache 实现

 

具体的实现方法请参考 LruCache 的谷歌官方文档:http://developer.android.com/reference/android/util/LruCache.html。

你也可以看我转载的博文:LruCache 结合 FIFO 策略实现bitmap缓存

 

最后总结一下,无论是类 FIFO 还是 LRU,一个总的原则是:禁止永久缓存你的 bitmap,这里永久的意思是在一个线程生命周期内。

 

现在安全了吗?NO!

 

  • 控制 bitmap 的生成时机——百米冲刺,你能持续做有氧呼吸么?

难道对缓存进行了合理的控制还不够?对的,还不够,静态地控制缓存的容量算是解决了一部分问题,这是显而易见的,然而隐藏在深处的动态产生的内存呢?

 

好比说我们要批量加载 bitmap,一般是在 UI 线程以外的线程中进行的,通常是一个 bitmap 由一个 AyncTask 来产生,当你快速滑动一个GridView的时候,大批量的 bitmap 加载将生成大量的 AyncTask,同时也将产生大量的bitmap,这些bitmap 被快速地丢入缓存又从缓存中丢弃,然而这些被丢弃的 bitmap 不会马上被释放掉,无论它们是 SoftReference 还是被 recycle(),都只是告诉垃圾回收器,这些是可以被系统回收的,但我们知道垃圾回收器是不会马上将其回收的,这个回收时机我们无法控制,只能由 VM 来决定。

 

于是当这些等待被回收但还没有马上被回收的 bitmap 瞬间达到一个峰值时,噢噢,OOM了……

 

所以,我们要控制 bitmap 的生成时机,注意到当我们在 Fling GridView 的时候,我们更在意列表的滑动流畅度,至于图像的加载则期望在 GridView 停止时马上完成,因为此时目光的注意力最集中。所以一种解决方案是,在列表快速滑动时无需生成 AyncTask 加载 bitmap,而只有当滑动停下时才快速加载。就好比运动员在百米冲刺时机体只需做无氧呼吸,而停下后则尽情地享受空气。


具体实现请看我的博文:在列表中控制 AsyncTask 加载 bitmap 的时机 


一种更为理想的方案是,根据列表的滑动速度来判断是否加载 bitmap,当速度慢到一个阈值时则加载,否则不加载。实际上第一种方案是此方案的一种特殊情况,即滑动速度为0时加载。

 

3 其它常见内存泄漏——互联网大拿们,你们怎么看?

1.   Context 泄漏,这种内存泄漏通常发生的非常隐秘,它通常是由于你在 Activity 的控制范围之外(比如静态区)长期引用了Activity 所致,Romain Guy 在他的博文中对此有详细说明:

 

Romain Guy 的博文(原文,需翻墙):

http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html

 

该文的山寨版翻译链接:

http://blog.csdn.net/sunchaoenter/article/details/7209635

 

2.   Alert Dialog 泄漏,通常我们在程序中需要创建 Alert Dialog 来发出警告信息,但是往往在Activity 销毁或者重新加载元素(旋转)之前忘记 dismiss,从而引起 Dialog 资源的泄漏,详细请看 Justin Schultz 的博文:

 

http://publicstaticdroidmain.com/2012/01/avoiding-android-memory-leaks-part-1/

 

3.   register receiver 泄漏,registerReceiver 之后忘记 unregisterReceiver

4.   InputStream/OutputStream 泄漏,忘记关闭输入输出流

 

最后两点来自博文:http://www.linuxidc.com/Linux/2011-10/44785.htm

 


原创粉丝点击