Android 高效安全加载图片

本人只是 Android小菜一个,写技术文档只是为了总结本身在最近学习到的知识,历来不敢为人师,若是里面有些不正确的地方请你们尽情指出,谢谢!html

1. 概述

Android 应用程序的设计中,几乎不可避免地都须要加载和显示图片,因为不一样的图片在大小上千差万别,有些图片可能只须要几十KB的内存空间,有些图片却须要占用几十MB的内存空间;或者一张图片不须要占用太多的内存,可是须要同时加载和显示多张图片。java

在这些状况下,加载图片都须要占用大量的内存,而 Android系统分配给每一个进程的内存空间是有限的,若是加载的图片所须要的内存超过了限制,进程就会出现 OOM,即内存溢出。android

本文针对加载大图片或者一次加载多张图片等两种不一样的场景,采用不一样的加载方式,以尽可能避免可能致使的内存溢出问题。缓存

2. 加载大图片

有时一张图片的加载和显示就须要占用大量的内存,例如图片的大小是 2592x1936 ,同时采用的位图配置是 ARGB_8888 ,其在内存中须要的大小是 2592x1936x4字节,大概是 19MB。仅仅加载这样一张图片就可能会超过进程的内存限制,进而致使内存溢出,因此在实际使用时确定没法直接加载到内存中。bash

为了不内存溢出,根据不一样的显示需求,采起不一样的加载方式:markdown

  • 显示一张图片的所有内容:对原图片进行 压缩显示
  • 显示一张图片的部份内容:对原图片进行 局部显示

2.1 图片压缩显示

图片的压缩显示指的是对原图片进行长宽的压缩,以减小图片的内存占用,使其可以在应用上正常显示,同时保证在加载和显示过程当中不会出现内存溢出的状况。 BitmapFactory 是一个建立Bitmap 对象的工具类,使用它能够利用不一样来源的数据生成Bitamp对象,在建立过的过程当中还能够对须要生成的对象进行不一样的配置和控制,BitmapFactory的类声明以下:网络

Creates Bitmap objects from various sources, including files, streams,and byte-arrays.
复制代码

因为在加载图片前,是没法提早预知图片大小的,因此在实际加载前必须根据图片的大小和当前进程的内存状况来决定是否须要对图片进行压缩,若是加载原图片所需的内存空间已经超过了进程打算提供或能够提供的内存大小,就必须考虑压缩图片。ide

2.1.1 肯定原图片长宽

简单来讲,压缩图片就是对原图的长宽按照必定的比例进行缩小,因此首先要肯定原图的长宽信息。为了得到图片的长宽信息,利用 BitmapFactory.decodeResource(Resources res, int id, Options opts) 接口,其声明以下:函数

