天天看點

自定義圖檔加載器自定義圖檔加載器

自定義圖檔加載器

前言

圖檔加載器是一個非常常用的功能子產品,但是一般我們不會去從零開始自己寫一個,因為有Glide、Picasso、Fresco等一些優秀的開源庫,或者公司自己維護了一套。再者完整實作這一整套功能是很耗費精力的。而這篇文章僅僅是為了學習程式的設計模式和LruCache、Bitmap優化顯示等,去實作這樣的一個庫。

先說下面向對象六大原則及在此項目裡的展現:

1. 單一原則

簡單來說,一個類中,應該是一組相關性很高的函數、資料的封裝。具體根據類的職責。

2. 開閉原則

提取共同的函數為一個接口,通過設定接口對象或者實作該接口的對象,實作不同的緩存政策。

3. 裡氏替換原則

在這裡展現的就是隻要實作IImageCache接口的都可以設定到緩存去。

4. 依賴倒置原則

子產品間依賴通過抽象發生,實作類不直接發生依賴關系,其依賴是通過抽象類和接口。通過這樣減少耦合。

5. 接口隔離原則

fileOutputStream等其他流的關閉,這裡隻要是實作Closeable接口的都可以關閉close();隻要知道類實作了closeable,就可關閉,其他的一概不關心,這就是接口隔離。

6. 迪米特原則

ImageCache裡面使用了DiskLruCache,但是使用者不需要知道實作細節,隻需要和ImageCache打交道。即使裡面的DiskLruCache替換為其他的緩存實作,使用者也不會感覺到。

下面開始分析

ImageLoader:負責下載下傳圖檔和加載的類.

ImageCache:含有LruCache和DiskLruCache.

ImageResizer:擷取特定采樣的bitmap.

IImageCacahe:抽象ImageCache的接口,之前說的六大原則,很重要的一點是強調抽象.

ImageCache裡面使用了LruCache和DiskLruCache:

lruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap bitmap) {
                return bitmap.getRowBytes() * bitmap.getHeight() / ;
            }
        };       
        try {
            diskLruCache = DiskLruCache.open(diskCacheDir, , , DISK_CACHE_SIZE);
        } catch (IOException e) {
            e.printStackTrace();
        }
           

這裡緩存大小cacheSize設定為目前程序可用記憶體的0.25倍.sizeOf完成對Bitmap對象大小的計算。

lruCache緩存和讀取:

lruCache.put(key, bitmap), Bitmap bitmap = lruCache.get(key);

DiskLruCache緩存和讀取:

public void addToDiskCache(Bitmap bitmap, String key) throws IOException {
        DiskLruCache.Editor editor = diskLruCache.edit(key);
        if (editor != null) {
            OutputStream outputStream = editor.newOutputStream(DISK_CACHE_INDEX);
            if (addToDisk(bitmap, outputStream)) {
                editor.commit();
            } else {
                editor.abort();
            }
            diskLruCache.flush();
            outputStream.close();
        }
    }
           
try {
                DiskLruCache.Snapshot snapShot = diskLruCache.get(key);
                if (snapShot != null) {
                    FileInputStream fileInputStream = (FileInputStream) snapShot.getInputStream(DISK_CACHE_INDEX);
                    FileDescriptor fileDescriptor = fileInputStream.getFD();
                    bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(fileDescriptor, width, height);
                    if (bitmap != null)
                        addToCache(bitmap, url);
                    return bitmap;
                }
            } catch (IOException e) {
                e.printStackTrace();
                Log.e(TAG, "getDiskLruCache error");
            }
           

BitmapFactory.decodeStream(in),不能decode兩次。是以用ImageResizer.decodeSampledBitmapFromFileDescriptor根據ImageView大小擷取對應采樣率的Bitmap(因為ImageView小,而圖檔的分辨率大時,加載原圖是很浪費記憶體的)。主要是計算options.inSampleSize:

public Bitmap decodeSampledBitmapFromFileDescriptor(FileDescriptor fd, int reqWidth, int reqHeight) {
        final BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeFileDescriptor(fd, null, options);
        options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight);
        options.inJustDecodeBounds = false;
        return BitmapFactory.decodeFileDescriptor(fd, null, options);
    }
           

ImageLoader裡面先從ImageCache中讀取,若是沒有,者從網絡下載下傳,但是第一次下載下傳擷取的Bitmap不能直接設定到ImageView中去。

private IImageCacahe cache;
private Bitmap downloadBitmapFromUrl(String urlString, int width, int height) {
        Bitmap bitmap = null;
        HttpURLConnection urlConnection = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), IO_BUFFER_SIZE);
            bitmap = BitmapFactory.decodeStream(in);
            if (cache instanceof ImageCache)
                ((ImageCache) cache).addToDiskCache(bitmap, Utils.keyFormUrl(urlString));
            bitmap.recycle();
            bitmap = cache.getFromCache(urlString, width, height);// TODO: 2017/4/4  這裡因為第一次下載下傳的原圖需要重新采樣擷取需求大小的bitmap,而BitmapFactory.decodeStream(in),不能decode兩次。
        } catch (final IOException e) {
            Log.e(TAG, "Error in downloadBitmap: " + e);
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            Utils.close(in);
        }
        return bitmap;
    }
           

根據imageview的大小,通過BitmapFactory.Options options,,options.inSampleSize,計算需要的inSampleSize壓縮得到bitmap再加載到imageview上去。

若未采樣優化,加載一屏6張圖檔原圖,總20張,上下滑動時,記憶體占用情況如下:

自定義圖檔加載器自定義圖檔加載器

優化後記憶體占用情況如下 :這裡隻是指定了下需要的bitmap顯示的大小(機關為像素,不同的裝置上相同的dp顯示的像素不一緻,是以大小也會不同)。根據需要的大小及其FileDescriptor,從其DiskLruCache擷取bitmap,再顯示到ImageVIew中去。

自定義圖檔加載器自定義圖檔加載器

對比記憶體占用差別相當大,但是顯示的效果一緻并沒有打折扣。

還有需要解決的一個問題是ImageView的複用,例如在Listview中加載圖檔,如果需要加載的圖檔使用者已經劃過去了,那麼應該忽略這張圖檔,具體的就是在 target.setTag(TAG_KEY_URI, url),線程池的任務中 handler.obtainMessage(SUCCESS_COMPLETE, new LoaderResult(target, url, bitmap)).sendToTarget();然後在handler的handleMessage中

imageView.getTag(TAG_KEY_URI);對比url是否一緻,不一緻則忽略該圖檔。

最後順便接入了leakcanary,發現記憶體洩漏:華為mate8上Android6.0的HwPhoneWindow的mContext持有MainActivity對象,導緻洩漏;紅米note2上Android5.0:未發現有記憶體洩漏。這裡是華為的系統問題,不知有沒有大神能指點這該如何解決,感激不盡。

項目源碼位址:https://github.com/Ulez/UImageLoader

參考資料:

1.Android開發藝術探索

2.Android源碼設計模式解析與實戰

繼續閱讀