关于Android加载图片时的OOM的一些解决方法和优化

来源:互联网 发布:淘宝 暴走大事件 编辑:程序博客网 时间:2024/06/09 17:25

1、通过强引用和弱引用以及LRU算法。

private static final int HARD_CACHE_CAPACITY = 20;//强引用的bitmap的数量

//为了提高图片的利用率,通过单链表实现先进先出,将老的图片移到软引用里面保存

private static LinkedHashMap<String, Drawable> sHardBitmapCache = new LinkedHashMap<String, Drawable>(
   HARD_CACHE_CAPACITY / 2, 0.75f, true) {
  protected boolean removeEldestEntry(Map.Entry<String, Drawable> eldest) {
   if (size() > HARD_CACHE_CAPACITY) {
    // Entries push-out of hard reference cache are transferred to
    // soft reference cache
    sSoftBitmapCache.put(eldest.getKey(),
      new WeakReference<Drawable>(eldest.getValue()));
    return true;
   } else
    return false;

  };
 };

//通过软引用保存部分图片

 private static LRULinkedHashMap<String, WeakReference<Drawable>> sSoftBitmapCache = new LRULinkedHashMap<String, WeakReference<Drawable>>(
   20);

 

// 实现LRU算法,将bitmap从软引用中移出
 public static class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {
  private final int maxCapacity;

  private static final float DEFAULT_LOAD_FACTOR = 0.75f;

  private final Lock lock = new ReentrantLock();

  public LRULinkedHashMap(int maxCapacity) {
   super(maxCapacity, DEFAULT_LOAD_FACTOR, true);
   this.maxCapacity = maxCapacity;
  }

  @Override
  protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {
   return size() > maxCapacity;
  }

  @Override
  public boolean containsKey(Object key) {
   try {
    lock.lock();
    return super.containsKey(key);
   } finally {
    lock.unlock();
   }
  }

  @Override
  public V get(Object key) {
   try {
    lock.lock();
    return super.get(key);
   } finally {
    lock.unlock();
   }
  }

  @Override
  public V put(K key, V value) {
   try {
    lock.lock();
    return super.put(key, value);
   } finally {
    lock.unlock();
   }
  }

  public int size() {
   try {
    lock.lock();
    return super.size();
   } finally {
    lock.unlock();
   }
  }

  public void clear() {
   try {
    lock.lock();
    super.clear();
   } finally {
    lock.unlock();
   }
  }

  public Collection<Map.Entry<K, V>> getAll() {
   try {
    lock.lock();
    return new ArrayList<Map.Entry<K, V>>(super.entrySet());
   } finally {
    lock.unlock();
   }
  }
 }

2、通过压缩图片,可以有效的減少bitmap的大小,但是图片清晰度要求高的项目中此方法不可取,另外在项目中,如果是从网络上取图片,不建议这么做,浪费了很大的流量,而且还导致图片不清晰,应该将图片的参数传给服务器端,服务器端根据参数来压缩图片,这样可以节约流量。如果是拍照,建议调用下面的方法,因为之前在项目中遇到拍照,由于有些手机内存小,而且图片比较高清,decode的时候,图片过大导致内存溢出。

 InputStream is = this.getResources().openRawResource(R.drawable.pic);
     BitmapFactory.Options options=new BitmapFactory.Options();
     options.inJustDecodeBounds = false;
     options.inSampleSize = 2;   //width,hight设为原来的四分之一
     Bitmap btp =BitmapFactory.decodeStream(is,null,options);

3、通过调用Bitmap的recycle 方法

   这个方法只是做一个标记,告诉Java虚拟机,这个图片可以回收了,bitmap图片的回收还是要根据GC的时机

   这个方法有一个弊端就是容易造成trying to use a  recycled bitmap.的异常

   ImageView imageView=(ImageView) findViewById(R.id.image);

   Bitmap bitmap=BitmapFactory.decodeStream....

    imageView.setImageBitmap(bitmap);

    if(bitmap!=null)

     bitmap.recycle();

