天天看點

圖檔加載架構Android-Universal-Image-Loader使用及解析

這是一個在github上很流行的異步圖檔加載架構,簡單友善實用,主要采用二級緩存來加載緩存圖檔(不僅僅是網上的,本地也可以),圖檔加載方式如下。

圖檔加載架構Android-Universal-Image-Loader使用及解析
圖檔加載架構Android-Universal-Image-Loader使用及解析

每次顯示圖檔時,先讀取緩存,當緩存沒有時,則讀取本地資料,隻有以上兩者都沒有的時候才從網絡擷取。

圖檔的二級緩存本質也是使用DiskLruCache進行讀取,這個東西可以看看郭大神的介紹http://blog.csdn.net/guolin_blog/article/details/28863651,下面來看一下如何使用它。

使用

隻需兩步,就能輕松實作圖檔的加載,

  • 在Application設定全局參數。
public class UILApplication extends Application {

    @Override
public void onCreate() {
    super.onCreate();

    initImageLoader(getApplicationContext());
 }

public static void initImageLoader(Context context) {

    ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
       config.threadPriority(Thread.NORM_PRIORITY - )//設定線程的優先權,0-10
      .denyCacheImageMultipleSizesInMemory()//設定之後緩存同一URI不同尺寸的圖檔時,隻緩存最後加載的那一張
      .diskCacheFileNameGenerator(new Md5FileNameGenerator())//圖檔的命名方式
      .diskCacheSize( *  * ) // 磁盤可以儲存多大的圖檔
      .tasksProcessingOrder(QueueProcessingType.LIFO)//設定圖檔處理的順序為後進先出,預設為FIFO
      .writeDebugLogs(); // 輸出log

      //設定完ImageLoaderConfiguration之後,調用Imageloader的init()方法初始化
    ImageLoader.getInstance().init(config.build());
   }
}
           

通過建造者模式設定ImageLoaderConfiguration的屬性,然後調用ImageLoader初始化。

  • 顯示圖檔

    接下來,隻需要在調用displayImage方法,來顯示圖檔

