本文转自夏安明-<Android 那些你所不知道的Bitmap对象详解>,感谢这些无私的奉献!
我们知道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对象,代码如下

[java] view plain copy
  1. packagecom.example.bitmap;
  2. importandroid.content.res.Resources;
  3. importandroid.graphics.Bitmap;
  4. importandroid.graphics.drawable.BitmapDrawable;
  5. publicclassRecycleBitmapDrawableextendsBitmapDrawable{
  6. privateintdisplayResCount=0;
  7. privatebooleanmHasBeenDisplayed;
  8. publicRecycleBitmapDrawable(Resourcesres,Bitmapbitmap){
  9. super(res,bitmap);
  10. }
  11. /**
  12. *@paramisDisplay
  13. */
  14. publicvoidsetIsDisplayed(booleanisDisplay){
  15. synchronized(this){
  16. if(isDisplay){
  17. mHasBeenDisplayed=true;
  18. displayResCount++;
  19. }else{
  20. displayResCount--;
  21. }
  22. }
  23. checkState();
  24. }
  25. /**
  26. *检查图片的一些状态,判断是否需要调用recycle
  27. */
  28. privatesynchronizedvoidcheckState(){
  29. if(displayResCount<=0&&mHasBeenDisplayed
  30. &&hasValidBitmap()){
  31. getBitmap().recycle();
  32. }
  33. }
  34. /**
  35. *判断Bitmap是否为空且是否调用过recycle()
  36. *@return
  37. */
  38. privatesynchronizedbooleanhasValidBitmap(){
  39. Bitmapbitmap=getBitmap();
  40. returnbitmap!=null&&!bitmap.isRecycled();
  41. }
  42. }
除了上面这个RecycleBitmapDrawable之外呢,我们还需要一个自定义的ImageView来控制什么时候显示Bitmap以及什么时候隐藏Bitmap对象

[java] view plain copy
  1. packagecom.example.bitmap;
  2. importandroid.content.Context;
  3. importandroid.graphics.drawable.Drawable;
  4. importandroid.graphics.drawable.LayerDrawable;
  5. importandroid.util.AttributeSet;
  6. importandroid.widget.ImageView;
  7. publicclassRecycleImageViewextendsImageView{
  8. publicRecycleImageView(Contextcontext){
  9. super(context);
  10. }
  11. publicRecycleImageView(Contextcontext,AttributeSetattrs){
  12. super(context,attrs);
  13. }
  14. publicRecycleImageView(Contextcontext,AttributeSetattrs,intdefStyle){
  15. super(context,attrs,defStyle);
  16. }
  17. @Override
  18. publicvoidsetImageDrawable(Drawabledrawable){
  19. DrawablepreviousDrawable=getDrawable();
  20. super.setImageDrawable(drawable);
  21. //显示新的drawable
  22. notifyDrawable(drawable,true);
  23. //回收之前的图片
  24. notifyDrawable(previousDrawable,false);
  25. }
  26. @Override
  27. protectedvoidonDetachedFromWindow(){
  28. //当View从窗口脱离的时候,清除drawable
  29. setImageDrawable(null);
  30. super.onDetachedFromWindow();
  31. }
  32. /**
  33. *通知该drawable显示或者隐藏
  34. *
  35. *@paramdrawable
  36. *@paramisDisplayed
  37. */
  38. publicstaticvoidnotifyDrawable(Drawabledrawable,booleanisDisplayed){
  39. if(drawableinstanceofRecycleBitmapDrawable){
  40. ((RecycleBitmapDrawable)drawable).setIsDisplayed(isDisplayed);
  41. }elseif(drawableinstanceofLayerDrawable){
  42. LayerDrawablelayerDrawable=(LayerDrawable)drawable;
  43. for(inti=0,z=layerDrawable.getNumberOfLayers();i<z;i++){
  44. notifyDrawable(layerDrawable.getDrawable(i),isDisplayed);
  45. }
  46. }
  47. }
  48. }