造成异常的原因是系统在调用imageView.setImageBitmap(bitmap)的时候,回根据bitmap这个引用去找在内存中的图片,结果发现图片已经回收了,就回报这个异常。

所以, 怎样才可以保证不会早了呢?

关于图片显示,重要的时间点:

设置进去的时间点;

 画面画出来的时间点;

最保险最笨的做法,在新的图片设置进去以后再recycle掉老的图片,这样做的坏处在于,在某个时间段,你需要的空间是double的【新旧两套都在】;

如果你不偏向于那么做,又有时间,可以考虑后面一个时间点,除了setImage以及其它代码中显示调用那个bitmap的时候我们会检查bitmap,在acticvity变为visible的时候系统还是会去找之前设置进去的bitmap【即使你的onResume方法里面并没有提到去refresh UI,这件事情它也是会去做的,大概不然它就不知道这次该显示些什么了】。所以,在UI线程里面,在一个不可能被打断的方法里面,是先设置新的bitmap还是先recycle旧的图片是没有影响的。

譬如说 mBitmap.recycle();

mBitmap = ….. //设置

mImageView.setImage(mBitmap);

这样的代码是完全可以的。

 你先调用ImageView.setImageBitmap(null)

然后再调用if(bitmap!=null)

     bitmap.recycle();

说白了要先找到源,处理了,然后再去回收。

  如果有一张图片被多个Activity以用,而且是通过HashMap引用的,如果你要执行recycle

  这个时候为了保险起见

       Bitmap bitmap=map.remove(url)

       map.put(url,null);

      if(bitmap!=null){

       bitmap.recycle();

}

map.remove(url);

最重要的就是确保:在UI线程【因为设置UI显示只能在UI主线程里】里面一个不可能被打断的方法里面。这个是为了确保在两者之间UI主线程不可能被打断,不可能刚好从invisible变成visible。

所以,特别小心两种东西:

1. 多线程【个人觉得最好不要在其他线程里面调用UI用过的bitmap的recycle方法,多线程之间是很难保证时间顺序的,暂时没有想出一种在background thread里面recycle的合理的方式】;

2. 非及时发生的方法:譬如,发intent啊,发notify啊去通知UI主线程去做UI重新刷新并不能替代mImageView.setImage(mBitmap);这样的句子。完全有可能,你确实发了intent出去了,但是目标activity之一还没有做UI重新设置【Q: maybe没收到 or 收到但还是等待处理,不确定这两种可能是不是都有可能】,这个时候这个acitivity变成visible了,系统仍然试图找旧的图片,找不到了就会报exception了。


4对于listView这样的控件应该加上缓存机制

例如

final Viewhodler viewhodler;

   if (convertView == null) {
    viewhodler = new Viewhodler();
    LayoutInflater inflater = ((Activity) context)
      .getLayoutInflater();
    convertView = inflater.inflate(R.layout.film_item, null);

    viewhodler.icon = (ImageView) convertView
      .findViewById(R.id.icon);
    viewhodler.value = (TextView) convertView
      .findViewById(R.id.price);
      convertView.setTag(viewhodler);

   } else {
    viewhodler = (Viewhodler) convertView.getTag();
   }

 

 

5在一个Activity中如果有一个listView或者gallery加载图片,退出Activity,这些线程仍旧在执行,而且大量内存被占用,而且更加容易造成oom,这个我在之前的项目中经常遇到这样的问题,后来我通过线程池+FutureTask解决了。在退出Activity的时候关闭在这个Activity里面开的线程,可以让Activity尽快结束,以便Java垃圾回收机制能够回收。

 如果通过线程池+Runnable实现的图片加载,是没法停止单个的线程的。但是可以通过线程池+FutureTask来实现。

下面先介绍一下FuturTask

 

FutureTask是一种可以取消的异步的计算任务。它的计算是通过Callable实现的,它等价于可以携带结果的Runnable,并且有三个状态:等待、运行和完成。完成包括所有计算以任意的方式结束,包括正常结束、取消和异常。

Future有个get方法而获取结果只有在计算完成时获取,否则会一直阻塞直到任务转入完成状态,然后会返回结果或者抛出异常。

