Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android高效ImageLoader的實現

Android高效ImageLoader的實現

編輯:關於Android編程

在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(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));

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

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved