大家好!本人是即将毕业学生一枚,闲暇时间经常看大神们写的博客学到很多东西。最近在做毕业设计的时候遇到一些问题,然后把自己的问题和解决方法总结一下,有不对的地方希望大家多多包涵,提出批评与指导。
这篇博文主要介绍使用AndroidStudio对内存进行分析和跟踪,还有就是从源码角度解决ImageLoader引起的OOM问题。
我正在做的项目使用到了ImageLoader来加载图片,我也是第一次使用,就拿来直接用了。写完的代码运行很正常的加载图片,并没有发现什么问题。但是在测试的时候发现了问题。当多次打开软件中一个浏览大图片的Activity(每次的图片都不一样)后,这时在这个Activity中点击返回后,屏幕会突然变黑,然后回到了MainActivity,并没有回到上一级Activity,甚至连之间打开的多个Activity都没有返回。想了一下问题怀疑可能是重启了这个应用。但那个大图片的Activity开始打开的几次都可以正常的回到上一级Activity,再多打开几次就不能正常回退了,这时候我就开始看LogCat打出的日志,看看有没有发生什么异常。发现ImageLoader产生了OOM的Bug。如下图:
然后上网上查了一下ImageLoader引起内存泄露的问题,发现有很多人遇到,但说到解决的办法的,并没有很好的答案。大部分都是说ImageLoaderConfiguration.Builder(context)和DisplayImageOptions.Builder()里面的配置问题,但并不能解决这个问题。还有人说换一个图片加载的框架,像Universal-Image-Loader(UIL)和Google的Volley框架,我尝试换了Universal-Image-Loader(UIL)这个,但发现还是有这个问题。而且项目换一个框架改动比较大,就没有尝试Volley框架。还有人说是应为使用了cacheInMemory(true)和cacheOnDisc(true)这两个属性导致的,但是我将他们设为false还是会引发OOM,所以就自己用工具分析。
既然产生了OOM,我们就要分析内存,看看具体是什么原因导致的。使用Android Studio工具(发现很多地方都比eclipse强大),在使用AndroidStudio的Memory工具观察的时候发现了问题,在我多次打开这个Activity的时候,发现Memory在一直的增长,每次Activity退出后Memory也没有下降。
就这样当我多次浏览不同的图片,多次打开这个Activity后,这个Memory就一直的增长,当增长到120+M的时候应用突然挂掉,之后又自动重启。发现了这个现象我们就可以借助AndroidStudio的强大工具来分析导致这个问题的原因。在Memory窗口的左边有四个按钮,分别是:
Enabled(蓝色的开关):就是一个正常的开关功能
Initiate GC(橙色小卡车):就是手动调用GC,我们在抓内存前,一定要手动点击 Initiate GC按钮手动触发GC,这样抓到的内存使用情况就是不包括Unreachable对象的(Unreachable指的是可以被垃圾回收器回收的对象,但是由于没有GC发生,所以没有释放,这时抓的内存使用中的Unreachable就是这些对象)
Dump Java Heap(紫色带向下的箭头):获取hprof文件(hprof文件是我们使用MAT工具分析内存时使用的文件),但这里直接产生的文件MAT还不能直接使用,需用转换成标准的hprof文件。可以使用AndroidStudio转换或者用hprof-conv命令转化,网上可以查到。
Start Allocation Tracking(紫色带圆圈):开始分配追踪,第一次点击可以指定追踪内存的开始位置,第二次点击可以结束追踪的位置。这样我们截取了一段要分析的内存,等待几秒钟AndroidStudio会给我们打开一个Allocation视图(感觉和MAT工具差不多,不过MAT工具更加强大,我们也可以获取hprof文件,使用MAT来分析)例如下图:
我截取了这段内存,接下来我们就看Allocation视图来分析。
通过视图中的内容我们首先看到1号线程它占用了百分之92的内存,那么我们就点开它看一下,每次都只要点开其中内存占用最大的就可以。
到划红线的那个位置就可以了,这里已经到了安卓的graphics源码的层次,所以我们不需要再向下看了。我们看一下红线上面的ImageLoader里的那个decodeStream方法:
它里面就是调用了BitmapFactory.decoceStream()方法然后返回了一个Bitmap对象。基本上已经可以确定占用如此多内存的就是Bitmap对象,那么我们只要将这个Bitmap释放掉就可以了。那么就接着分析看看在什么地方释放,我们看一下最上面的ImageLoader.core包下的LoadAndDisplayImageTask类的run()方法,发现里面有一个Bitmap对象,他是调用了内部tryLoadBitmap()方法给赋值的,而这个方法最后其实就是我们上面看到的调用的decodeStream方法返回的Bitmap。应为这之间的Bitmap都是方法里的局部变量所以我们不用考虑这些Bitmap的释放,那就接着向下看,发现这个Bitmap被传到了DisplayBitmapTask这个类中,那么我们再进入到这个类中看发现了一个惊喜:
这个对象被传到了DisplayBitmapTask的成员变量中,DisplayBitmapTask本身也实现了Runnable接口,那么我就想是不是应为DisplayBitmapTask这个类没有及时回收导致的Bitmap成员变量占用内存的问题,所以我就在run()方法跑完之后,让Bitmap = null。然后又将程序运行了一遍,但是发现根本没有解决这个问题。这时候又重头想了一下整个过程,发现我的操作是不对的,虽然我成功将DisplayBitmapTask中的Bitmap成员变量置为了空,但是他只是真正的Bitmap内存的一个引用,我并没有将真正的Bitmap内存空间所释放,只是将他的一个引用给释放了。通过网上查询发现,Bitmap会保存在C层的内存中,如果我们想要释放他在C层中的内存,可以调用Bitmap的recycle()方法,从代码中看,这个方法本身会调用Bitmap.cpp中的Bitmap_recycle方法,这个是native方法。这样就可以通知底层将C层中的Bitmap内存释放掉,而且还会将一些相关的引用计数置0。但是并不会立即释放掉,这要看系统。
一下子完美解决。既然查到了原因,那就好办了,我将Bitmap = null这句话改为Bitmap.recycle()。好了,接下来我再次运行,发现这回应用直接就崩了,只要能在Logcat中抓到Log,一切问题都好解决。看了一下log发现下面这个Error:
通过字面理解的意思就是,我们的View去显示了已经recycle的Bitmap。仔细想一想确实是,我们在ImageLoader中的一个线程的run方法中直接将Bitmap释放掉了,那我们的应用岂不是直接显示一个空的Bitmap,这当然会引起错误。既然不能在这里释放掉Bitmap,那我们应该在什么地方释放呢?那一定是在我们的View不再使用这个Bitmap的时候调用,想一想,那我们只要在Activity或者fragment的onDestory()方法中释放了不就可以了吗?但这时又有一个问题产生,我们该如何在上层应用获取到这个View用到的Bitmap的对象呢?想了半天发现好像只能通过改写ImageLoader的源码来想办法将这个对象会传到上层应用。正当我在ImageLoader中寻找在哪里写这个回调方法时,突然我看到了这段代码:
发现其实ImageLoader的源码中已经为我们写好了这个回调监听器接口,而且在ImageLoader的displayImage方法中,也重载了一个提供了传递ImageLoaderListener实现对象的方法。这样我们只需要实现ImageLoaderListener这个接口,并实现他的onLoadingComplete方法,在这个方法中对Bitmap做处理就可以了。
我们只需要在Activity或Fragment的onDestory()方法中再调用cleanBitmapList()方法就可以了。通过上面的更改我再次运行应用,发现真的解决了这个问题,我的应用内存不再会是一直的上升,而是上升之后又会下降。如图:
注:这个方式比较适合cacheInMemory(false)和cacheOnDisc(false)的情况写,应为如果你将这两个设置为true时,那么下次再次加载图片时他会从内存和硬盘中去加载图片,这两个地方也持有Bitmap的引用,这时就会再次出现trying to use a recycled bitmap 这个错误。当然你也是可以设置为true的,只要在清除Bitmap(也就是我们这里调用cleanBitmapList)的地方再加上mImageLoader.clearDiscCache()和mImageLoader.clearMemoryCache()这两句话就可以。
好了,整个ImageLoader引起的OOM问题额分析与解决的过程就是这些,希望能对需要的人有帮助,通过这个问题我也学会到,遇到问题最好的解决办法就是我们要从最根本源码的角度去分析,这样既能学到很多东西,又可以解决困难问题。