1. 程式人生 > >Android高效ImageLoader的實現

Android高效ImageLoader的實現

在android開發過程圖片載入和顯示基本上是每個專案中都會包含的功能,這就導致每個專案裡面ImageLoader是標配。當然我們在使用的過程中有很多牛逼的(效能好,使用簡單方便)開源框架可供挑選。但是如果自己手動實現一個高效的ImageLoader那給自己的技術樹裡面又添加了一個靚麗的枝幹。ok,接下來我們一起來分析和探討一下高效ImageLoder的實現。

一般來說,優秀的ImageLoader都具有以下幾個共性:

  • 圖片的同步載入
  • 圖片的非同步載入
  • 圖片按需要壓縮
  • 記憶體快取
  • 磁碟快取
  • 網路拉取
  • 使用方便簡單
  • 效能好

雖然我們可能做不到優秀,但是我們也得往這個目標和方向上使勁。所以接下來的實現中,我們也會盡力去做到這些。

  • 前兩年很多同行使用軟引用和弱引用來實現圖片的多級快取,但是現在使用軟引用和弱引用已經變得不再可靠,它主要存在以下幾點弊端和風險 因為從 Android 2.3 (API Level 9)開始,垃圾回收器會更傾向於回收持有軟引用或弱引用的物件,這讓軟引用和弱引用變得不再可靠。

  • 另外,Android 3.0 (API Level 11)中,圖片的資料會儲存在本地的記憶體當中,因而無法用一種可預見的方式將其釋放,這就有潛在的風險造成應用程式的記憶體溢位並崩。

基於這些問題和風險,我們使用在3.0以後被Android引入的LruCache來進行圖片記憶體快取,(這個類是3.1版本中提供的,如果你是在更早的Android版本中開發,則需要匯入android-support-v4的jar包)

LruCache在處理圖片釋放時使用的原則是最久未使用原則,即:當LruCache記憶體儲的圖片總大小大於指定記憶體時,自動釋放未使用時長最久的圖片。ok,記憶體快取就它了。

我們說過優秀ImageLoader還應該包含磁碟快取,那麼我們在磁碟快取中我們可以考慮用最基本的檔案存取來實現,因為一般來說,我們的磁碟快取在我看來就是對我們的資料進行一下備份,方便需要的時候獲取,不太需要多麼優秀的演算法來控制它們。但是現在業內的普遍做法是使用DisckLruCache來做磁碟快取。好吧,雖然筆者不太清楚這樣做的原理,但是我們照貓畫虎,也就用這個吧。畢竟我們是奔著優秀去的,業內的一些反響比較好的圖片載入框架都用的它,肯定是有使用的價值所在。

ok,分析完基本的技術選型,我們開始進入框架編寫的正題。

我們先從呼叫開始講起,我們這邊提供兩種呼叫的方式:同步呼叫和非同步非同步呼叫,我們把這兩個方法定義如下。

    /**
     * 圖片非同步載入
     */
    public void bind(String resource,ImageView imageView,int reqWidth,int reqHeight){

    }

    /**
     *圖片同步載入
     */
    public Bitmap load(String resource,int reqWidth,int reqHeight){
        Bitmap bitmap = null;
        return bitmap;
    }

兩種載入方式的實現,我們接下來一步一步地寫。圖片使用和載入的效率有高到低的順序為:記憶體快取 >SD卡快取>網路請求

ok,接下來我們對記憶體快取和SD卡快取進行初始化配置。在這裡我們提供一個ImgLoaderConfig來設定相關配置,這個主要包括以下幾個屬性(需要的話再進行進一步擴充套件):

    /**
     * 記憶體快取大小
     */
    private int memoryCacheSize;
    /**
     * SD卡快取大小
     */
    private long diskCacheSize;
    /**
     * SD卡快取路徑
     */
    private String diskCachePath;

好了,有個這個配置類,我們可以編寫我們的初始化配置方法了:

/**
     * 初始化方法
     *
     * @param config  圖片載入框架相關配置
     * @param context 上下文
     */
    public void init(ImgLoaderConfig config, Context context) {
        if (null == context) {
            throw new IllegalArgumentException("the context could not is null");
        }

        if (config.getMemoryCacheSize() <= 0) {
            int maxMemory = (int) Runtime.getRuntime().maxMemory() / 1024;
            memoryCacheSize = maxMemory / 8;
        } else {
            memoryCacheSize = config.getMemoryCacheSize();
        }

        if (config.getDiskCacheSize() <= 0) {
            diskCacheSize = DISK_CACHE_SIZE;
        } else {
            diskCacheSize = config.getDiskCacheSize();
        }

        mMemoryCache = new LruCache<String, Bitmap>(memoryCacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                //計算bitmap所佔記憶體,使用高版本api時可以使用 value.getByteCount();
                return value.getRowBytes() * value.getHeight() / 1024;
            }
        };

        if (TextUtils.isEmpty(config.getDiskCachePath())) {
            diskCachePath = context.getCacheDir().getAbsolutePath();
        } else {
            diskCachePath = config.getDiskCachePath();
        }

        File file = new File(diskCachePath);
        if (!file.exists()) {
            file.mkdirs();
        }

        try {
            mDiskLruCache = DiskLruCache.open(file, 1, 1, diskCacheSize);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

初始化配置完成後,我們來編寫優先順序最高的從記憶體快取存、取圖片:

/**
     * 將bitmap加入記憶體快取中
     *
     * @param key    快取的key
     * @param bitmap 待快取的bitmap
     */
    private void addImg2Memory(String key, Bitmap bitmap) {
        if (null == mMemoryCache) {
            throw new IllegalArgumentException("the memoryCache could not be null");
        }

        if (null == mMemoryCache.get(key)) {
            mMemoryCache.put(key, bitmap);
        }
    }

    /**
     * 根據key從記憶體快取中獲取bitmap
     *
     * @param key 快取的key
     * @return 快取的bitmap
     */
    private Bitmap loadImgFromMemory(String key) {
        if (null == mMemoryCache) {
            throw new IllegalArgumentException("the memoryCache could not be null");
        }

        if (mMemoryCache.size() < 1) {
            return null;
        }

        Bitmap bitmap = mMemoryCache.get(key);
        return bitmap;
    }

記憶體快取內如果無法取到圖片,我們就嘗試從SD快取內取(SD卡快取存取時需要注意一個細節問題,使用網路請求連結直接做存取的key是不可取的,因為連結內可能帶有特殊字元,所以需要把它們轉換成MD5的字串做為存取的key)

/**
     * 將圖片存入SD卡
     *
     * @param key         存取的key
     * @param inputStream 檔案輸入流
     */
    private void addImg2Disk(String key, InputStream inputStream) {

        String MD5key = MD5Util.getMD5Str(key);
        try {
            DiskLruCache.Editor editor = mDiskLruCache.edit(MD5key);
            OutputStream outputStream = editor.newOutputStream(0);
            if (writeImgToDisk(outputStream, inputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            mDiskLruCache.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 將圖片寫入SD卡
     *
     * @param outputStream 檔案的輸出流
     * @param inputStream  資料的輸入流
     * @return 寫入操作是否成功
     */
    private boolean writeImgToDisk(OutputStream outputStream, InputStream inputStream) {
        BufferedInputStream bis = new BufferedInputStream(inputStream);
        BufferedOutputStream bos = new BufferedOutputStream(outputStream);
        int b;
        try {
            while ((b = bis.read()) != -1) {
                bos.write(b);
            }
            bis.close();
            bos.flush();
            bos.close();
            return true;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return false;
    }
    /**
     * 從SD卡內載入所需圖片
     * @param key 存取key
     * @param reqWidth
     * @param reqHeight
     * @return
     */
    private Bitmap loadImgFromDisk(String key, int reqWidth, int reqHeight) {
        Bitmap bitmap = null;
        String MD5key = MD5Util.getMD5Str(key);
        try {
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(MD5key);
            if (null != snapshot) {
                //該處傳入引數0的意義不做贅述,技術細節延後討論
                FileInputStream fileInputStream = (FileInputStream) snapshot.getInputStream(0);
                FileDescriptor fd = fileInputStream.getFD();
                bitmap = ImgResizer.decodeFromFileDescriptor(fd, reqWidth, reqHeight);
                if (null != bitmap) {
                    addImg2Memory(key, bitmap);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
        return bitmap;
    }

如果在SD卡快取內同樣沒有找到圖片,則需要從網路進行載入(在這裡我們不做網路載入的優化的說明和探討,只給出最簡單的網路請求載入方式。網路請求的優化包括使用執行緒池排程請求和斷點續傳等留待下次專門開一篇部落格進行探討)

    /**
     * 從網路獲取圖片
     * @param url 獲取圖片的連結
     * @return 網路請求得到的輸入流
     */
    private InputStream reqImgFromHttp(String url) {
        try {
            URL httpUrl = new URL(url);
            InputStream inputStream = httpUrl.openConnection().getInputStream();
            return inputStream;
        } catch (MalformedURLException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

至此,我們的三級快取是編寫完了。我們來完善我們最開始定義的同步載入和非同步載入的方法(這裡只對非同步載入的方法進行說明,同步載入就不做贅述了):

    /**
     * 非同步的方式將圖片繫結到控制元件上
     *
     * @param source 圖片來源
     * @param img    需要繫結的控制元件
     */
    public void bind(final String source, final ImageView img, final int reqWidth, final int reqHeight) {
        if (TextUtils.isEmpty(source) || img == null) {
            return;
        }
        Bitmap bitmap = loadImgFromMemory(source);
        if (null != bitmap) {
            img.setImageBitmap(bitmap);
        } else {
            bitmap = loadImgFromDisk(source, reqWidth, reqHeight);
            if (null != bitmap) {
                img.setImageBitmap(bitmap);
            } else {
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        InputStream inputStream = reqImgFromHttp(source);
                        addImg2Disk(source, inputStream);
                        final Bitmap bitmap = ImgResizer.decodeFromStream(inputStream, reqWidth, reqHeight);
                        if (bitmap == null) {
                            return;
                        }
                        addImg2Memory(source, bitmap);
                        Looper looper = Looper.getMainLooper();
                        Handler handler = new Handler(looper);
                        handler.post(new Runnable() {
                            @Override
                            public void run() {
                                img.setImageBitmap(bitmap);
                            }
                        });
                    }
                }).start();
            }
        }

    }

至此,我們的圖片載入框架主體就編寫完成了,當然我們還需要看一個問題就是圖片的按需縮放,這邊就不過多說明了,提供一下原始碼吧。

package com.york.devbase.imgload;

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

import java.io.FileDescriptor;
import java.io.InputStream;

/**
 * Created by york_zhang on 2016/3/24.
 */
public class ImgResizer {
    /**
     * 計算bitmap的縮放比例
     *
     * @param options   bitmap的原始相關資訊
     * @param reqWidth  所需的圖片的寬
     * @param reqHeight 所需的圖片的高
     * @return bitmap的縮放比例
     */
    public static int calculateInSampleSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {
        if (reqWidth == 0 || reqHeight == 0) {
            return 1;
        }

        final int width = options.outWidth;
        final int height = options.outHeight;
        int inSampleSize = 1;
        if (height > reqHeight || width > reqWidth) {
            int halfWidth = width / 2;
            int halfHeight = height / 2;
            /**
             * 確保縮放比例能同時適用控制元件的寬高
             *
             */
            while ((halfWidth / inSampleSize) > reqWidth) {
                inSampleSize++;
            }

            while ((halfHeight / inSampleSize) > reqHeight) {
                inSampleSize++;
            }
        }
        return inSampleSize;
    }

    /**
     * 從檔案內讀取bitmap
     * ps:該處不呼叫BitmapFactory.decodeFile()原因在於,FileInputStream是一種有序的檔案流,
     * 兩次呼叫decode方法會影響檔案流的位置屬性,導致第二次呼叫的時候得到的bitmap為null
     *
     * @param fd        檔案
     * @param reqWidth  需要載入圖片的寬度
     * @param reqHeight 需要載入圖片的高度
     * @return 縮放後的bitmap
     */
    public static Bitmap decodeFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        /**
         * 設定inJustDecodeBounds為true,只讀取bitmap的相關引數,不會真正解析bitmap
         */
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        int inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inSampleSize = inSampleSize;
        /**
         * 設定inJustDecodeBounds為false,真正解析bitmap
         */
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeFileDescriptor(fd, null, options);
        return bitmap;
    }

    /**
     * 從流內解析並按需求壓縮bitmap
     *
     * @param inputStream 待解析的流
     * @param reqWidth    需要載入圖片的寬度
     * @param reqHeight   需要載入圖片的高度
     * @return 縮放後的bitmap
     */
    public static Bitmap decodeFromStream(InputStream inputStream, int reqWidth, int reqHeight) {
        BitmapFactory.Options options = new BitmapFactory.Options();
        /**
         * 設定inJustDecodeBounds為true,只讀取bitmap的相關引數,不會真正解析bitmap
         */
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(inputStream, null, options);
        int inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inSampleSize = inSampleSize;
        /**
         * 設定inJustDecodeBounds為false,真正解析bitmap
         */
        options.inJustDecodeBounds = false;
        Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null, options);
        return bitmap;
    }

}

下面我們看一下這個框架的使用(使用起來特別方便):

ImgLaodManager imgLaodManager = new ImgLaodManager();
imgLaodManager.init(new ImgLoaderConfig(),this);
imgLaodManager.bind("http://upload.news.cecb2b.com/2014/1209/1418100017289.jpg", mImageView,ScreenUtils.dip2px(100),ScreenUtils.dip2px(100));

到這裡我們整個圖片三級快取和載入框架就算完成了雛形,接下來我們抽時間把它做進一步完善,方便我們把它用到我們實際的專案開發中去。