/** * Synonym for opening the given resource and calling * {@link #decodeResourceStream}. * * @param res The resources object containing the image data * @param id The resource id of the image data * @param opts null-ok; Options that control downsampling and whether the * image should be completely decoded, or just is size returned. * @return The decoded bitmap, or null if the image data could not be * decoded, or, if opts is non-null, if opts requested only the * size be returned (in opts.outWidth and opts.outHeight) * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig} * is {@link android.graphics.Bitmap.Config#HARDWARE} * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve} */
    public static Bitmap decodeResource(Resources res, int id, Options opts) {
复制代码

经过这个函数声明,能够看到经过这个接口能够获得图片的长宽信息,同时因为返回 null并不申请内存空间,避免了没必要要的内存申请。工具

为了获得图片的长宽信息,必须传递一个 Options 参数,其中的 inJustDecodeBounds 设置为 true,其声明以下:

/** * If set to true, the decoder will return null (no bitmap), but * the <code>out...</code> fields will still be set, allowing the caller to * query the bitmap without having to allocate the memory for its pixels. */
    public boolean inJustDecodeBounds;
复制代码

下面给出获得图片长宽信息的示例代码:

BitmapFactory.Options options = new BitmapFactory.Options();
    // 指定在解析图片文件时,仅仅解析边缘信息而不建立 bitmap 对象。
    options.inJustDecodeBounds = true;
    // R.drawable.test 是使用的 2560x1920 的测试图片资源文件。
    BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
    int width = options.outWidth;
    int height = options.outHeight;
    Log.i(TAG, "width: " + width + ", height: " + height);
复制代码

在实际测试中,获得的长宽信息以下:

01-05 04:06:23.022 29836 29836 I Android_Test: width: 2560, height: 1920
复制代码

2.1.2 肯定目标压缩比例

得知原图片的长宽信息后,为了可以进行后续的压缩操做,必需要先肯定目标压缩比例。所谓压缩比例就是指要对原始的长宽进行的裁剪比例,若是若是原图片是 2560x1920,采起的压缩比例是 4,进行压缩后的图片是 640x480,最终大小是原图片的1/16。 压缩比例在 BitmapFactory.Options中对应的属性是 inSampleSize,其声明以下:

/** * If set to a value > 1, requests the decoder to subsample the original * image, returning a smaller image to save memory. The sample size is * the number of pixels in either dimension that correspond to a single * pixel in the decoded bitmap. For example, inSampleSize == 4 returns * an image that is 1/4 the width/height of the original, and 1/16 the * number of pixels. Any value <= 1 is treated the same as 1. Note: the * decoder uses a final value based on powers of 2, any other value will * be rounded down to the nearest power of 2. */
    public int inSampleSize;
复制代码

须要特别注意的是,inSampleSize 只能是 2的幂,若是传入的值不知足条件,解码器会选择一个和传入值最节俭的2的幂;若是传入的值小于 1,解码器会直接使用1

要肯定最终的压缩比例,首先要肯定目标大小,即压缩后的目标图片的长宽信息,根据原始长宽和目标长宽来选择一个最合适的压缩比例。下面给出示例代码:

/** * @param originWidth the width of the origin bitmap * @param originHeight the height of the origin bitmap * @param desWidth the max width of the desired bitmap * @param desHeight the max height of the desired bitmap * @return the optimal sample size to make sure the size of bitmap is not more than the desired. */
    public static int calculateSampleSize(int originWidth, int originHeight, int desWidth, int desHeight) {
        int sampleSize = 1;
        int width = originWidth;
        int height = originHeight;
        while((width / sampleSize) > desWidth && (height / sampleSize) > desHeight) {
            sampleSize *= 2;
        }
        return sampleSize;
    }
复制代码

须要注意的是这里的desWidthdesHeight 是目标图片的最大长宽值,而不是最终的大小,由于经过这个方法肯定的压缩比例会保证最终的图片长宽不大于目标值。 在实际测试中,把原图片大小设置为2560x1920,把目标图片大小设置为100x100:

int sampleSize = BitmapCompressor.calculateSampleSize(2560, 1920, 100, 100);
    Log.i(TAG, "sampleSize: " + sampleSize);
复制代码

测试结果以下:

01-05 04:42:07.752  8835  8835 I Android_Test: sampleSize: 32
复制代码

最终获得的压缩比例是32,若是使用这个比例去压缩2560x1920的图片,最终获得80x60的图片。

2.1.3 压缩图片

在前面两部分,分别肯定了原图片的长宽信息和目标压缩比例,其实肯定原图片的长宽也是为了获得压缩比例,既然已经获得的压缩比较,就能够进行实际的压缩操做了,只须要把获得的inSampleSize经过Options传递给BitmapFactory.decodeResource(Resources res, int id, Options opts)便可。 下面是示例代码:

public static Bitmap compressBitmapResource(Resources res, int resId, int inSampleSize) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = false;
        options.inSampleSize = inSampleSize;
        return BitmapFactory.decodeResource(res, resId, options);
    }
复制代码

2.2 图片局部显示

图片压缩会在必定程度上影响图片质量和显示效果,在某些场景下并不可取,例如地图显示时要求必须是高质量图片,这时就不能进行压缩处理,在这种场景下其实并不要求要一次显示图片的全部部分,能够考虑一次只加载和显示图片的特定部分,即***局部显示***。

要实现局部显示的效果,可使用BitmapRegionDecoder 来实现,它就是用来对图片的特定部分进行显示的,尤为是在原图片特别大而没法一次所有加载到内存的场景下,其声明以下:

/** * BitmapRegionDecoder can be used to decode a rectangle region from an image. * BitmapRegionDecoder is particularly useful when an original image is large and * you only need parts of the image. * * <p>To create a BitmapRegionDecoder, call newInstance(...). * Given a BitmapRegionDecoder, users can call decodeRegion() repeatedly * to get a decoded Bitmap of the specified region. * */
    public final class BitmapRegionDecoder { ... }
