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. 總結

本文主要針對不一樣的圖片加載場景提出了不一樣的加載策略,以保證在加載和顯示過程當中既然能知足基本的顯示需求,又不會致使內存溢出,具體包括針對單個圖片的壓縮顯示,局部顯示和針對多圖的內存緩存技術,如如有表述不清甚至錯誤的地方,請及時提出,你們一塊兒學習。

相關文章
相關標籤/搜索