//listener 加載周期的監聽
    //progressListener 加載進度監聽
    ImageLoader.getInstance().displayImage(String uri, ImageView imageView, DisplayImageOptions options,ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
           

注意,上面有一個參數DisplayImageOptions ,顧名思義是顯示這個圖檔的參數設定,我們需要自定義它

DisplayImageOptions options = new DisplayImageOptions.Builder()
      .showImageOnLoading(R.drawable.ic_stub)//加載圖檔時要顯示的圖檔
      .showImageForEmptyUri(R.drawable.ic_empty)//空的url時顯示的圖檔
      .showImageOnFail(R.drawable.ic_error)//加載失敗時顯示的圖檔
      .cacheInMemory(true)//是否緩存
      .cacheOnDisk(true)//是否存到磁盤
      .considerExifParams(true)//是否考慮exif格式
      .displayer(new RoundedBitmapDisplayer())//圖檔展示的形狀
           

隻需要簡單的設定以上兩步,就能實作高效的圖檔加載了。點我,進入官方位址供下載下傳demo

源碼實作

整個使用過程中除了displayImage,其他的都是參數配置。現在來解析displayImage究竟怎麼實作的。

public synchronized void init(ImageLoaderConfiguration configuration) {
        if (configuration == null) {
            throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
        }
        if (this.configuration == null) {
            L.d(LOG_INIT_CONFIG);
            engine = new ImageLoaderEngine(configuration);
            this.configuration = configuration;
        } else {
            L.w(WARNING_RE_INIT_CONFIG);
        }
    }
           

在Application 的init初始化中.也就是指派了全局變量configuration ,和new了一個ImageLoaderEngine。

ImageLoaderEngine也就是一個線程池類,還記得在ImageLoaderConfiguration 中有taskExecutor(Executor executor) 和taskExecutorForCachedImages(Executor executorForCachedImages)方法嗎,該類就在這裡操作這兩個線程池,我們一般不定義那兩個方法,使用預設方法構造它們。

//一般使用這個方法,将imageView包裝成ImageViewAware
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options,
            ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        displayImage(uri, new ImageViewAware(imageView), options, listener, progressListener);
}
//最終實作的方法
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        //檢查ImageLoaderConfiguration 是否為空
        checkConfiguration();
        if (imageAware == null) {
            throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
        }
        if (listener == null) {
            listener = defaultListener;
        }
        if (options == null) {
            options = configuration.defaultDisplayImageOptions;
        }

        if (TextUtils.isEmpty(uri)) {
            //清除map中key為imageAware的hashcode。它和緩存圖檔的唯一值組成鍵值對
            engine.cancelDisplayTaskFor(imageAware);
            //回調
            listener.onLoadingStarted(uri, imageAware.getWrappedView());
            //shouldShowImageForEmptyUri在DisplayImageOptions 設定
            if (options.shouldShowImageForEmptyUri()) {
                imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
            } else {
                imageAware.setImageDrawable(null);
            }
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
            return;
        }
        //一般沒有設定目标圖檔大小,故設定為目前imageview的寬高
        if (targetSize == null) {
            targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
        }
        //根據url和目标圖檔的寬高組成唯一值
        String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
        //儲存到map
        engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
        //回調
        listener.onLoadingStarted(uri, imageAware.getWrappedView());
        //檢視緩存是否有目标圖檔。
        Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
        if (bmp != null && !bmp.isRecycled()) {
            L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
            //shouldPostProcess()由DisplayImageOptions 設定,自定義一個圖檔處理器,用來
            //下載下傳和解析圖檔。一般使用系統預設的,他會下載下傳并将圖檔截到<=imageview長寬。
            if (options.shouldPostProcess()) {
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                        defineHandler(options));                
                if (options.isSyncLoading()) {//同步加載,在主線程執行處理方法,預設不同步
                    displayTask.run();
                } else {//添加到線程池執行                  
                    engine.submit(displayTask);
                }
            } else {
                //一般沒自定義的情況都執行這裡,緩存有,是因為圖檔經過壓縮處理才加載到緩存的,是以不需
                //要解析。直接顯示
                //調用DisplayBitmapTask 的run方法顯示圖檔,
                //本質也是調用DisplayImageOptions 設定的display(BitmapDisplay display)顯示圖檔
                //架構為BitmapDisplay 接口實作了多個顯示形狀類
                //位于core/display包
                options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//回調
            }
        } else {//接下來是緩存沒有的情況下,
            if (options.shouldShowImageOnLoading()) {//DisplayImageOptions 設定,加載時顯示圖檔
                imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
            } else if (options.isResetViewBeforeLoading()) {//DisplayImageOptions設定,加載前置為空
                imageAware.setImageDrawable(null);
            }
            //調用LoadAndDisplayImageTask run()方法,執行圖檔加載和展示。
            ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                    options, listener, progressListener, engine.getLockForUri(uri));
            LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                    defineHandler(options));
            if (options.isSyncLoading()) {//同步加載,在主線程執行處理方法,預設不同步
                displayTask.run();
            } else {//添加到線程池執行  
                engine.submit(displayTask);
            }
        }
    }
           

由以上方法可知道,先讀取緩存,緩存有直接顯示,沒有的話到LoadAndDisplayImageTask 執行下一步。

接下來,就來看看LoadAndDisplayImageTask 的run方法,看看是怎麼執行的。

public void run() {
        //架構為每個url都配備了一個ReentrantLock ,防止同一時間加載同個url多次
        ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
        loadFromUriLock.lock();
        Bitmap bmp;
        try {
            checkTaskNotActual();//檢查imageview是否被回收

            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {
                //緩存不存在,加載圖檔
                bmp = tryLoadBitmap();
                if (bmp == null) return; // listener callback already was fired

                checkTaskNotActual();
                checkTaskInterrupted();
                //注意和shouldPostProcess不同,DisplayImageOptions設定
                if (options.shouldPreProcess()) {
                    bmp = options.getPreProcessor().process(bmp);
                }
                //是否要儲存到緩存。取決于DisplayImageOptions 設定,
                if (bmp != null && options.isCacheInMemory()) {
                    configuration.memoryCache.put(memoryCacheKey, bmp);
                }
            } else {//緩存有的話,修改個标示而已
                loadedFrom = LoadedFrom.MEMORY_CACHE;
            }
            //DisplayImageOptions設定
            if (bmp != null && options.shouldPostProcess()) {
                bmp = options.getPostProcessor().process(bmp);              
            }
        } catch (TaskCancelledException e) {
            fireCancelEvent();
            return;
        } finally {
            loadFromUriLock.unlock();
        }
        //設定要顯示的圖檔資訊
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        //調用DisplayBitmapTask 的run方法顯示圖檔,
        //本質也是調用DisplayImageOptions 設定的display(BitmapDisplay display)顯示圖檔
        //架構為BitmapDisplay 接口實作了多個顯示形狀類
        //位于core/display包
        runTask(displayBitmapTask, syncLoading, handler, engine);
    }
           