复制代码

这里也说明了若是使用BitmapRegionDecoder进行局部显示:首先经过newInstance()建立实例,再利用decodeRegion()对指定区域的图片内存建立Bitmap对象,进而在显示控件中显示。

经过BitmapRegionDecoder.newInstance()建立解析器实例,其函数声明以下:

/** * Create a BitmapRegionDecoder from an input stream. * The stream's position will be where ever it was after the encoded data * was read. * Currently only the JPEG and PNG formats are supported. * * @param is The input stream that holds the raw data to be decoded into a * BitmapRegionDecoder. * @param isShareable If this is true, then the BitmapRegionDecoder may keep a * shallow reference to the input. If this is false, * then the BitmapRegionDecoder will explicitly make a copy of the * input data, and keep that. Even if sharing is allowed, * the implementation may still decide to make a deep * copy of the input data. If an image is progressively encoded, * allowing sharing may degrade the decoding speed. * @return BitmapRegionDecoder, or null if the image data could not be decoded. * @throws IOException if the image format is not supported or can not be decoded. * * <p class="note">Prior to {@link android.os.Build.VERSION_CODES#KITKAT}, * if {@link InputStream#markSupported is.markSupported()} returns true, * <code>is.mark(1024)</code> would be called. As of * {@link android.os.Build.VERSION_CODES#KITKAT}, this is no longer the case.</p> */
    public static BitmapRegionDecoder newInstance(InputStream is, boolean isShareable) throws IOException { ... }
复制代码

须要注意的是,这只是BitmapRegionDecoder其中一个newInstance函数,除此以外还有其余的实现形式,读者有兴趣能够本身查阅。 在建立获得BitmapRegionDecoder实例后,能够调用decodeRegion方法来建立局部Bitmap对象,其函数声明以下:

/** * Decodes a rectangle region in the image specified by rect. * * @param rect The rectangle that specified the region to be decode. * @param options null-ok; Options that control downsampling. * inPurgeable is not supported. * @return The decoded bitmap, or null if the image data could not be * decoded. * @throws IllegalArgumentException if {@link BitmapFactory.Options#inPreferredConfig} * is {@link android.graphics.Bitmap.Config#HARDWARE} * and {@link BitmapFactory.Options#inMutable} is set, if the specified color space * is not {@link ColorSpace.Model#RGB RGB}, or if the specified color space's transfer * function is not an {@link ColorSpace.Rgb.TransferParameters ICC parametric curve} */
    public Bitmap decodeRegion(Rect rect, BitmapFactory.Options options) { ... }
复制代码

因为这部分比较简单,下面直接给出相关示例代码:

// 解析获得原图的长宽值,方便后面进行局部显示时指定须要显示的区域。
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inJustDecodeBounds = true;
    BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
    int width = options.outWidth;
    int height = options.outHeight;

    try {
        // 建立局部解析器 
        InputStream inputStream = getResources().openRawResource(R.drawable.test);
        BitmapRegionDecoder decoder = BitmapRegionDecoder.newInstance(inputStream,false);
        
        // 指定须要显示的矩形区域,这里要显示的原图的左上 1/4 区域。
        Rect rect = new Rect(0, 0, width / 2, height / 2);

        // 建立位图配置,这里使用 RGB_565,每一个像素占 2 字节。
        BitmapFactory.Options regionOptions = new BitmapFactory.Options();
        regionOptions.inPreferredConfig = Bitmap.Config.RGB_565;
        
        // 建立获得指定区域的 Bitmap 对象并进行显示。
        Bitmap regionBitmap = decoder.decodeRegion(rect,regionOptions);
        ImageView imageView = (ImageView) findViewById(R.id.main_image);
        imageView.setImageBitmap(regionBitmap);
    } catch (Exception e) {
        e.printStackTrace();
    }
复制代码

从测试结果看,确实只显示了原图的左上1/4区域的图片内容,这里再也不贴出结果。

3. 加载多图片

有时须要在应用中同时显示多张图片,例如使用ListView,GridViewViewPager时,可能会须要在每一项都显示一个图片,这时状况就会变得复杂些,由于能够经过滑动改变控件的可见项,若是每增长一个可见项就加载一个图片,同时不可见项的图片继续在内存中,随着不断的增长,就会致使内存溢出。