FutureTask有下面几个重要的方法:

1.get()

阻塞一直等待执行完成拿到结果

 

2.get(int timeout, TimeUnit timeUnit)

阻塞一直等待执行完成拿到结果,如果在超时时间内,没有拿到抛出异常

 

3.isCancelled()

是否被取消

 

4.isDone()

是否已经完成

 

5.cancel(boolean mayInterruptIfRunning)

试图取消正在执行的任务

我们正是要通过cancel方法来取消一个线程,在退出Activity的时候调用FutureTask的cancel(true)方法

 

6.通过以上的方法,就不会有内存溢出了么,答案是否定的,因为由于Java垃圾回收机制的不确定性,可能在某一个时刻,内存占用较大,而且这个时候还在不断的加载图片,这个时候就有可能发生内存溢出的情况。所以在代码中要对发生内存溢出进行处理。

而且这个异常也要捕获。以下是截取的部分代码片段

try {
    URL url = new URL(imageUrl);
    conn = (HttpURLConnection) url.openConnection();
    conn.setConnectTimeout(10 * 1000);
    conn.connect();
    inputStream = conn.getInputStream();
    final BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    // options.inSampleSize = 2;
    bitmap = BitmapFactory.decodeStream(inputStream, null, options);
    if (bitmap != null) {
     drawable = new BitmapDrawable(context.getResources(),
       bitmap);
     addBitmapToCache(imageUrl, drawable);
     savePic(bitmap, imageUrl);// 保存图片
    }
   } catch (Exception e) {
    return null;
   } catch (OutOfMemoryError oom) {
    clearCache();
    System.gc();
     } finally {
    if (inputStream != null) {
     try {
      inputStream.close();
     } catch (Exception e) {

     }
    }
    if (conn != null) {
     try {
      conn.disconnect();
     } catch (Exception e) {

     }
    }

   }

7.程序没有发生OOM异常意味着程序没有问题了么,其实不是的,如果一个应用程序占用了大量的内存,会导致这个应用程序运行缓慢,而且很卡,所以解决内存问题对于提高程序的性能尤为重要,所以在项目中,大家要做好内存的优化,开发高性能的应用程序。

 

 

方法之一:

BitmapFactory.Options options = new BitmapFactory.Options();

options.inPreferredConfig = Config.ARGB_8888;

options.inPurgeable = true;// 允许可清除

options.inInputShareable = true;// 以上options的两个属性必须联合使用才会有效果

String sname = String.format( “xxx.png”, sTowerStyle, j, sDirction, i);

InputStream is = am.open(sname);

arrBmp[ iBmpIndex] = BitmapFactory .decodeStream(is, null, options);

 

方法之二:

将图片转化为缩略图再加载: 
BitmapFactory.Options options = new BitmapFactory.Options();  

options.inSampleSize = 2;  

Bitmap img = BitmapFactory.decodeFile("/sdcard/1.png", options);  

该段代码便是读取1.png的缩略图,长度、宽度都只有原图片的1/2。图片大小削减,占用的内存天然也变小了。这么做的弊病是图片质量变差,inSampleSize的值越大,图片的质量就越差。因为各手机厂商缩放图片的算法不合,在不合手机上的缩放图片质量可能会不合。笔者就遭受过moto手机上图片缩放后质量可以接管,三星手机上同样的缩放比例,质量却差很多的景象。

 

方法之三:

用ARBG_4444色彩模式加载图片: 

Android中有四种,分别是:

ALPHA_8:每个像素占用1byte内存

ARGB_4444:每个像素占用2byte内存

ARGB_8888:每个像素占用4byte内存

RGB_565:每个像素占用2byte内存

Android默认的色彩模式为ARGB_8888,这个色彩模式色彩最细腻,显示质量最高。但同样的,占用的内存也最大。

BitmapFactory.Options options = new BitmapFactory.Options();  

options.inPreferredConfig = Bitmap.Config.ARGB_4444;      

Bitmap img = BitmapFactory.decodeFile("/sdcard/1.png", options);  

