Android Bitmap OOM 问题

OOM 就是 Out Of Memory (内存溢出)

Dalvik 最大内存

Android 系统会为每个 APP 分配一个独立的工作空间或者说分配一个单独的 Dalvik 虚拟机,让每个 APP 都可以独立运行而不相互影响

Android 对于每个 Dalvik 虚拟机都会有一个最大内存限制,如果当前占用的内存加上我们申请的内存资源超过了这个限制 ,系统就会抛出 OOM 错误

当然,你不要将 OOM 和 RAM(物理内存) 混淆了,即使当前 RAM 中剩余的内存有 1G 多,但是 OOM 还是会发生

如果 RAM 不足的话,就是杀应用了,而不是仅仅是 OOM 了

Dalvik 的最大内存标准,不同的机型是不一样的,可以使用下面的代码查看

ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE);
Log.e("oom","最大内存:" + activityManager.getMemoryClass());

或者直接在命令行键入

adb shell getprop | grep dalvik.vm.heapgrowthlimit

也可以打开系统源码 /system/build.prop 文件,看下文件中这一部分的信息得出

dalvik.vm.heapstartsize=8m
dalvik.vm.heapgrowthlimit=192m
dalvik.vm.heapsize=512m
dalvik.vm.heaptargetutilization=0.75
dalvik.vm.heapminfree=2m
dalvik.vm.heapmaxfree=8m

我们关注的地方有三个

  1. heapstartsize 堆内存的初始大小
  2. heapgrowthlimit 标准的应用的最大堆内存大小
  3. heapsize 设置了使用 android:largeHeap 的应用的最大堆内存大小