上面的實作大體上和display()方法差不多,也是拿到圖檔并且顯示,位于的不同是那圖檔的方法tryLoadBitmap(),看一下是怎麼實作的。

//記憶體沒有圖檔的時候才會調用該方法的。
private Bitmap tryLoadBitmap() throws TaskCancelledException {
        Bitmap bitmap = null;
        //是以一開始就是去磁盤拿
        File imageFile = configuration.diskCache.get(uri);
        if (imageFile != null && imageFile.exists() && imageFile.length() > ) {
            //草,終于發現,修改标示,給圖檔url加個file://字首,代表本地檔案
            //突然發現,拿到圖檔像素很大,加載的時候記憶體嘩啦啦的掉
            //不行,加載前得縮小它
            //decodeImage有加載圖檔和裁剪圖檔的功效
            loadedFrom = LoadedFrom.DISC_CACHE;
            bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
        }
        if (bitmap == null || bitmap.getWidth() <=  || bitmap.getHeight() <= ) {
            //草你大爺,還是沒有,上網擷取,改個标示
            loadedFrom = LoadedFrom.NETWORK;
            String imageUriForDecoding = uri;
            if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                imageFile = configuration.diskCache.get(uri);
                if (imageFile != null) {
                    imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                }
            }

            checkTaskNotActual();
            //解析圖檔
            bitmap = decodeImage(imageUriForDecoding);

            if (bitmap == null || bitmap.getWidth() <=  || bitmap.getHeight() <= ) {
                fireFailEvent(FailType.DECODING_ERROR, null);
            }
        }

        return bitmap;
    }
           

tryLoadBitmap也就是根據url擷取到bitmap的執行個體,想知道怎麼擷取的,就看看decodeImage是怎麼寫的,媽的跳來跳去煩死了

private Bitmap decodeImage(String imageUri) throws IOException {
        //擷取圖檔縮放方式,這裡他把所有縮放方法都歸為兩類CENTER_INSIDE和CROP
        //個人覺得是1等比例縮放到最長邊等于imageview的一邊
        //2等比例縮放到最短邊等于imageview的一邊,導緻會裁剪到最長邊的一部分
        ViewScaleType viewScaleType = imageAware.getScaleType();
        ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
                getDownloader(), options);
        //然後調用架構的解析類,
        //這裡隻有一個解析類位于包core/decode/BaseImageDecoder.java
        //ImageLoaderConfiguration會預設設定的
        return decoder.decode(decodingInfo);
    }
           

現在,是以的矛頭都指向了BaseImageDecoder(預設)類,草蛋的搞完就收工了。

public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
        Bitmap decodedBitmap;
        ImageFileInfo imageInfo;
        //根據url擷取輸入流,也就是調用core/download/BaseImageDownloader.java
        //這個下載下傳類的getStream方法來實作下載下傳,可以去該類看看
        InputStream imageStream = getImageStream(decodingInfo);
        if (imageStream == null) {
            return null;
        }
        try {
            //下面這段代碼想表達的意思,設定BitmapFactory.Option的inSampleSize
            //減少加載時記憶體的消耗,
            imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
            imageStream = resetStream(imageStream, decodingInfo);
            Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
            decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
        } finally {
            IoUtils.closeSilently(imageStream);
        }

        if (decodedBitmap == null) {
            L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
        } else {
            //在這裡設定圖檔的旋轉和縮放,inSampleSize不一定縮放到滿意的大小
            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                    imageInfo.exif.flipHorizontal);
        }
        return decodedBitmap;
    }
           