为了不这种状况的内存溢出问题,就须要对不可见项对应的图片资源进行回收,即当前项被滑出屏幕的显示区域时考虑回收相关的图片,这时回收策略对整个应用的性能有较大影响。

  • 当即回收:在当前项被滑出屏幕时当即回收图片资源,但若是被滑出的项很快又被滑入屏幕,就须要从新加载图片,这无疑会致使性能的降低。
  • 延迟回收:在当前项被滑出屏幕时不当即回收,而是根据必定的延迟策略进行回收,这时对延迟策略有较高要求,若是延迟时间过短就退回到当即回收情况,若是延迟时间较长就可能致使一段时间内,内存中存在大量的图片,进而引起内存溢出。 经过上面的分析,针对加载多图的状况,必需要采起延迟回收,而Android提供了一中基于LRU,即最近最少使用策略的内存缓存技术: LruCache, 其基本思想是,以强引用的方式保存外界对象,当缓存空间达到必定限制后,再把最近最少使用的对象释放回收,保证使用的缓存空间始终在一个合理范围内。

其声明以下:

/** * A cache that holds strong references to a limited number of values. Each time * a value is accessed, it is moved to the head of a queue. When a value is * added to a full cache, the value at the end of that queue is evicted and may * become eligible for garbage collection. */
public class LruCache<K, V> { ... }
复制代码

从声明中,能够了解到其实现LRU的方式:内部维护一个有序队列,每当其中的一个对象被访问就被移动到队首,这样就保证了队列中的对象是根据最近的使用时间从近到远排列的,即队首的对象是最近使用的,队尾的对象是最久以前使用的。正是基于这个规则,若是缓存达到限制后,直接把队尾对象释放便可。

在实际使用中,为了建立LruCache对象,首先要肯定该缓存可以使用的内存大小,这是效率的决定性因素。若是缓存内存过小,没法真正发挥缓存的效果,仍然须要频繁的加载和回收资源;若是缓存内存太大,可能致使内存溢出的发生。在肯定缓存大小的时候,要结合如下几个因素:

  • 进程可使用的内存状况
  • 资源的大小和须要一次在界面上显示的资源数量
  • 资源的访问频率

下面给出一个简单的示例:

// 得到进程可使用的最大内存量
    int maxMemory = (int) Runtime.getRuntime().maxMemory();
    
    mCache = new LruCache<String, Bitmap>(maxMemory / 4) {
        @Override
        protected int sizeOf(String key, Bitmap value) {
            return value.getByteCount();
        }
    };
复制代码

在示例中简单地把缓存大小设定为进程可使用的内存的 1/4,固然在实际项目中,要考虑的因素会更多。须要注意的是,在建立LruCache对象的时候须要重写sizeOf方法,它用来返回每一个对象的大小,是用来决定当前缓存实际大小并判断是否达到了内存限制。

在建立了LruCache对象后,若是须要使用资源,首先到缓存中去取,若是成功取到就直接使用,不然加载资源并放入缓存中,以方便下次使用。为了加载资源的行为不会影响应用性能,须要在子线程中去进行,能够利用AsyncTask来实现。 下面是示例代码:

public Bitmap get(String key) {
        Bitmap bitmap = mCache.get(key);
        if (bitmap != null) {
            return bitmap;
        } else {
            new BitmapAsyncTask().execute(key);
            return null;
        }
    }

    private class BitmapAsyncTask extends AsyncTask<String, Void, Bitmap> {
        @Override
        protected Bitmap doInBackground(String... url) {
            Bitmap  bitmap = getBitmapFromUrl(url[0]);
            if (bitmap != null) {
                mCache.put(url[0],bitmap);
            }
            return bitmap;
        }

        private Bitmap getBitmapFromUrl(String url) {
            Bitmap bitmap = null;
            // 在这里要利用给定的 url 信息从网络获取 bitmap 信息.
            return bitmap;
        }
    }
复制代码

示例中,在没法从缓存中获取资源的时候,会根据url信息加载网络资源,当前并无给出完整的代码,有兴趣的同窗能够本身去完善。

4. 总结

本文主要针对不一样的图片加载场景提出了不一样的加载策略,以保证在加载和显示过程当中既然能知足基本的显示需求,又不会致使内存溢出,具体包括针对单个图片的压缩显示,局部显示和针对多图的内存缓存技术,如如有表述不清甚至错误的地方,请及时提出,你们一块儿学习。

相关文章
相关标签/搜索