这个自定类也比较简单,重写了setImageDrawable()方法,在这个方法中我们先获取ImageView上面的图片,然后通知之前显示在ImageView的Drawable不在显示了,Drawable会判断是否需要调用recycle(),当View从Window脱离的时候会回调onDetachedFromWindow(),我们在这个方法中回收显示在ImageView的图片,具体的使用方法

[java] view plain copy
  1. ImageViewimageView=newImageView(context);
  2. imageView.setImageDrawable(newRecycleBitmapDrawable(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只是起到缓存数据作用,并没有回收内存。一般我们的代码会这么写

[java] view plain copy
  1. packagecom.example.bitmap;
  2. importjava.lang.ref.SoftReference;
  3. importjava.util.Collections;
  4. importjava.util.HashSet;
  5. importjava.util.Iterator;
  6. importjava.util.Set;
  7. importandroid.annotation.TargetApi;
  8. importandroid.graphics.Bitmap;
  9. importandroid.graphics.Bitmap.Config;
  10. importandroid.graphics.BitmapFactory;
  11. importandroid.graphics.drawable.BitmapDrawable;
  12. importandroid.os.Build;
  13. importandroid.os.Build.VERSION_CODES;
  14. importandroid.support.v4.util.LruCache;
  15. publicclassImageCache{
  16. privatefinalstaticintMAX_MEMORY=4*102*1024;
  17. privateLruCache<String,BitmapDrawable>mMemoryCache;
  18. privateSet<SoftReference<Bitmap>>mReusableBitmaps;
  19. privatevoidinit(){
  20. if(hasHoneycomb()){
  21. mReusableBitmaps=Collections
  22. .synchronizedSet(newHashSet<SoftReference<Bitmap>>());
  23. }
  24. mMemoryCache=newLruCache<String,BitmapDrawable>(MAX_MEMORY){
  25. /**
  26. *当保存的BitmapDrawable对象从LruCache中移除出来的时候回调的方法
  27. */
  28. @Override
  29. protectedvoidentryRemoved(booleanevicted,Stringkey,
  30. BitmapDrawableoldValue,BitmapDrawablenewValue){
  31. if(hasHoneycomb()){
  32. mReusableBitmaps.add(newSoftReference<Bitmap>(oldValue
  33. .getBitmap()));
  34. }
  35. }
  36. };
  37. }
  38. /**
  39. *从mReusableBitmaps中获取满足能设置到BitmapFactory.Options.inBitmap上面的Bitmap对象
  40. *@paramoptions
  41. *@return
  42. */
  43. protectedBitmapgetBitmapFromReusableSet(BitmapFactory.Optionsoptions){
  44. Bitmapbitmap=null;
  45. if(mReusableBitmaps!=null&&!mReusableBitmaps.isEmpty()){
  46. synchronized(mReusableBitmaps){
  47. finalIterator<SoftReference<Bitmap>>iterator=mReusableBitmaps
  48. .iterator();
  49. Bitmapitem;
  50. while(iterator.hasNext()){
  51. item=iterator.next().get();
  52. if(null!=item&&item.isMutable()){
  53. if(canUseForInBitmap(item,options)){
  54. bitmap=item;
  55. iterator.remove();
  56. break;
  57. }
  58. }else{
  59. iterator.remove();
  60. }
  61. }
  62. }
  63. }
  64. returnbitmap;
  65. }
  66. /**
  67. *判断该Bitmap是否可以设置到BitmapFactory.Options.inBitmap上
  68. *
  69. *@paramcandidate
  70. *@paramtargetOptions
  71. *@return
  72. */
  73. @TargetApi(VERSION_CODES.KITKAT)
  74. publicstaticbooleancanUseForInBitmap(Bitmapcandidate,
  75. BitmapFactory.OptionstargetOptions){
  76. //在Anroid4.4以后,如果要使用inBitmap的话,只需要解码的Bitmap比inBitmap设置的小就行了,对inSampleSize
  77. //没有限制
  78. if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.KITKAT){
  79. intwidth=targetOptions.outWidth/targetOptions.inSampleSize;
  80. intheight=targetOptions.outHeight/targetOptions.inSampleSize;
  81. intbyteCount=width*height
  82. *getBytesPerPixel(candidate.getConfig());
  83. returnbyteCount<=candidate.getAllocationByteCount();
  84. }
  85. //在Android
  86. //4.4之前,如果想使用inBitmap的话,解码的Bitmap必须和inBitmap设置的宽高相等,且inSampleSize为1
  87. returncandidate.getWidth()==targetOptions.outWidth
  88. &&candidate.getHeight()==targetOptions.outHeight
  89. &&targetOptions.inSampleSize==1;
  90. }
  91. /**
  92. *获取每个像素所占用的Byte数
  93. *
  94. *@paramconfig
  95. *@return
  96. */
  97. publicstaticintgetBytesPerPixel(Configconfig){
  98. if(config==Config.ARGB_8888){
  99. return4;
  100. }elseif(config==Config.RGB_565){
  101. return2;
  102. }elseif(config==Config.ARGB_4444){
  103. return2;
  104. }elseif(config==Config.ALPHA_8){
  105. return1;
  106. }
  107. return1;
  108. }
  109. @TargetApi(VERSION_CODES.HONEYCOMB)
  110. publicstaticbooleanhasHoneycomb(){
  111. returnBuild.VERSION.SDK_INT>=Build.VERSION_CODES.HONEYCOMB;
  112. }
  113. }
上面只是一些事例性的代码,将从LruCache中移除的BitmapDrawable对象的弱引用保存在一个set中,然后从set中获取满足BitmapFactory.Options.inBitmap条件的Bitmap对象用来提高解码Bitmap性能,使用如下

[java] view plain copy
  1. publicstaticBitmapdecodeSampledBitmapFromFile(Stringfilename,
  2. intreqWidth,intreqHeight){
  3. finalBitmapFactory.Optionsoptions=newBitmapFactory.Options();
  4. ...
  5. BitmapFactory.decodeFile(filename,options);
  6. ...
  7. //Ifwe'rerunningonHoneycombornewer,trytouseinBitmap.
  8. if(ImageCache.hasHoneycomb()){
  9. options.inMutable=true;
  10. if(cache!=null){
  11. BitmapinBitmap=cache.getBitmapFromReusableSet(options);
  12. if(inBitmap!=null){
  13. options.inBitmap=inBitmap;
  14. }
  15. }
  16. }
  17. ...
  18. returnBitmapFactory.decodeFile(filename,options);
  19. }

通过这篇文章你是不是对Bitmap对象有了更进一步的了解,在应用加载大量的Bitmap对象的时候,如果你做到上面几点,我相信应用发生OutOfMemoryError的概率会很小,并且性能会得到一定的提升,我经常会看到一些同学在评价一个图片加载框架好不好的时候,比较片面的以自己使用过程中是否发生OutOfMemoryError来定论,当然经常性的发生OutOfMemoryError你应该先检查你的代码是否存在问题,一般一些比较成熟的框架是不存在很严重的问题,毕竟它也经过很多的考验才被人熟知的,今天的讲解就到这里了,有疑问的同学可以在下面留言!


更多相关文章

  1. 如何避免Android内存泄漏 .
  2. Android(安卓)浅析 ContentProvider (三) 获取原理
  3. Android中实现Launcher功能之二 ----- 添加窗口小部件以及AppWid
  4. fackbook的Fresco (FaceBook推出的Android图片加载库-Fresco)
  5. 关于Android(安卓)Activity之间传递数据的6种方式
  6. ArcGIS for Android示例解析之FeatureLayer服务-----SelectFeatu
  7. Android(安卓)中文API
  8. Android(安卓)WakeLock 使用总结
  9. Android中文API(114)——TabWidget

随机推荐

  1. Android(安卓)GPS学习笔记—HAL实现
  2. Android(安卓)Power Management
  3. Android绘图之绘制太极图
  4. Android(安卓)AsyncTask实现异步任务的执
  5. Android(安卓)Studio 常见 Build 问题 Er
  6. Android(安卓)HAL 开发 (1)
  7. Android(安卓)IOS WebRTC 音视频开发总结
  8. Androidstudio开发button按钮的操作以及
  9. Android(安卓)游戏开发基础
  10. Android(安卓)APIDemos 研读之一:android.