在這裡需要将一下prepareDecodingOptions方法

protected Options prepareDecodingOptions(ImageSize imageSize, ImageDecodingInfo decodingInfo) {
        //這個ImageScaleType 在DisplayImageOptions裡面設定。
        //預設是ImageScaleType.IN_SAMPLE_POWER_OF_2這個參數的含義是什麼,看下面
        ImageScaleType scaleType = decodingInfo.getImageScaleType();
        int scale;
        if (scaleType == ImageScaleType.NONE) {
            scale = ;
        } else if (scaleType == ImageScaleType.NONE_SAFE) {
            scale = ImageSizeUtils.computeMinImageSampleSize(imageSize);
        } else {
            ImageSize targetSize = decodingInfo.getTargetSize();
            //IN_SAMPLE_POWER_OF_2的話powerOf2 設為true
            boolean powerOf2 = scaleType == ImageScaleType.IN_SAMPLE_POWER_OF_2;
            //關于inSampleSize 的計算都在下面的方法中,咬咬牙進去看一下
            scale = ImageSizeUtils.computeImageSampleSize(imageSize, targetSize, decodingInfo.getViewScaleType(), powerOf2);
        }
        Options decodingOptions = decodingInfo.getDecodingOptions();
        decodingOptions.inSampleSize = scale;
        return decodingOptions;
    }
           

計算BitmapFactory.Option inSampleSize 的時候有原圖檔長寬已經目标圖檔長寬,目标圖檔時根封裝ImageView的ImageViewAmare計算出來的,通過這些資訊,計算出符合的圖檔長寬

public static int computeImageSampleSize(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
            boolean powerOf2Scale) {
        final int srcWidth = srcSize.getWidth();
        final int srcHeight = srcSize.getHeight();
        final int targetWidth = targetSize.getWidth();
        final int targetHeight = targetSize.getHeight();

        int scale = ;
        //這個viewScaleType是在ImageViewAware中定義,他将圖檔的scaletype隻是分成兩類
        switch (viewScaleType) {
            //第一類,等比例縮放圖檔長寬最大值小于等于imageview長寬,這樣圖檔會全部顯示,
            //imageview不一定被全部占用
            case FIT_INSIDE:
                //如果設定ImageScaleType.IN_SAMPLE_POWER_OF_2的話,inSampleSize 縮放值為2的平方數
                //以5*10的imageview顯示100*100的圖檔為例,如果設定了,scale 為16,沒有設定的話,scale 為20
                if (powerOf2Scale) {
                    final int halfWidth = srcWidth / ;
                    final int halfHeight = srcHeight / ;
                    while ((halfWidth / scale) > targetWidth || (halfHeight / scale) > targetHeight) { 
                        scale *= ;
                    }
                } else {
                    scale = Math.max(srcWidth / targetWidth, srcHeight / targetHeight); // max
                }
                break;
            //第二類,等比例縮放圖檔長寬最小值小于等于imageview長寬,這樣imageview會全部占用,
            //圖檔可能會被裁剪部分,預設使用這個參數
            case CROP:
                //以5*10的imageview顯示100*100的圖檔為例,如果設定了,scale 為8,沒有設定的話,scale 為10
                if (powerOf2Scale) {
                    final int halfWidth = srcWidth / ;
                    final int halfHeight = srcHeight / ;
                    while ((halfWidth / scale) > targetWidth && (halfHeight / scale) > targetHeight) { 
                        scale *= ;
                    }
                } else {
                    scale = Math.min(srcWidth / targetWidth, srcHeight / targetHeight); // min
                }
                break;
        }

        if (scale < ) {
            scale = ;
        }
        //根據最大允許長寬值在設定一下scale 
        scale = considerMaxTextureSize(srcWidth, srcHeight, scale, powerOf2Scale);

        return scale;
    }
           

通過計算得出inSampleSize ,就能大幅度減少圖檔加載消耗的記憶體。

注意有ViewScaleType 和 ImageScaleType ,ViewScaleType 是在DisplayImageOptions裡面設定。

用來決定decodingOptions.inSampleSize的計算方法,而ImageScaleType 是在ImageViewAware中定義,用來決定scaleType縮放類型,隻有等比例縮放和等比例裁剪.

