Android中如何去处理大图片导致的OOM

降低图片内存占用-我们去压缩图片

从上面说的来看,影响一张图片占用内存的有两方面的因素,( 1 )压缩尺寸 ( 2 )色彩模式;
从色彩模式的角度,对于一个 ARGB_8888 的图片,在满足业务需求的情况下,比如并不要求这张图片特别清晰逼真,那么可以在压缩尺寸之前,可以同时将option 的值重新设置一下,比如设置为 RGB_565
options.inPreferredConfig = Bitmap.Config.RGB_565;

ARGB_8888,表示一个像素占8+8+8+8=32=4字节,而RGB_565,表示一个像素占用5+6+5=16=2 字节。这样设置之后图片内存占用会减半

下面的三个步骤,可以解决好OOM的问题:

下面重点来看一下尺寸方面的压缩。我们首先要知道这张图片的原始尺寸是多少,然后才能决定压缩的比例。
 

第一步、预先获取图片的原始尺寸

为了避免 OOM 异常,我们在解析每张图片之前,最好都能预先检查一下图片的大小,以保证这些图片不会过大,占用太多内存。
BitmapFactory 这个类提供了多个解析方法 (decodeByteArray, decodeFile, decodeResource ) 用于创建Bitmap 对象,我们应该根据图片的来源选择合适的方法。
比如 SD 卡中的图片可以使用 decodeFile 方法,网络上的图片可以使用 decodeStream 方法,资源文件中的图片可以使用decodeResource 方法。但这些方法会尝试为已经构建的 bitmap 分配内存,这时就会很容易导致OOM 出现。
为此每一种解析方法都提供了一个可选的 BitmapFactory.Options 参数,当将这个参数的
inJustDecodeBounds 属性设置为 true 时,我们再去解析图片,这是解析方法返回的 bitmap 对象为null, 但是 BitmapFactory.Options outHeight/outWidth/outMimeType 等属性都会被赋值。
这个技巧让我们可以获取到图片的长宽值和 MIME 类型等信息,同时解析方法不会给 bitmap 分配内存。
如下代码所示:
BitmapFactory.Options options = new BitmapFactory.Options(); 
options.inJustDecodeBounds = true; 
BitmapFactory.decodeResource(getResources(), R.id.my_image, options); 
int imageHeight = options.outHeight; 
int imageWidth = options.outWidth; 
String imageType = options.outMimeType;

 第二步、压缩图片尺寸

现在图片的大小已经知道了,我们就可以决定是把整张图片加载到内存中还是加载一个压缩版的图片到内存中。
比如,你的 ImageView 只有 128x96 像素的大小,只是为了显示一张缩略图,这时候把一张 1024x768 像素的图片完全加载到内存中显然是不值得的。
那我们怎样才能对图片进行压缩呢?通过设置 BitmapFactory.Options inSampleSize 的值就可以实现。
比如我们有一张 2048x1536 像素的图片,将 inSampleSize 的值设置为 4 ,就可以把这张图片压缩成
512x384 像素。

原本加载这张图片需要占用12M的内存,压缩后就只需要占用0.75M(假设图片是ARGB_8888类型,即每个像素点占用4个字节)

下面的方法可以根据传入的宽和高,计算出合适的 inSampleSize 值:
 public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        // 源图片的高度和宽度 
        final int height = options.outHeight;
        final int width = options.outWidth;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            // 计算出实际宽高和目标宽高的比率 
            final int heightRatio = Math.round((float) height / (float) reqHeight);
            final int widthRatio = Math.round((float) width / (float) reqWidth);
            // 选择宽和高中最小的比率作为inSampleSize的值,这样可以保证最终图片的宽和高一定都会大 于等于目标的宽和高。
            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;
        }
        return inSampleSize;
    }

使用这个方法,首先你要将BitmapFactory.OptionsinJustDecodeBounds属性设置为true,解析一次图片。然后将BitmapFactory.Options连同期望的宽度和高度一起传递到calculateInSampleSize方法中,就可以得到合适的inSampleSize值了。之后再解析一次图片,使用新获取到的inSampleSize值,并把inJustDecodeBounds设置为false,就可以得到压缩后的图片


    public static Bitmap decodeSampledBitmapFromResource(Resources res, int resId, int reqWidth, int reqHeight) {
        // 第一次解析将inJustDecodeBounds设置为true,来获取图片大小 
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeResource(res, resId, options);
        // 调用上面定义的方法计算inSampleSize值
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        // 使用获取到的inSampleSize值再次解析图片 
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeResource(res, resId, options);
    }

下面的代码非常简单地将任意一张图片压缩成100x100的缩略图,并在ImageView上展示。

mImageView.setImageBitmap( decodeSampledBitmapFromResource(getResources(), R.id.myimage, 100, 100));

 第三步、加载大量图片 内存缓存技术

一张图片的问题解决了,有多张图片要加载怎么办?比如使用 ListView, GridView 或者 ViewPager 这样的组件来加载图片,屏幕上显示的图片可以通过滑动屏幕等事件不断地增加,就有可能导致OOM
为了保证内存的使用始终维持在一个合理的范围,通常会把被移除屏幕的图片进行回收处理。此时垃圾回收器也会认为你不再持有这些图片的引用,从而对这些图片进行GC 操作。
但是这个带来另外一个问题。当某些图片滑出屏幕并回收之后,用户有可能又把它重新滑入屏幕,这时就需要把原来加载过的图片重新加载一遍。这样性能肯定是瓶颈。

