我们知道Android系统分配给每一个应用程序的内存是有限的,Bitmap作为消耗内存大户。我们对Bitmap的管理稍有不当就可能引发OutOfMemoryError,而Bitmap对象在不同的Android版本号中存在一些差异,今天就给大家介绍下这些差异。并提供一些在使用Bitmap的须要注意的地方。
在Android2.3.3(API 10)及之前的版本号中,Bitmap对象与其像素数据是分开存储的,Bitmap对象存储在Dalvik heap中,而Bitmap对象的像素数据则存储在Native Memory(本地内存)中或者说Derict Memory(直接内存)中,这使得存储在Native Memory中的像素数据的释放是不可预知的。我们能够调用recycle()方法来对Native Memory中的像素数据进行释放,前提是你能够清楚的确定Bitmap已不再使用了,假设你调用了Bitmap对象recycle()之后再将Bitmap绘制出来,就会出现"Canvas: trying to use a recycled bitmap"错误。而在Android3.0(API 11)之后,Bitmap的像素数据和Bitmap对象一起存储在Dalvik heap中,所以我们不用手动调用recycle()来释放Bitmap对象,内存的释放都交给垃圾回收器来做,或许你会问,为什么我在显示Bitmap对象的时候还是会出现OutOfMemoryError呢?
在说这个问题之前我顺便提一下。在Android2.2(API 8)之前,使用的是Serial垃圾收集器,从名字能够看出这是一个单线程的收集器,这里的”单线程"的意思并不仅仅是使用一个CPU或者一条收集线程去收集垃圾,更重要的是在它进行垃圾收集时,必须暂停其它全部的工作线程,Android2.3之后,这样的收集器就被取代了,使用的是并发的垃圾收集器。这意味着我们的垃圾收集线程和我们的工作线程互不影响。
简单的了解垃圾收集器之后,我们对上面的问题举一个简单的样例。假如系统启动了垃圾回收线程去收集垃圾。而此时我们一下子产生大量的Bitmap对象,此时是有可能会产生OutOfMemoryError。由于垃圾回收器首先要推断某个对象是否还存活(JAVA语言推断对象是否存活使用的是根搜索算法 GC Root Tracing)。然后利用垃圾回收算法来对垃圾进行回收,不同的垃圾回收器具有不同的回收算法。这些都是须要时间的, 发生OutOfMemoryError的时候。我们要明白究竟是由于内存泄露(Memory Leak)引发的还是内存溢出(Memory overflow)引发的。假设是内存泄露我们须要利用工具(比方MAT)查明内存泄露的代码并进行改正,假设不存在泄露,换句话来说就是内存中的对象确实还必须活着,那我们能够看看能否够通过某种途径,降低对象对内存的消耗。比方我们在使用Bitmap的时候,应该依据View的大小利用BitmapFactory.Options计算合适的inSimpleSize来对Bitmap进行相相应的裁剪,以降低Bitmap对内存的使用,假设上面都做好了还是存在OutOfMemoryError(一般这样的情况非常少发生)的话。那我们仅仅能调大Dalvik heap的大小了。在Android 3.1以及更高的版本号中,我们能够在AndroidManifest.xml的application标签中添加一个值等于“true”的android:largeHeap属性来通知Dalvik虚拟机应用程序须要使用较大的Java Heap。可是我们也不鼓舞这么做。
在Android 2.3及下面管理Bitmap
从上面我们知道,在Android2.3及下面我们推荐使用recycle()方法来释放内存。我们在使用ListView或者GridView的时候,该在什么时候去调用recycle()呢?这里我们用到引用计数,使用一个变量(dispalyRefCount)来记录Bitmap显示情况,假设Bitmap绘制在View上面displayRefCount加一, 否则就减一, 仅仅有在displayResCount为0且Bitmap不为空且Bitmap没有调用过recycle()的时候,我们才需求对该Bitmap对象进行recycle(),所以我们须要用一个类来包装下Bitmap对象,代码例如以下
package com.example.bitmap; import android.content.res.Resources; import android.graphics.Bitmap; import android.graphics.drawable.BitmapDrawable; public class RecycleBitmapDrawable extends BitmapDrawable { private int displayResCount = 0; private boolean mHasBeenDisplayed; public RecycleBitmapDrawable(Resources res, Bitmap bitmap) { super(res, bitmap); } /** * @param isDisplay */ public void setIsDisplayed(boolean isDisplay){ synchronized (this) { if(isDisplay){ mHasBeenDisplayed = true; displayResCount ++; }else{ displayResCount --; } } checkState(); } /** * 检查图片的一些状态,推断是否须要调用recycle */ private synchronized void checkState() { if (displayResCount <= 0 && mHasBeenDisplayed && hasValidBitmap()) { getBitmap().recycle(); } } /** * 推断Bitmap是否为空且是否调用过recycle() * @return */ private synchronized boolean hasValidBitmap() { Bitmap bitmap = getBitmap(); return bitmap != null && !bitmap.isRecycled(); } }
除了上面这个RecycleBitmapDrawable之外呢,我们还须要一个自己定义的ImageView来控制什么时候显示Bitmap以及什么时候隐藏Bitmap对象
package com.example.bitmap; import android.content.Context; import android.graphics.drawable.Drawable; import android.graphics.drawable.LayerDrawable; import android.util.AttributeSet; import android.widget.ImageView; public class RecycleImageView extends ImageView { public RecycleImageView(Context context) { super(context); } public RecycleImageView(Context context, AttributeSet attrs) { super(context, attrs); } public RecycleImageView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); } @Override public void setImageDrawable(Drawable drawable) { Drawable previousDrawable = getDrawable(); super.setImageDrawable(drawable); //显示新的drawable notifyDrawable(drawable, true); //回收之前的图片 notifyDrawable(previousDrawable, false); } @Override protected void onDetachedFromWindow() { //当View从窗体脱离的时候,清除drawable setImageDrawable(null); super.onDetachedFromWindow(); } /** * 通知该drawable显示或者隐藏 * * @param drawable * @param isDisplayed */ public static void notifyDrawable(Drawable drawable, boolean isDisplayed) { if (drawable instanceof RecycleBitmapDrawable) { ((RecycleBitmapDrawable) drawable).setIsDisplayed(isDisplayed); } else if (drawable instanceof LayerDrawable) { LayerDrawable layerDrawable = (LayerDrawable) drawable; for (int i = 0, z = layerDrawable.getNumberOfLayers(); i < z; i++) { notifyDrawable(layerDrawable.getDrawable(i), isDisplayed); } } } }
这个自定类也比較简单,重写了setImageDrawable()方法,在这种方法中我们先获取ImageView上面的图片,然后通知之前显示在ImageView的Drawable不在显示了,Drawable会推断是否须要调用recycle(),当View从Window脱离的时候会回调onDetachedFromWindow(),我们在这种方法中回收显示在ImageView的图片,详细的用法
ImageView imageView = new ImageView(context); imageView.setImageDrawable(new RecycleBitmapDrawable(context.getResource(), bitmap));
仅仅须要用RecycleBitmapDrawable包装Bitmap对象。然后设置到ImageView上面就能够啦,详细的内存释放我们不须要管,是不是非常方便呢?这是在Android2.3以及下面的版本号管理Bitmap的内存。
在Android 3.0及以上管理Bitmap
由于在Android3.0及以上的版本号中,Bitmap的像素数据也存储在Dalvik heap中,所以内存的管理就直接交给垃圾回收器了,我们并不须要手动的去释放内存,而今天讲的主要是BitmapFactory.Options.inBitmap的这个字段,假如这个字段被设置了,我们在解码Bitmap的时候。他会去重用inBitmap设置的Bitmap,降低内存的分配和释放,提高了应用的性能。然而在Android 4.4之前,BitmapFactory.Options.inBitmap设置的Bitmap必须和我们须要解码的Bitmap的大小一致才行。在Android4.4以后,BitmapFactory.Options.inBitmap设置的Bitmap大于或者等于我们须要解码的Bitmap的大小就OK了,我们先假设一个场景,还是在使用ListView。GridView去载入大量的图片。为了提高应用的效率,我们一般会做相相应的内存缓存和硬盘缓存,这里我们仅仅说内存缓存,而内存缓存官方推荐使用LruCache, 注意LruCache仅仅是起到缓存数据作用,并没有回收内存。一般我们的代码会这么写
package com.example.bitmap; import java.lang.ref.SoftReference; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.Set; import android.annotation.TargetApi; import android.graphics.Bitmap; import android.graphics.Bitmap.Config; import android.graphics.BitmapFactory; import android.graphics.drawable.BitmapDrawable; import android.os.Build; import android.os.Build.VERSION_CODES; import android.support.v4.util.LruCache; public class ImageCache { private final static int MAX_MEMORY = 4 * 102 * 1024; private LruCache<String, BitmapDrawable> mMemoryCache; private Set<SoftReference<Bitmap>> mReusableBitmaps; private void init() { if (hasHoneycomb()) { mReusableBitmaps = Collections .synchronizedSet(new HashSet<SoftReference<Bitmap>>()); } mMemoryCache = new LruCache<String, BitmapDrawable>(MAX_MEMORY) { /** * 当保存的BitmapDrawable对象从LruCache中移除出来的时候回调的方法 */ @Override protected void entryRemoved(boolean evicted, String key, BitmapDrawable oldValue, BitmapDrawable newValue) { if (hasHoneycomb()) { mReusableBitmaps.add(new SoftReference<Bitmap>(oldValue .getBitmap())); } } }; } /** * 从mReusableBitmaps中获取满足 能设置到BitmapFactory.Options.inBitmap上面的Bitmap对象 * @param options * @return */ protected Bitmap getBitmapFromReusableSet(BitmapFactory.Options options) { Bitmap bitmap = null; if (mReusableBitmaps != null && !mReusableBitmaps.isEmpty()) { synchronized (mReusableBitmaps) { final Iterator<SoftReference<Bitmap>> iterator = mReusableBitmaps .iterator(); Bitmap item; while (iterator.hasNext()) { item = iterator.next().get(); if (null != item && item.isMutable()) { if (canUseForInBitmap(item, options)) { bitmap = item; iterator.remove(); break; } } else { iterator.remove(); } } } } return bitmap; } /** * 推断该Bitmap能否够设置到BitmapFactory.Options.inBitmap上 * * @param candidate * @param targetOptions * @return */ @TargetApi(VERSION_CODES.KITKAT) public static boolean canUseForInBitmap(Bitmap candidate, BitmapFactory.Options targetOptions) { // 在Anroid4.4以后,假设要使用inBitmap的话,仅仅须要解码的Bitmap比inBitmap设置的小即可了。对inSampleSize // 没有限制 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { int width = targetOptions.outWidth / targetOptions.inSampleSize; int height = targetOptions.outHeight / targetOptions.inSampleSize; int byteCount = width * height * getBytesPerPixel(candidate.getConfig()); return byteCount <= candidate.getAllocationByteCount(); } // 在Android // 4.4之前,假设想使用inBitmap的话,解码的Bitmap必须和inBitmap设置的宽高相等。且inSampleSize为1 return candidate.getWidth() == targetOptions.outWidth && candidate.getHeight() == targetOptions.outHeight && targetOptions.inSampleSize == 1; } /** * 获取每一个像素所占用的Byte数 * * @param config * @return */ public static int getBytesPerPixel(Config config) { if (config == Config.ARGB_8888) { return 4; } else if (config == Config.RGB_565) { return 2; } else if (config == Config.ARGB_4444) { return 2; } else if (config == Config.ALPHA_8) { return 1; } return 1; } @TargetApi(VERSION_CODES.HONEYCOMB) public static boolean hasHoneycomb() { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB; } }
上面仅仅是一些事例性的代码,将从LruCache中移除的BitmapDrawable对象的弱引用保存在一个set中,然后从set中获取满足BitmapFactory.Options.inBitmap条件的Bitmap对象用来提高解码Bitmap性能。使用例如以下
public static Bitmap decodeSampledBitmapFromFile(String filename, int reqWidth, int reqHeight) { final BitmapFactory.Options options = new BitmapFactory.Options(); ... BitmapFactory.decodeFile(filename, options); ... // If we‘re running on Honeycomb or newer, try to use inBitmap. if (ImageCache.hasHoneycomb()) { options.inMutable = true; if (cache != null) { Bitmap inBitmap = cache.getBitmapFromReusableSet(options); if (inBitmap != null) { options.inBitmap = inBitmap; } } } ... return BitmapFactory.decodeFile(filename, options); }
通过这篇文章你是不是对Bitmap对象有了更进一步的了解,在应用载入大量的Bitmap对象的时候,假设你做到上面几点。我相信应用发生OutOfMemoryError的概率会非常小。而且性能会得到一定的提升。我常常会看到一些同学在评价一个图片载入框架好不好的时候。比較片面的以自己使用过程中是否发生OutOfMemoryError来定论。当然常常性的发生OutOfMemoryError你应该先检查你的代码是否存在问题。一般一些比較成熟的框架是不存在非常严重的问题,毕竟它也经过非常多的考验才被人熟知的,今天的解说就到这里了,有疑问的同学能够在下面留言。