圖檔的大緻加載流程就這樣結束了,當然還有很多小細節沒有提到,但我已疲軟,隻能如此了。

總結

  • 記憶體緩存

    雖然是自己寫的,和LruCache很像,也是使用LinkedHashMap來實作Lru緩存,系統預設使用,如想使用其他緩存方式,可在cache/memory/包檢視

  • 磁盤緩存

    也就是使用DiskLruCache,也是使用LinkedHashMap來實作Lru緩存,系統預設使用,如想使用其他緩存方式,可在cache/memory/包檢視

  • DisplayImageOption方法介紹

    showImageOnLoading(int/drawable) 加載的時候顯示的圖檔

    showImageForEmptyUri(int/drawable) url為空時顯示的圖檔

    showImageOnFail(int/drawable) 加載失敗顯示的圖檔

    resetViewBeforeLoading(boolean) 預設為false,當沒有設定showImageOnLoading的時候生效,加載圖檔中的時候imageview圖檔是否設為空

    cacheInMemory(boolean) 預設為false 是否緩存到記憶體

    cacheOnDisk(boolean cacheOnDisk) 預設為false 是否緩存到磁盤

    imageScaleType(ImageScaleType) 設定scaletype,與圖檔的inSampleSize由關系, 預設為 ImageScaleType.IN_SAMPLE_POWER_OF_2,将inSampleSiz設定為2的平方數,一般預設值

    bitmapConfig(Bitmap.Config) 圖檔config預設Bitmap.Config#ARGB_8888

    decodingOptions(Options ) 設定圖檔Option,一般使用imageScaleType配置來動态設定。

    delayBeforeLoading(int ) 加載延時

    extraForDownloader(Object ) 在ImageDownloader.getStream調用這個參數,但沒使用到它,不知何意

    considerExifParams(boolean ) 是否設定Exif格式,關于該格式可以百度之

    preProcessor(BitmapProcessor) 自定義個圖檔處理Process,在LoadAndDisplayImageTask 加載完圖檔之後(下載下傳+設定大小)會調用該方法,修改完之後放到記憶體中

    postProcessor(BitmapProcessor ) 同上,不同的是記憶體中已經存在,隻是拿出來臨時修改

    displayer(BitmapDisplayer ) 設定imageview顯示形狀 ,在core/display/包可看類型,預設正常形狀加載

    syncLoading(boolean ) 加載的時候是否同步,預設異步加載

    handler(Handler ) 自定義一個handle來顯示圖檔,預設不需要

  • ImageLoaderConfiguration介紹

    memoryCacheExtraOptions(int , int ) 設定記憶體緩存的最大長寬度,預設不處理

    diskCacheExtraOptions(int , int ,BitmapProcessor ) 設定磁盤緩存的最大長寬度,預設不處理

    taskExecutor(Executor ) 定義線程池存儲圖檔加載線程,預設不處理,取預設值

    taskExecutorForCachedImages(Executor ) 定義線程池存儲圖檔加載線程,預設不處理,取預設值

    threadPoolSize(int ) 線程池的線程數量,預設為DEFAULT_THREAD_POOL_SIZE = 3

    threadPriority(int ) 線程權限,預設為 DEFAULT_THREAD_PRIORITY = Thread.NORM_PRIORITY - 2

    denyCacheImageMultipleSizesInMemory()同一url圖檔有不同尺寸時,隻緩存最後一個,預設為全部緩存

    tasksProcessingOrder(QueueProcessingType )任務處理順序,有先進先出和後進先出

    memoryCacheSize(int ) 記憶體緩存大小。預設為目前app可用記憶體的1/8

    Builder memoryCacheSizePercentage(int )記憶體緩存大小占比,1-100

    memoryCache(MemoryCache )記憶體緩存模式,預設為LruCache

    diskCacheFileCount(int ) 磁盤緩存檔案數量上限

    diskCacheFileNameGenerator(FileNameGenerator )檔案名生成器,預設為hashcode 生成

    imageDownloader(ImageDownloader ) 圖檔下載下傳器,預設使用預設值BaseImageDownLoader

    imageDecoder(ImageDecoder )圖檔解析器,預設使用預設值BaseImageDecoder