以上代码便是将1.png以ARGB_4444模式读出。内存削减固然不如第一种办法明显,然则对于大多半图片,看不出与ARGB_8888模式有什么差别。不过在读取有渐变结果的图片时,可能有色彩条呈现。别的,会影响图片的殊效处理惩罚。

 

 

方法之四:

调用图片的recycle()办法: 

这个其实不是真正降落图片内存的办法。首要目标是标识表记标帜图片对象,便利收受接管图片对象的本地数据。图片对象的本地数据占用的内存最大,并且与法度Java项目组的内存是分隔策画的。所以经常呈现Java heap足够应用,而图片产生OutOfMemoryError的景象。在图片不应用时调用该办法,可以有效降落图片本地数据的峰值,从而削减OutOfMemoryError的概率。不过调用了recycle()的图片对象处于“放弃”状况,调用时会造成法度错误。所以在无法包管该图片对象绝对不会被再次调用的景象下,不建议应用该办法。希罕要重视已经用setImageBitmap(Bitmap img)办法分派给控件的图片对象,可能会被体系类库调用,造成法度错误。

 

 

方法之五:

应用Matrix对象放大的图片如何更改色彩模式:

固然应用Matrix对象放大图片,必然会花费更多的内存,但有时辰也不得不如许做。放大后的图片应用的ARGB_8888色彩模式,就算原图片是ARGB_4444色彩模式也一样,并且没有办法在放大时直接指定色彩模式。可以采取以下办法更改图片色彩模式。

Matrix matrix = new Matrix();  

float newWidth = 200;//图片放大后的宽度  

float newHeight = 300;//图片放大后的长度  

matrix.postScale(newWidth / img.getWidth(), newHeight/ img.getHeight());  

Bitmap img1 = Bitmap.createBitmap(img, 0, 0, img.getWidth(), img.getHeight(), matrix, true);//获得放大的图片  

img2 = img1.copy(Bitmap.Config.ARGB_4444, false);//获得ARGB_4444色彩模式的图片  

img = null;  

img1 = null;  

这里比起本来的图片额外生成了一个图片对象img1。然则体系会主动收受接管img1,所以实际内存还是削减了。

 

 

注:

尽量不要使用setImageBitmap或setImageResource或BitmapFactory.decodeResource来设置一张大图,因为这些函数在完成decode后,最终都是通过java层的createBitmap来完成的,需要消耗更多内存.


因此,改用先通过BitmapFactory.decodeStream方法,创建出一个bitmap,再将其设为ImageView的 source,decodeStream最大的秘密在于其直接调用 JNI >> nativeDecodeAsset() 来完成decode,无需再使用java层的createBitmap,从而节省了java层的空间.


如果在读取时加上图片的Config参数,可以更有效减少加载的内存,从而有效阻止抛出out of Memory异常另外,decodeStream直接拿的图片来读取字节码了, 不会根据机器的各种分辨率来自动适应,使用了decodeStream之后,需要在hdpi和mdpi,ldpi中配置相应的图片资源,否则在不同分辨率机器上都是同样大小(像素点数量),显示出来的大小就不对了.

 

BitmapFactory.Options.inPurgeable;
如果 inPurgeable 设为True的话表示使用BitmapFactory创建的Bitmap用于存储Pixel的内存空间在系统内存不足时可以被回收,在应用需要再次访问Bitmap的Pixel时(如绘制Bitmap或是调用getPixel),系统会再次调用BitmapFactory decoder重新生成Bitmap的Pixel数组.为了能够重新解码图像,bitmap要能够访问存储Bitmap的原始数据.在inPurgeable为false时表示创建的Bitmap的Pixel内存空间不能被回收,这样BitmapFactory在不停decodeByteArray创建新的Bitmap对象,不同设备的内存不同,因此能够同时创建的Bitmap个数可能有所不同,200个bitmap足以使大部分的设备重新OutOfMemory错误.当isPurgable设为true时,系统中内存不足时,可以回收部分Bitmap占据的内存空间,这时一般不会出现OutOfMemory 错误.

 

 

 

 

 


 

原创粉丝点击