避免 Bitmap 引起的 OOM

  1. 使用低内存占用量的编码方式

    通过设置 BitmapFactory.Options 类中的属性 inPreferredConfig

    默认是 Bitmap.Config.ARGB_8888 ,可以修改成 Bitmap.Config.ARGB_4444

    • Bitmap.Config ARGB_4444

      每个像素占四位,即 A=4,R=4,G=4,B=4,那么一个像素点占 4+4+4+4=16 位

    • Bitmap.Config ARGB_8888 每个像素占八位,即 A=8,R=8,G=8,B=8,那么一个像素点占 8+8+8+8=32位

  2. 压缩图片

    通过设置 BitmapFactory.Options 类中的属性 inSampleSize

    inSampleSize 用于设置缩放倍数,比如写 2,即长宽变为原来的 1/2 ,图片就是原来的 1/4

    如果不进行缩放的话设置为 1 即可

    但是不能一味的压缩,毕竟这个值太小的话,图片会很模糊,而且要避免图片的拉伸变形,所以需要在程序中动态的计算,这个 inSampleSize 的合适值

    这时候就会用到 BitmapFactory.Options 类的一个属性 inJustDecodeBounds,设置为 true 后,decodeFiel 并不会分配内存空间,但是可以计算出原始图片的长宽

    调用 options.outWidth | outHeight 获取出图片的宽高,然后通过一定的算法,即可得到适合的 inSampleSize

    public static int caculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        int width = options.outWidth;
        int height = options.outHeight;
        int inSampleSize = 1;
        if (width > reqWidth || height > reqHeight) {
            int widthRadio = Math.round(width * 1.0f / reqWidth);
            int heightRadio = Math.round(height * 1.0f / reqHeight);
            inSampleSize = Math.max(widthRadio, heightRadio);
        }
        return inSampleSize;
    }
    

    然后就可以如下使用了

    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true; // 设置了此属性一定要记得将值设置为false
    Bitmap bitmap = null;
    bitmap = BitmapFactory.decodeFile(url, options);
    options.inSampleSize = computeSampleSize(options,128,128);
    options.inPreferredConfig = Bitmap.Config.ARGB_4444;
    /* 下面两个字段需要组合使用 */  
    options.inPurgeable = true;
    options.inInputShareable = true;
    options.inJustDecodeBounds = false;
    try {
        bitmap = BitmapFactory.decodeFile(url, options);
    } catch (OutOfMemoryError e) {
            Log.e(TAG, "OutOfMemoryError");
    }
    
  3. 及时回收图像

    如果引用了大量的 Bitmap 对象,而应用又不需要同时显示所有图片可以将暂时不用到的 Bitmap 对象及时回收掉

    对于一些明确知道图片使用情况的场景可以主动 recycle 回收,比如引导页的图片,使用 完就 recycle,帧动画,加载一张,画一张,释放一张

    使用时加载,不显示时直接置 null 或 recycle 比如:imageView.setImageResource(0);

    不过某些情况下会出现特定图片反复加载,释放,再加载等,低效率的事情.

  4. 简单通过 SoftReference 引用方式管理图片资源

    建个 SoftReference 的 hashmap 使用图片时先查询这个 hashmap 是否有 softreference, softreference 里的图片是否为空,如果为空就加载图片到 softreference 并加入hashmap 无需再代码里显式的处理图片的回收与释放,gc 会自动处理资源的释放

    这种方式处理起来简单实用,能一定程度上避免前一种方法反复加载释放的低效率

    private Map<String, SoftReference<Bitmap>> imageMap = new HashMap<String, SoftReference<Bitmap>>();
    
    public Bitmap loadBitmap(final String imageUrl,final ImageCallBack imageCallBack) {
        SoftReference<Bitmap> reference = imageMap.get(imageUrl);
        if(reference != null) {
            if(reference.get() != null) {
                return reference.get();
            }
        }
        final Handler handler = new Handler() {
            public void handleMessage(final android.os.Message msg) {
                //加入到缓存中
                Bitmap bitmap = (Bitmap)msg.obj;
                imageMap.put(imageUrl, new SoftReference<Bitmap>(bitmap));
                if(imageCallBack != null) {
                    imageCallBack.getBitmap(bitmap);
                }
            }
        };
        new Thread(){
            public void run() {
                Message message = handler.obtainMessage();
                message.obj = downloadBitmap(imageUrl);
                handler.sendMessage(message);
            }
        }.start();
        return null ;
    }
    
    // 从网上下载图片
    private Bitmap downloadBitmap (String imageUrl) {
        Bitmap bitmap = null;
        try {
            bitmap = BitmapFactory.decodeStream(new URL(imageUrl).openStream());
            return bitmap ;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        } 
    }
    
    public interface ImageCallBack{
        void getBitmap(Bitmap bitmap);
    }
    
  5. 使用 LruCache + sd 的缓存方式

    可以使用 Android 内置的 LruCache 类进行 cache 处理

    当存储 Image 的大小大于 LruCache 设定的值,那么近期使用次数最少的图片就会被回收掉,系统会自动释放内存

    1. 先设置缓存图片的内存大小,比如设置为手机内存的 1/8,

      手机内存大小的获取方式

      int MAXMEMONRY = (int)(Runtime.getRuntime() .maxMemory()/1024);
      
    2. LruCache 里面的键值对分别是 URL 和对应的图片

    3. 重写了 sizeOf() 的方法,返回的是图片数量

      private LruCache<String, Bitmap> mMemoryCache;
      private LruCacheUtils() {
          if (mMemoryCache == null)
              mMemoryCache = new LruCache<String, Bitmap>(
                      MAXMEMONRY / 8) {
                  @Override
                  protected int sizeOf(String key, Bitmap bitmap) {
                      // 重写此方法来衡量每张图片的大小,默认返回图片数量。
                      return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
                  }
      
                  @Override
                  protected void entryRemoved(boolean evicted, String key,
                          Bitmap oldValue, Bitmap newValue) {
                      Log.v("tag", "hard cache is full , push to soft cache");
      
                  }
              };
      }
      
    4. 实现清空缓存、添加图片到缓存、从缓存中取得图片、从缓存中移除移除和清除缓存是必须要做的事,因为图片缓存处理不当就会报内存溢出,所以一定要引起注意

      public void clearCache() {
          if (mMemoryCache != null) {
              if (mMemoryCache.size() > 0) {
                  Log.d("CacheUtils",
                          "mMemoryCache.size() " + mMemoryCache.size());
                  mMemoryCache.evictAll();
                  Log.d("CacheUtils", "mMemoryCache.size()" + mMemoryCache.size());
              }
              mMemoryCache = null;
          }
      }
      
      public synchronized void addBitmapToMemoryCache(String key, Bitmap bitmap) {
          if (mMemoryCache.get(key) == null) {
              if (key != null && bitmap != null)
                  mMemoryCache.put(key, bitmap);
          } else
              Log.w(TAG, "the res is aready exits");
      }
      
      public synchronized Bitmap getBitmapFromMemCache(String key) {
          Bitmap bm = mMemoryCache.get(key);
          if (key != null) {
              return bm;
          }
          return null;
      }
      
      /**
       * 移除缓存
       * 
       * @param key
       */
      public synchronized void removeImageCache(String key) {
          if (key != null) {
              if (mMemoryCache != null) {
                  Bitmap bm = mMemoryCache.remove(key);
                  if (bm != null)
                      bm.recycle();
              }
          }
      }
      

参考文档

  1. 图片缓存之内存缓存技术LruCache,软引用

Android 基础教程

关于   |   FAQ   |   我们的愿景   |   广告投放   |  博客

  简单教程,简单编程 - IT 入门首选站

Copyright © 2013-2022 简单教程 twle.cn All Rights Reserved.