使用内存缓存技术可以很好的解决这个问题,它可以让组件快速地重新加载和处理图片。

下面我们就来看一看如何使用内存缓存技术来对图片进行缓存,从而让你的应用程序在加载很多图片的时候可以提高响应速度和流畅性。
内存缓存技术对那些大量占用应用程序宝贵内存的图片提供了快速访问的方法。其中最核心的类是
LruCache ( 此类在 android-support-v4 的包中提供 )
LruCache 非常适合用来缓存图片,它的主要算法原理是把最近使用的对象用强引用存储在
LinkedHashMap 中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。
之前非常流行的内存缓存技术使用的是软引用或弱引用 (SoftReference or WeakReference) 。但是现在已经不再推荐使用这种方式了,因为从 Android 2.3 (API Level 9) 开始,垃圾回收器会更倾向于回收持有软引用或弱引用的对象,这让软引用和弱引用变得不再可靠。另外,Android 3.0 (API Level 11) 中,图片的数据会存储在本地的内存当中,因而无法用一种可预见的方式将其释放,这就有潜在的风险造成应用程序的内存溢出并崩溃。
为了能够选择一个合适的缓存大小给 LruCache ,需要考虑以下几个因素,例如:

 
1 )应用程序最大可用内存是多少?
2 )设备屏幕上一次最多能显示多少张图片?有多少图片需要进行预加载,因为有可能很快也会显示在屏幕上?
3 )设备的屏幕大小和分辨率分别是多少?
一个超高分辨率的设备比起一个较低分辨率的设备,在持有相同数量图片的时候,需要更大的缓存空间。
4 )图片的尺寸和大小,还有每张图片会占据多少内存空间。
5 )图片被访问的频率有多高?会不会有一些图片的访问频率比其它图片要高?
如果有的话,你也许应该让一些图片常驻在内存当中,或者使用多个 LruCache 对象来区分不同组的图
片。
6 )平衡数量和质量。有时候,存储多个低像素的图片,同时在后台去开线程加载高像素的图片会更有效。

 缓存大小不是固定的,应当具体情况具体分析。但是不能太小,也不能太大;因为如果缓存太小,有可能造成图片频繁地被释放和重新加载;而缓存太大,则有可能会引起OOM。 下面是一个使用 LruCache来缓存图片的例子:


    private LruCache<String, Bitmap> mMemoryCache;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // 获取到可用内存的最大值,使用内存超出这个值会引起OutOfMemory异常。 
        // LruCache通过构造函数传入缓存值,以KB为单位。 
        int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
        // 使用最大可用内存值的1/8作为缓存的大小。 
        int cacheSize = maxMemory / 8;
        mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                // 重写此方法来衡量每张图片的大小,默认返回图片数量。 
                return bitmap.getByteCount() / 1024;
            }
        };
    }

    public void addBitmapToMemoryCache(String key, Bitmap bitmap) {
        if (getBitmapFromMemCache(key) == null) {
            mMemoryCache.put(key, bitmap);
        }
    }

    public Bitmap getBitmapFromMemCache(String key) {
        return mMemoryCache.get(key);
    }

在这个例子当中,使用了系统分配给应用程序的八分之一内存来作为缓存大小。在中高配置的手机当中,这大概会有4(32/8)的缓存空间。

一个全屏幕的 GridView 使用 4 800x480 分辨率的图片来填充,则大概会占用 1.5 兆的空间
(800x480x4) 。因此,这个缓存大小可以存储 2.5 页的图片。
当向 ImageView 中加载一张图片时,首先会在 LruCache 的缓存中进行检查。
如果找到了相应的键值,则会立刻更新 ImageView ,否则开启一个后台线程来加载这张图片。

    public void loadBitmap(int resId, ImageView imageView) {
        final String imageKey = String.valueOf(resId);
        final Bitmap bitmap = getBitmapFromMemCache(imageKey);
        if (bitmap != null) {
            imageView.setImageBitmap(bitmap);
        } else {
            imageView.setImageResource(R.drawable.image_placeholder);
            BitmapWorkerTask task = new BitmapWorkerTask(imageView);
            task.execute(resId);
        }
    }

 BitmapWorkerTask 还要把新加载的图片的键值对放到缓存中

class BitmapWorkerTask extends AsyncTask<Integer, Void, Bitmap> {
    // 在后台加载图片。 
    @Override
    protected Bitmap doInBackground(Integer... params) {
        final Bitmap bitmap = decodeSampledBitmapFromResource(getResources(), params[0], 100, 100);
        addBitmapToMemoryCache(String.valueOf(params[0]), bitmap);
        return bitmap;
    }
}

 以上方法,是程序加载超大图片和大量图片的基本优化方法,也是开源框架如Universal-Image-Loader等的基本原理。掌握了这个,再去看一些图片加载框架的源码,应该就很轻松了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值