一. 设置图片产生的OOM问题
在开始讲显示图片产生的OOM问题前先介绍下两个基本概念:
1. bitMap和Drawable的概念:
BitMap代表一张位图,它将图像定义为由点(像素)组成,每个点可以由多种色彩表示,位图文件图像效果好,方便图像剪切、旋转、缩放等操作,但是位图是非压缩格式的,需要占用较大存储空间,不利于在网络上传送,在android开发过程中也容易发生oom问题。
而Drawable指的是是一个可画的对象,可能是一张位图,也可能是一个矢量图,还有可能是一个图层(LayerDrawable),所以Drawable可以说是对所有图片来源的封装。
2.设置图片产出的OOM错误:
有时在android 开发过程中,直接使用setImageBitmap调用BitmapFactory.decodeResource或者setImageResource和setImageDrawable来设置一张大图,当图片的数量过大时会产生内存溢出错误,主要是因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存。因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source,decodeStream最大的秘密在于其直接调用JNI>>nativeDecodeAsset()来完成decode,无需再使用java层的createBitmap,从而节省了内存空间。
其中setImageResource方法让图片显示在界面是在UI线程中实现的,而setImageDrawable则是独立的线程实现,所以综合来看,setImageDrawable是最省内存的。下面来对比下用不同的方法设置同一张900多K的图片:
(1).没有加载图片时:堆大小为8.990MB
(2).通过ImageView.setImageResource显示图片时,堆的大小为17.99MB。
(3).通过ImageView.setImageDrawable显示图片时,堆的大小为17.99MB。
(4).在布局文件.xml中通过src显示图片时,堆的大小还是17.99MB。
(5).在布局文件.xml中通过background显示图片时,堆的大小还是17.99MB。
(6) 通过以下代码显示图片时,Bitmap nBitmap = BitmapFactory.decodeResource(getResources(),R.drawable.highpicture);
nImageView.setImageBitmap(nBitmap);堆大小还是17.99MB。
(7) 通过以下代码显示图片时,InputStream is = getResources().openRawResource(R.drawable.highpicture);
Bitmap nBitmap = BitmapFactory.decodeStream(is);
nImageView.setImageBitmap(nBitmap);
堆大小还是17.99MB。
(8) 通过以下代码显示图片时,InputStream is = getResources().openRawResource(R.drawable.highpicture);
BitmapFactory.Options options =new BitmapFactory.Options();
options.inJustDecodeBounds=false;
options.inSampleSize = 10;
Bitmap nBitmap = BitmapFactory.decodeStream(is,null,options);
nImageView.setImageBitmap(nBitmap);
堆大小大大减少了,为9.148MB。
从上面的测试结果可以看出:各种直接设置图片的方法占用的内存Allocated都是基本上一样的,除非对图片经常处理后再显示才能减少内存,比如上面的(8)。接着先介绍下setImageDrawable和setImageResource的区别。
nImageView.setImageResource(R.drawable.highpicture)这个方法是在UI线程中对图片读取和解析的,所以有可能对一个Activity的启动造成延迟。所以如果顾虑到这个官方建议用setImageDrawable和setImageBitmap来代替。不管是setImgaeBitmap还是setImageResource最后都是调用了setImageDrawable,所以综合来看setImageDrawable是最省内存高效的,如果担心图片过大或者图片过多影响内存和加载效率,可以自己解析图片然后通过调用setImageDrawable或者setImageBitmap方法进行设置,一般用setImageBitmap比较多,因为Bitmap对图片的处理比较方便。
setImageDrawable参数是Drawable,也是可以接受不同来源的图片,方法中所做的事情就是更新ImageView的图片。上面两个方法实际上最后调用的都是setImageDrawable(setImageResource没有直接调用,不过更新的方法与setImageDrawable一样)。
接着来测试验证下当加载多个图片时会产生什么现象,显示10个左右的1.6M的图片时界面如下所示。
(1) 当没有加载图片时:堆大小为11.830MB。
(2) 使用BitmapFactory.decodeFile(nPath)设置图片显示时,直接OOM。
(3)通过代码设置图片显示: nInputStream = new FileInputStream(new File(nPath));
Bitmap nBitmap = BitmapFactory.decodeStream(nInputStream);
也是内存溢出OOM。
(4) BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 8;
nInputStream = new FileInputStream(new File(nPath));
Bitmap nBitmap = BitmapFactory.decodeStream(nInputStream,null,opts);
此时占用的堆内存大小为15.549MB,此时虽然没有发生OOM的现象,但是在进入图片列表界面时伴随着卡屏,说明在UI线程中加载图片还是比较耗时的。
(5) 使用优化后的显示方式的堆内存大小为12.792MB,此时界面也没有卡顿的情况发生,很流畅,此时内存优化的效果大大提高了,至于优化的方法在本章的后边会重点介绍,这里主要先把常用的设置图片方法进行比较,其内存情况如下所示:
界面加载只有三张1.6M左右图片的情况:
(1) 通过以下代码显示图片:nInputStream = new FileInputStream(new File(nPath));
BitmapnBitmap = BitmapFactory.decodeStream(nInputStream);
直接OOM内存溢出。
(2) 通过以下代码显示图片:Bitmap nBitmap = BitmapFactory.decodeFile(nPath),直接内存溢出。
(3) 通过opts.inSampleSize = 8设置图片options属性时:堆大小为14.590MB。
从上面的对比可以看出当加载几张超过1M的图片时,使用BitmapFactory.decodeFile和decodeStream等方法加载显示图片时,都还是会发生内存溢出。通过以上的测试可以看出,当图片越大或者图片越多时,发生OOM的概率越大,所以加载图片显示时,对图片进行处理和内存优化是必须的。接着为了避免OOM的情况发生,我们试着加载两张只有100k左右的图片时的情况。
(1) 当没有加载图片显示时:堆大小为12.919MB。
(2) 通过以下代码设置图片显示时:opts.inSampleSize = 8和BitmapFactory.decodeStream(nInputStream,null, opts),堆大小为13.722MB。
(3) 通过以下代码设置图片显示时:Bitmap nBitmap =BitmapFactory.decodeFile(nPath),堆大小为15.953MB。
(4) 通过以下代码设置图片显示时:Bitmap nBitmap =BitmapFactory.decodeStream(nInputStream),堆大小为15.946MB。
(5) 优化后的代码显示图片时:13.650MB。
从上面的对比可以看出inSampleSize设置图片的占用内存的比例后,图片占用的栈内存是大大减少啊,但是通过将要介绍的优化方法,可以把堆内存减少到13.65MB,优化效果更佳,下面将重点介绍。
二. 加载图片优化
优化的方法主要有四点:1.增加内存缓存和硬件缓存;2.使用软引用;3.线程加载图片;4调用recycle()回收垃圾对象;5.减少图片对象占用的内存。以下分别详细介绍。
(1)使用线程池加载图片:当界面上需要加载的图片比较多时,如果在UI线程加载的话,会影响到界面上的性能,甚至可能直接造成ANR(界面无响应),所以在加载图片时,最好用后台线程进行加载,如果显示的图片界面是ListView(列表),建议用线程池的方式是最好的。而本人在项目开发时经常用的是AsyncTask通过executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR, path)把异步线程放到线程池中进行并发处理。这样的好处就是除了对线程起到管理作用外,还可以在doInBackground加载和处理图片,而onPostExecute则在加载和处理完毕图片后,把图片显示在ImageView中,方便开发。
(2)使用软引用:定义一个显示图片的控件设置为一个软引用,比如WeakReference<ImageView> = new WeakReference<ImageView>(imageView);虽然这里在正常的流程中对于加载图片时的内存没什么作用,但是当内存要溢出时,既将要发生OOM时,垃圾回收线程会回收软引用,避免了内存溢出的错误,该优化看似作用不大,但是却能保证你的Apk在关键时刻,还能继续运行,这对于用户体验来说却是至关重要的。
(3) 使用内存缓存LruCache类:在测试验证过程中,当没有使用缓存机制时,每次加载图片的耗时是15-25ms之间,当使用了缓存机制后使用的时间只有1-5ms之间,这是一种牺牲内存换取效率的方法,如果内存是在有限的情况下则不使用该方法。申请的内存缓存的大小一般都是最大内存的1/8,该方法类的使用也比较简单,加载图片一开始就调用(Bitmap) mLruCache.get(path),如果Bitmap为空时则按正常流程获取图片并处理显示,最后调用mLruCache.put(path,nBitmap)把处理后的图片对象放到LruCache缓存中,LruCache其实就是封装了HasMap,同样的我们也可以对HasMap进行封装,自定义我们需要的缓存方法类。内存缓存对加载最近使用的图片时会很高效,但是我们不能确保需要的图片会一直在缓存中。一来是像GirdView和ListView这些大数据量的组件很容易填满内存缓存,等到新的图片要显示时则没有内存空间可以缓存了,二是当你的应用在后台运行时,有很大的可能会被“来电”或者其他高优先级的进程给打断,在后台时被杀掉,此时内存缓存就会失效了,一旦用户重新回到该应用中时,我们就需要重新处理每个图片。所以很多情况下也可以使用硬盘缓存,不管app在后台被杀掉还是内存不够等情况都可以一并解决,目前用得比较多的硬盘缓存技术是通过DiskLruCache进行保存,由于该类相对比较复杂,后续再详细介绍。
(4) 及时调用recycle()回收没用的图片对象,这里主要就是图片对象所占的内存太大,如果要等垃圾回收线程检测到后再回收,难免会有慢一拍的时候,那么此时就有可能由于图片所占的大内存出现OOM的情况,所以对于图片对象没被使用时要及时回收。比如在以下代码中,在按比例缩小图片并获取时,传进去的Bitmap没处理前可能占据了大量的内存,如果最后也不回收,那么有可能还长时间存在内存中,就很大可能发生OOM的情况。所以当函数运行到最后时,调用recycle()进行内存回收。
private Bitmap resizeBitmapByScale(
Bitmap bitmap,float scale, boolean recycle) {
int width =Math.round(bitmap.getWidth() * scale);
int height= Math.round(bitmap.getHeight() * scale);
if (width== bitmap.getWidth()
&& height == bitmap.getHeight()) return bitmap;
Bitmap target= Bitmap.createBitmap(width, height, getConfig(bitmap));
Canvas canvas= new Canvas(target);
canvas.scale(scale,scale);
Paint paint= new Paint(Paint.FILTER_BITMAP_FLAG | Paint.DITHER_FLAG);
canvas.drawBitmap(bitmap,0, 0, paint);
if (!bitmap.isRecycled())bitmap.recycle();
return target;
}
(5)减少图片对象所占的内存大小:先介绍下图片所占用的内存算法,公式为:
图片内存大小=width*height*Config,width为图片的宽,height为图片的高,Config为图片的像素,android有四种类型的像素:
ALPHA_8:每个像素占用1byte内存
ARGB_4444:每个像素占用2byte内存
ARGB_8888:每个像素占用4byte内存
RGB_565:每个像素占用2byte内存
也就是说如果Config设置为ARGB_8888,那么一张480*320的图片占用的内存就是480*320*4 byte(大概0.59M),如果想要减少图片所占的内存大小,就可以通过设置其他的像素值,从而减少占用的内存大小。公式中的Config参数对应于android中BitmapFactory.Options的inPreferredConfig参数。在开发过程中,可以通过如下代码可以设置图片的像素。
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inPreferredConfig = Bitmap.Config.RGB_565;Bitmap.Config.RGB_565;
另外缩小图片也可以减少所占用的内存大小,可通过BitmapFactory.Options中的inSampleSize参数可以达到减少所占内存大小的目的,通过以下代码可以进行缩小图片:opts.inSampleSize = 4(按1/4倍进行缩小)。
以上方法用得最多的应该是ListView等列表组件,因为加载的图片越多,占用内存越多,需要优化的概率就越高。看到这里或许你已经发觉了,其实本文的优化点很多跟开源代码库的Universal-Image-Loader思路是一致的。没错,很多加载图片的优化都是基于以上几个优化点的,只是Univesal-Image-Loader开源库适用的范围更广,考虑的情况比较多。总之只要在开发加载大量图片并显示时能够基于以上几个优化点进行优化,相信加载图片的代码将提高好几倍。