不容易呀,第一次花了4个小时写一篇原创的技术文章。呵呵,本人文采和编码技术有限,是否写清楚了,也不知道是否对各位麻油有用,不管怎么着,作为自己的技术积累,还是得写出来,以供自己以后回忆,顺便为论坛为码友做点共享。
这篇文章是我在实际项目中使用的已被证实非常有效的一套内存管理机制。为了表现一下程序猿的应该有的共享精神,就此分享一下。
在android应用的中,造成内存泄漏的一般情况下最大的罪魁祸首自然非图片莫属了,如果其他普通的对象造成内存泄漏,那毋庸置疑,你的代码肯定出大的问题。
要解决内存问题,很多从J2EE/J2ME过来的人都认为用软引用弱引用即可,可是事实是:在占内存很大的bitmap对象面前,软引用弱引用显得力不从心。还有些人在用一种android最新的内存管理算法--称为LRU的东西,我的一些朋友就在用,听他们说貌似很不错,基本能解决绝大多数图片内存问题,但是貌似在一些比较极端情况下还是会内存溢出,比如说,页面图片很多,每张图片又比较大,此时如果快速滑动浏览图片,内存崩溃的几率还是很高的。至于其他的还有什么更高效的内存回收,小菜我也就不知道了。但是现在我想写写自己的经过实战总结出来的有效的内存回收办法,那么废话不多说,咱就来说道说道。要说我们的这套内存管理方法,就不得不说我们的这个项目架构背景了,由于我们采用单Acitivity的方式,所以所有的大页面(这里大页面就相当于传统多Acitivity方式中单个的全屏Acitivity)都是用ViewGroup或者其子类--这里我把大页面称为ViewPage,每个ViewPage包含一个或者多个独立的自定义View或者ViewGroup(或者其子类)--这些独立的View或者ViewGroup我称之为ViewChild。这样的UI,是不是很像Fragment呢?没错,这个完全可以用Fragment方式弄。出于效率的考虑,我们把应用要用到的所有ViewPage和ViewChild在应用启动的时候就实例化出来了,这就碰到一个非常棘手的问题,由于这些ViewPage和ViewChild当中要用很多携带背景或者SRC图片的系统控件,所以应用刚启动,内存就飙到了30M左右,天哪,这可得了? //注:看到这样的UI架构,很多人要骂了吧?呵呵,不但你们要骂,我更是在现实中跟架构师,也是我们的技术总监,天天吵架!因为这脑残的架构就是他的杰作。简直无法直视,但还是得解决啊!怎么办...???
在这样的背景下,对付内存的办法,可以说无所不用其极了,不夸张的说我们这套管理内存的机制,已经到达了登峰造极的地步。各种逻辑处理,算法处理,能用的内存处理API都基本用上了。下面,各位观众,干货要来了!
首先,“精准即时”回收图片。 说道“精准即时”,什么是精准,什么是即时?我们知道,使用java的软引用弱引用,既不能做到精准,更不能做到即时;这是由java的GC机制决定的,这个大家都知道,我不多说。精准,就是准确回收想要回收的某些图片;即时,就是想回收时,立即就回收之,而不是像java GC机制那样,回收的对象,回收的时机都是不确定的。下面就说说,怎么做到精准即时。 如何做到精准即时?为了实现这个目的,我们采用了图片分页的机制,即把属于某个页面的图片统一用一个标致标记起来,我们称之为sessionId,当页面要显示时,加载图片,给每一张图片设置上sessionId(至于如何给bitmap和sessionId做个映射,各有各法,这个就大家自己去思考了);当某一页面离开时,把属于这个sessionId的图片找出来,然后用bitmap.recycel() 一 一 全部回收之,这样既能做到精准,也能做到即时了。(传统的activity开发方式,页面什么时候加载,什么时候离开,我想大家都知道HO?). 做到精准即时,这是我们这套内存管理机制的核心,当然,光靠这点远远不够。下面继续。。。 其次,准确的控制图片的“加载时机”以及准确的控制“哪些图片该加载”。大家都知道,android当中,大量加载图片的地方无非就是ListView,GridView,Gallery等等可自由滑动的带有 不确定数目子视图的控件。一般这些控件如果带图片,在快速滑动的时候就会造成内存浮动过大,从而造成内存泄漏。一般的菜鸟,在使用这些控件时,都是让每一个子视图在执行getView方法时,直接就去加载图片了。这样可想而知,会造成两方面的浪费:一,不需要加载时,去加载了,这是一个浪费;二,不需要加载的也加载了,这是二次浪费。 看到这,有些人迷糊了。这就对了,你现在的疑问就是我要表达的--“加载时机”和“哪些图片该加载”的问题。你想啊,当你滑动这些控件的时候,很多图片都是匆匆而过,所以,一般滑动的时候,也是用户不想停下来看图片的时候,这时候,完全没必要去设置显示图片;不设置不显示,当然也就不会在内存中引用这些图片的对象,也就不会造成内存占用,这个就是“加载时机问题”。 而“哪些图片该加载”呢?很简单,当你不滑动时,停下来要看的显示在手机屏幕区域内的那几张图片才是你该加载的图片。那么,怎么做到“哪些图片该加载”,我的做法是,在getView的时候,给ImageView设置一个tag,这是tag的值是图片所在Item在ListView中的位置--position。当图片加载到内存,要设置显示到ImageView的时候,从这个ImageView对象取出tag,判断其是否在屏幕显示区域内的positon范围,怎么获取到这个范围呢?只要让adapter或者ListView所在的父View或者activty实现OnScrollListener,然后重写onScrollStateChanged(AbsListView view, int scrollState)方法,然后就可以通过AbsListView对象的getFirstVisiblePosition()和getLastVisiblePosition()获取到“屏幕显示区域内的positon范围”。还有一种方法,先把旧的postion保存起来,由于adapter中itemView是循环重用的这个ImageView的tag值postion会随着滑动不停的变化,我们可用旧position和新的position做比对,如果相等,说明此时的ImageView在屏幕显示区域内,否则就已经出了了屏幕外,此时就不用设置bitmap显示了。这两种方式,我都做通了,不必怀疑可行性,我们项目中采用的后者。实现思路是写了,具体代码怎么去写,这个大家自己去弄了,因为代码版权是属于公司,我的不便全盘奉送,这里只提供代码片段吧:
第一步,在adapter中给ImageView设置tag:
holder.iv_ico.setTag(position);// 这行代码相当关键,能够使滑出屏幕外的项中的图片即时被回收
第二步,当图片加载完,当要往ImageView中设置的时候,判断旧的position是否与新的position相等,如果不等,就不用吧图片设置到ImageView上了:
if(oldpos == position){
setImage( iv,...);
}
这里我得补充一点,如果图片需要网络获取的,你可以在滑动时候,去执行网络图片下载,但不要把图片设置显示;但一般情况下,我推荐在滑动时不要去执行网络获取图片,虽然提前下载到客户端而不显示,能给应用带来更好的性能,但是这样也给公司带来不少的带宽压力和服务器压力。 总之,原则就是:只在需要加载的时侯加载,只设置需要在屏幕上显示的那几张图片,就OK了。 以上是非xml布局中要设置显示的背景和SRC图片处理机制,那么,xml中各种控件的背景和SRC要不要做处理呢?如果是采用传统多Activity的方法,我觉得可以不必处理,但是对我们这个项目的UI架构来说,就不得不处理了。前面说了,由于我们应用在启动时,就把各个页面用到的ViewPage和ViewChilde都实例化了,一旦实例化,那么他们所关联的xml布局中的各种控件是不是要实例化?各种控件的背景啊,SRC啊是不是要加载上来?毋庸置疑的,所以就造成了前面我说的我们项目启动就达到30M内存的无法直视的问题! 怎么办?得抓住关键点,启动就占30M内存,谁是罪魁祸首?通过前面分析,很明晰了,就是xml布局中的各种控件所携带的背景和SRC。那么,如果让这些背景啊,SRC啊在应用启动的时候不进行加载?一条路,只能在java代码中去设置加载,而不能写死在xml各种控件的属性上.我的办法是,用一个map需要设置背景或者SRC的控件的R.id.xxxx和图片R.drawable.xxx id做一个映射。当View要显示的时候将图片设置上去,View不显示的时候将其背景或者SRC回收。具体代码实现,我的代码这里就不表了,想要例子的给我留言就行。 最后,还有一个解决大背景图片加载问题的终极必杀技(上几百KB,甚至几M的大背景图片都不用耗分毫内存)。pad或者大屏幕android设备中经常会用到大背景图片,我看了很多大屏幕设备上的很多应用,没几个应用回去用大背景。为什么?因为他们没掌握用大背景的方法,一旦用了,程序启动就可能内存溢出,要么提示你,图片太多,无法加载--因为android设备对内存上限以及单张图片的大小是有限制的,只是每一个厂商的设备的系统对这块的限制不一样而已。这个呢可以贴出来滴。如下:
public static Bitmap loadBmpWithLowestMenmory(/* InputStream is */String path, int reqWidth,
int reqHeight) {
BitmapFactory.Options options = new BitmapFactory.Options();
// 以最不占内存的方式加载图片
options.inPreferredConfig = Bitmap.Config.ARGB_4444;
// 是否可在图片draw完后,清除图片内存?我的理解是这样的。
options.inPurgeable = true;//这个属性设置是关键,必须设置为true,否则图片还是一样的耗内存
// 然后使用以下两种方式创造一个Bitmap对象,这个步骤也是关键:
// 因为BitmapFactory.decodeStream消耗的是c层的内存,而不消耗java层的内存,
// 所以,加载图片时,java层的内存是不会往上涨的,这个你从DDMS中可以观察到
Bitmap bmp = BitmapFactory.decodeFile(path, options);
// Bitmap bmp = BitmapFactory.decodeStream(is, null, options);
return bmp;
}
最最后,如果这样还解决不了你的内存问题,在2.3.0adk以下可以通过代码设置你的应用的内存上限(有些机器是不允许的)
// 增强本应用内存回收效率和加大本应 用最小堆内存分配
// VMRuntime.getRuntime().setMinimumHeapSize(CWJ_HEAP_SIZE);
// VMRuntime.getRuntime().setTargetHeapUtilization(HEAP_UTILIZATION);
;在3.x和4.x的系统上可以在准配置文件AndroidManifest.xml的<application android:largeHeap="true">,这样的话,你的应用可以无内存上限,直到把手机内存全部耗完为止,当然,这个做法是完全不被推荐的,这只是在实在没办法的情况下可以使用。