Universal-Image-Loader(1)

UniversalImageLoader

[TOC]

本章主要介紹UniversalImageLoader的主要實現原理,和使用方法

在介紹之前,理解《Android開發藝術探索》的第十二章Bitmap的加載和Cache是十分重要的,因為該章節介紹了ImageLoader的最核心的工作原理,這樣能夠為更好地理解UIL打下堅實的基礎。因此建議先去閱讀理解該章的內容。

由該章的內容中可以知道,ImageLoader最重要的幾點工作如下

  • 圖片的同步加載
  • 圖片的異步加載
  • 圖片壓縮
  • 內存緩存
  • 磁盤緩存
  • 網絡拉取

因此我們從這幾點出發,結合UniversalImageLoader關鍵部分的源碼來具體分析UIL的實現原理以及如果使用。

1.UIL中主要工作類

從上面ImageLoader的主要工作中可以得知大概需要以下幾個類:

  1. ImageLoader:外界使用的主要工作類
  2. 線程池:用于圖片的網絡加載
  3. Handler:用于圖片的異步傳送加載
  4. ImageView:用于加載圖片的控件
  5. ImageResizer:用于圖片的壓縮
  6. LruCache:用于圖片的內存緩存
  7. DiskLruCache:用于圖片的磁盤緩存

對應以上的工作,UIL中重要的幾個類有:

  1. ImageLoader:主要工作類
  2. ImageLoaderConfiguration:配置類,用于配置大部分工作的類成員,包括線程池成員,內存緩存成員,磁盤緩存成員,圖片下載器,圖片解碼器和顯示圖片參數等等。該類的作用就是參與ImageLoader的初始化并配置好正常工作的參數,因此該類很重要
  3. ImageLoaderEngine:用于線程池的管理,是一個管理類。該類與ImageLoaderConfiguration類的區別是,Configuration只是用于配置線程池,而真正使用線程池工作的是ImageLoaderEngine類
  4. ImageLoadingInfo:用于配置顯示圖片的信息類,包括圖片的url、圖片的加載大小、顯示圖片的控件和用于配置顯示圖片的配置類DisplayImageOptions等等
  5. DisplayImageOptions:顯示圖片參數的配置類,其中包含了Handler用于異步加載,BitmapDisplayer用于最終顯示圖片。該類和ImageLoadingInfo的關系是,DisplayImageOptions包含于ImageLoadingInfo之中,畢竟DisplayImageOptions也是跟圖片的顯示參數有關系的
  6. LoadAndDisplayImageTask:封裝了獲取圖片并顯示的任務,用于被線程池執行
  7. ImageDownloader:用于下載圖片,即獲取圖片的輸入流
  8. ImageDecoder:用于圖片解碼壓縮,可以理解成將輸入流轉換成Bitmap的過程,因此一般ImageDecoder和ImageDownloader是一起工作的,一個負責獲取圖片的輸入流,一個負責將輸入流轉換成Bitmap圖片對象,其中就包含了按需求壓縮圖片的功能
  9. MemoryCache:內存緩存類
  10. DiskCache:磁盤緩存類

UIL中的主要工作大體上是由前5個類完成的,其實也很好理解,ImageLoader是向外提供的主要接口,ImageLoaderConfiguration為ImageLoader正常工作而提供配置參數,ImageLoaderEngine是在ImageLoader中的主要工作成員,ImageLoadingInfo是為了配置最后顯示圖片的參數比如如何顯示、顯示在哪里之類的,畢竟ImageLoader顧名思義是跟圖片的顯示密不可分的,因此沒有DisplayImageOptions怎么讓圖片得到正確的顯示。


2.UIL的工作流程

在深入源碼分析UIL之前,我們先來了解UIL的工作流程,這樣帶著大局觀去看程序就不會陷入了源碼中不能自拔,導致只見樹木不見森林的情況。因為了解了整體的工作流程之后,在看源碼的時候就能迅速的找到重點流程而忽略細節的處理,有助于能夠迅速地大體將框架學習一遍。因此在學習任何源碼之前可以的話,最好先去搜索相關的工作原理,在掌握了一定原理之后再去看源碼則會達到事半功倍的效果。

UIL的工作流程其實跟《Android開發藝術探索》中ImageLoader的工作流程大致一樣,就是利用3級緩存策略
內存緩存->磁盤緩存->網絡拉取

接下來如果一開始就從頭到尾分析源碼的工作流程可能會導致思路不清晰,因為其中用到了許多類,如果在分析工作流程的過程中再去分析一個個類的作用會將思路中斷,不利于對整個框架的學習,因此下面會先對一個個在工作流程中會被用到的一些比較關鍵的類進行分析,最后再通過源碼將整個過程過一遍。


3.ImageLoaderConfiguration

ImageLoaderConfiguration作為一個配置類,對于ImageLoader正常工作來說必不可少,因為它主要是為了配置好使ImageLoader正常工作的各種類。
ImageLoaderConfiguration采用的是建造者Builder模式來創建對象,雖然它擁有很多類成員,但是有一些是在Configuration對象被創建的時候初始化好了的。
從下面的代碼可以看出,在UIL內部,最重要的無外乎線程池Executor、內存緩存MemoryCache、磁盤緩存DiskCache、圖片下載器ImageDownloader和圖片顯示配置參數DisplayImageOptions這幾個類等等。
其中一些默認的參數是,線程池的大小默認是3,任務隊列采用的是FIFO,有默認的圖片下載器BaseImageDownLoader,圖片解碼器BaseImageDecoder和默認顯示圖片配置DisplayImageOptions

// 下面這些Configuration類中所有的可選方法,可以看出有些屬性是默認的
File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
        .memoryCacheExtraOptions(480, 800) // default = device screen dimensions
        .diskCacheExtraOptions(480, 800, null)
        .taskExecutor(...)
        .taskExecutorForCachedImages(...)
        .threadPoolSize(3) // default
        .threadPriority(Thread.NORM_PRIORITY - 2) // default
        .tasksProcessingOrder(QueueProcessingType.FIFO) // default
        .denyCacheImageMultipleSizesInMemory()
        .memoryCache(new LruMemoryCache(2 * 1024 * 1024))
        .memoryCacheSize(2 * 1024 * 1024)
        .memoryCacheSizePercentage(13) // default
        .diskCache(new UnlimitedDiskCache(cacheDir)) // default
        .diskCacheSize(50 * 1024 * 1024)
        .diskCacheFileCount(100)
        .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default
        .imageDownloader(new BaseImageDownloader(context)) // default
        .imageDecoder(new BaseImageDecoder()) // default
        .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
        .writeDebugLogs()
        .build();


我們從源碼中看默認項是怎么實現的,在源碼中我們可以看到,在build()的時候會判斷一些屬性是否為null,如果為null則調用對應的創建方法構造一個默認對象。可以看出默認被構造的有taskExecutor、diskCache、memoryCache、downloader、decoder和defaultDisplayImageOptions等等,這些都是ImageLoader加載顯示一個圖片所需要的類。因此實際上不要手動配置Configuration,只需要一個build的configuration也能使ImageLoader正常工作。

/** Builds configured {@link ImageLoaderConfiguration} object */
public ImageLoaderConfiguration build() {
    initEmptyFieldsWithDefaultValues();
    return new ImageLoaderConfiguration(this);
}

private void initEmptyFieldsWithDefaultValues() {
    if (taskExecutor == null) {
        taskExecutor = DefaultConfigurationFactory
                .createExecutor(threadPoolSize, threadPriority, tasksProcessingType);
    } else {
        customExecutor = true;
    }
    if (taskExecutorForCachedImages == null) {
        taskExecutorForCachedImages = DefaultConfigurationFactory
                .createExecutor(threadPoolSize, threadPriority, tasksProcessingType);
    } else {
        customExecutorForCachedImages = true;
    }
    if (diskCache == null) {
        if (diskCacheFileNameGenerator == null) {
            diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();
        }
        diskCache = DefaultConfigurationFactory
                .createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);
    }
    if (memoryCache == null) {
        memoryCache = DefaultConfigurationFactory.createMemoryCache(context, memoryCacheSize);
    }
    if (denyCacheImageMultipleSizesInMemory) {
        memoryCache = new FuzzyKeyMemoryCache(memoryCache, MemoryCacheUtils.createFuzzyKeyComparator());
    }
    if (downloader == null) {
        downloader = DefaultConfigurationFactory.createImageDownloader(context);
    }
    if (decoder == null) {
        decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);
    }
    if (defaultDisplayImageOptions == null) {
        defaultDisplayImageOptions = DisplayImageOptions.createSimple();
    }
}

另:BitmapDisplayer 默認初始化在DisplayImageOptions中
Handler 默認初始化在ImageLoader的displayImage中,然后傳送到LoadAndDisplayImageTask當中
DisplayImageOptions 默認初始化在ImageLoader的loadImage中


4.ImageLoaderEngine

線程池的管理類,因為主要的任務都是通過線程池來完成的,因此可以認為ImageLoader里面干活的就是ImageLoaderEngine類里面的線程池。
下面主要介紹ImageLoaderEngine的構造方法和兩個主要工作方法

/**
 * 構造方法,可以看到利用的是Configuration類當中的參數來配置本身的線程池成員
 */
ImageLoaderEngine(ImageLoaderConfiguration configuration) {
    this.configuration = configuration;

    taskExecutor = configuration.taskExecutor;
    taskExecutorForCachedImages = configuration.taskExecutorForCachedImages;

    taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
}

/** 執行任務的方法,可以看到內部是利用線程池成員來執行的*/
void submit(final LoadAndDisplayImageTask task) {
    taskDistributor.execute(new Runnable() {
        @Override
        public void run() {
            File image = configuration.diskCache.get(task.getLoadingUri());
            boolean isImageCachedOnDisk = image != null && image.exists();
            initExecutorsIfNeed();
            if (isImageCachedOnDisk) {
                taskExecutorForCachedImages.execute(task);
            } else {
                taskExecutor.execute(task);
            }
        }
    });
}

/** Submits task to execution pool */
void submit(ProcessAndDisplayImageTask task) {
    initExecutorsIfNeed();
    taskExecutorForCachedImages.execute(task);
}


5.LoadAndDisplayImageTask & DisplayBitmapTask

UIL工作中最主要的任務類,其中LoadAndDisplayImageTask包含了DisplayBitmapTask,從名稱上就能看出關系,LoadAndDisplayImageTask是下載和顯示圖片,而DisplayBitmapTask只是顯示顯示圖片。
其實這個任務類也是UIL的核心工作類,有必要深刻地理解其工作流程。

下面是LoadAndDisplayImageTask的run方法,這里跟之前學過ImageLoader的同步加載圖片流程差不多,這里主要分為幾步:

  • 加上同步鎖:loadFromUriLock.lock();
  • 從內存緩存中獲取圖片:bmp = configuration.memoryCache.get(memoryCacheKey);
  • 從磁盤緩存、網絡中獲取圖片:bmp = tryLoadBitmap();
  • 利用DisplayBitmapTask異步顯示圖片:runTask(displayBitmapTask, syncLoading, handler, engine);注意該方法中有利用到Handler用于異步加載圖片,而該Handler是在ImageLoader的displayImage中被默認初始化的
final class LoadAndDisplayImageTask
{
    ...

    @Override
    public void run() {
        if (waitIfPaused()) return;
        if (delayIfNeed()) return;

        ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
        L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
        if (loadFromUriLock.isLocked()) {
            L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
        }

        //加鎖
        loadFromUriLock.lock();
        Bitmap bmp;
        try {
            checkTaskNotActual();

            //從內存緩存中獲取
            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {
                //如果內存中獲取不到則從磁盤、網絡中獲取
                bmp = tryLoadBitmap();
                if (bmp == null) return; // listener callback already was fired

                checkTaskNotActual();
                checkTaskInterrupted();

                if (options.shouldPreProcess()) {
                    L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
                    bmp = options.getPreProcessor().process(bmp);
                    if (bmp == null) {
                        L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
                    }
                }

                //在從磁盤或者網絡獲取到圖片之后將其加載到內存里面
                if (bmp != null && options.isCacheInMemory()) {
                    L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
                    configuration.memoryCache.put(memoryCacheKey, bmp);
                }
            } else {
                loadedFrom = LoadedFrom.MEMORY_CACHE;
                L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
            }

            if (bmp != null && options.shouldPostProcess()) {
                L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
                bmp = options.getPostProcessor().process(bmp);
                if (bmp == null) {
                    L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
                }
            }
            checkTaskNotActual();
            checkTaskInterrupted();
        } catch (TaskCancelledException e) {
            fireCancelEvent();
            return;
        } finally {
            loadFromUriLock.unlock();
        }

        //在獲取了圖片之后將其放進DisplayBitmapTask中顯示圖片
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        runTask(displayBitmapTask, syncLoading, handler, engine);
    }

    /**
     * 該方法的作用是先從磁盤緩存里獲取數據
     * 如果沒有則調用tryCacheImageOnDisk()從網絡獲取圖片并將其存到磁盤中
     *     然后再從磁盤中獲取圖片
     * 因此整個流程是:
     *     磁盤緩存獲取->網絡獲取并存到磁盤緩存并從磁盤緩存獲取
     */
    private Bitmap tryLoadBitmap() throws TaskCancelledException {
        Bitmap bitmap = null;
        try {
            //先從磁盤中獲取圖片
            File imageFile = configuration.diskCache.get(uri);
            if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
                L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
                loadedFrom = LoadedFrom.DISC_CACHE;

                checkTaskNotActual();
                bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
            }
            if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
                L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
                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() <= 0 || bitmap.getHeight() <= 0) {
                    fireFailEvent(FailType.DECODING_ERROR, null);
                }
            }
        } catch (IllegalStateException e) {
            fireFailEvent(FailType.NETWORK_DENIED, null);
        } catch (TaskCancelledException e) {
            throw e;
        } catch (IOException e) {
            L.e(e);
            fireFailEvent(FailType.IO_ERROR, e);
        } catch (OutOfMemoryError e) {
            L.e(e);
            fireFailEvent(FailType.OUT_OF_MEMORY, e);
        } catch (Throwable e) {
            L.e(e);
            fireFailEvent(FailType.UNKNOWN, e);
        }
        return bitmap;
    }

    /** 
     * 從網絡中獲取圖片存儲到磁盤緩存中,通過downloadImage()實現的
     *     而后的resizeAndSaveImage(width, height)是為了根據實際需求將圖片壓縮并重新存入磁盤緩存
     *     
     * 下載成功則返回true
     */
    private boolean tryCacheImageOnDisk() throws TaskCancelledException {
        L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);

        boolean loaded;
        try {
            loaded = downloadImage();
            if (loaded) {
                int width = configuration.maxImageWidthForDiskCache;
                int height = configuration.maxImageHeightForDiskCache;
                if (width > 0 || height > 0) {
                    L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey);
                    resizeAndSaveImage(width, height); // TODO : process boolean result
                }
            }
        } catch (IOException e) {
            L.e(e);
            loaded = false;
        }
        return loaded;
    }

    /**
     * 該方法主要分為兩步:
     * 1.從網絡獲取圖片輸入流:InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
     *     其中Downloader在Configuration被創建的時候被默認初始化成BaseImageDownloader
     * 2.通過輸入流將圖片存入磁盤緩存:configuration.diskCache.save(uri, is, this);
     */
    private boolean downloadImage() throws IOException {
        InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
        if (is == null) {
            L.e(ERROR_NO_IMAGE_STREAM, memoryCacheKey);
            return false;
        } else {
            try {
                return configuration.diskCache.save(uri, is, this);
            } finally {
                IoUtils.closeSilently(is);
            }
        }
    }

}

上面的任務是圖片的同步加載過程,這是通過線程池執行的,最后還要將圖片顯示出來,此時就需要轉到主線程設置圖片,也就是通過Handler來操作,而顯示圖片的過程是在DisplayBitmapTask中實現的,下面來看一下DisplayBitmapTask的運行過程。

final class DisplayBitmapTask
{
    /**
     * 在該方法中,主要的作用是在加載圖片的過程中觸發各個回調函數
     * 其中最主要的是通過displayer將圖片加載顯示到imageAware控件當中
     *
     * 注:該任務應該運行在主線程
     */
    @Override
    public void run() {
        if (imageAware.isCollected()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else if (isViewWasReused()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else {
            L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
            //將圖片bitmap加載到imageAware中用于顯示
            displayer.display(bitmap, imageAware, loadedFrom);
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
        }
    }

}

上面DisplayBitmapTask只是定義好了如何加載圖片到控件當中,真正被轉移到主線程中執行還是在LoadAndDisplayImageTask中的runTask方法中,可以看到,runTask方法中是通過參數中的handler將DisplayBitmapTask執行到主線程中的。

final class LoadAndDisplayImageTask
{
    ...
        @Override
    public void run() {

        ...

        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        //displayBitmapTask執行
        runTask(displayBitmapTask, syncLoading, handler, engine);
    }

    /**
     * 從該方法中可以發現,displayBitmapTask是通過handler的post達到運行在主線程中加載圖片的效果
     */
    static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {
            if (sync) {
                r.run();
            } else if (handler == null) {
                engine.fireCallback(r);
            } else {
                handler.post(r);
            }
        }
}

6.ImageLoadingInfo

ImageLoadingInfo信息類的作用類似于ImageLoaderConfiguration配置類,只是擔當了信息參數儲存的角色,實際并不做任何工作。
比如ImageLoaderConfiguration配置類是為ImageLoader正常工作流程提供參數,比如線程池、緩存和下載器等等。
而ImageLoadingInfo的作用則是為圖片的加載顯示提供參數,比如圖片的url、圖片的顯示控件、目標顯示大小和顯示配置類DisplayImageOptions等等,注意DisplayImageOptions的作用是提供在顯示圖片時所需的參數和一些回調接口,下面有詳細說明。 這里只需要記住ImageLoadingInfo的作用就是跟圖片加載顯示有關。


final class ImageLoadingInfo {

    final String uri;
    final String memoryCacheKey;
    final ImageAware imageAware;
    final ImageSize targetSize;
    final DisplayImageOptions options;
    final ImageLoadingListener listener;
    final ImageLoadingProgressListener progressListener;
    final ReentrantLock loadFromUriLock;

    public ImageLoadingInfo(String uri, ImageAware imageAware, ImageSize targetSize, String memoryCacheKey,
            DisplayImageOptions options, ImageLoadingListener listener,
            ImageLoadingProgressListener progressListener, ReentrantLock loadFromUriLock) {
        this.uri = uri;
        this.imageAware = imageAware;
        this.targetSize = targetSize;
        this.options = options;
        this.listener = listener;
        this.progressListener = progressListener;
        this.loadFromUriLock = loadFromUriLock;
        this.memoryCacheKey = memoryCacheKey;
    }
}


7.DisplayImageOptions

DisplayImageOptions跟圖片的最終顯示密切相關,而上面ImageLoadingInfo只是提供了一些參數,但并不直接跟圖片顯示有關系,DisplayImageOptions與圖片如何正確顯示才是密切相關的,因此配置好DisplayImageOptions是ImageLoader正確工作的重要一環,跟ImageLoaderConfiguration一樣對于ImageLoader來說是必不可少的。
可以說在ImageLoader中,ImageLoaderConfiguration決定了圖片如何加載,DisplayImageOptions決定了圖片如何正確顯示,每張圖片的加載都離不開DisplayImageOptions的配置參數
以下是跟ImageLoaderConfiguration一樣的所有可選的顯示圖片參數配置。其中較重要的參數有cacheInMemory,cacheOnDisk這兩個參數默認是false,即不使用緩存,displayer決定圖片顯示的樣式,這幾個一般在創建Options對象時需要重新配置,尤其是緩存。

注:如果DisplayImageOptions沒有手動地創建賦予ImageLoader,則在ImageLoaderConfiguration中會默認創建一個Options對象,下面程序中后面帶有default的就是默認配置的參數。

// DON'T COPY THIS CODE TO YOUR PROJECT! This is just example of ALL options using.
DisplayImageOptions options = new DisplayImageOptions.Builder()
        .showImageOnLoading(R.drawable.ic_stub) // resource or drawable
        .showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
        .showImageOnFail(R.drawable.ic_error) // resource or drawable
        .resetViewBeforeLoading(false)  // default
        .delayBeforeLoading(1000)
        .cacheInMemory(false) // default
        .cacheOnDisk(false) // default
        .preProcessor(...)
        .postProcessor(...)
        .extraForDownloader(...)
        .considerExifParams(false) // default
        .imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default
        .bitmapConfig(Bitmap.Config.ARGB_8888) // default
        .decodingOptions(...)
        .displayer(new SimpleBitmapDisplayer()) // default
        .handler(new Handler()) // default
        .build();


8.ImageDownloader

圖片下載器,即通過圖片的URI來獲取到圖片的InputStream。ImageDownloader是一個interface,主要方法就只有getStream(String imgUri, Object extra),即外界通過該方法便可通過imgUri獲取到圖片的InputStream。UIL中有實現了該接口的BaseImageDownloader,我們來看看它是怎么實現的。

關注重點:

  1. ImageDownloader的作用主要是向外提供一個下載的接口方法getStream(String imgUri, Object extra)
  2. 默認連接超時時間為5s,讀取超時時間為20s
  3. 可以從多個地方比如網絡、文件、Content、Assets等地方獲取圖片輸入流
  4. getStreamFromNetwork從網絡獲取圖片輸入流:
    • 采用HttpURLConnection作為連接類
    • 如果服務器返回3xx則進行重定向
    • 將InputStream封裝成ContentLengthInputStream返回
  5. getStreamFromFile從文件獲取圖片輸入流
  6. ...

總的來說,ImageDownLoader的作用就是從圖片的uri獲取到相應的InputStream

public class BaseImageDownloader implements ImageDownloader {
    //連接超時時間
    public static final int DEFAULT_HTTP_CONNECT_TIMEOUT = 5 * 1000; // milliseconds
    //讀取超時時間
    public static final int DEFAULT_HTTP_READ_TIMEOUT = 20 * 1000; // milliseconds

    //緩存大小
    protected static final int BUFFER_SIZE = 32 * 1024; // 32 Kb
    //允許使用的用于URI中的字符
    protected static final String ALLOWED_URI_CHARS = "@#&=*+-_.,:!?()/~'%";
    //最大重定向的次數,用于在服務器返回3xx時進行重定向
    protected static final int MAX_REDIRECT_COUNT = 5;

    ...

    /**
     * 構造方法,可以自定義連接超時和讀取超時時間
     *     默認分別是5s和20s
     */
    public BaseImageDownloader(Context context) {
        this(context, DEFAULT_HTTP_CONNECT_TIMEOUT, DEFAULT_HTTP_READ_TIMEOUT);
    }

    public BaseImageDownloader(Context context, int connectTimeout, int readTimeout) {
        this.context = context.getApplicationContext();
        this.connectTimeout = connectTimeout;
        this.readTimeout = readTimeout;
    }

    /**
     * 實現ImageLoader方法,也是最主要的方法
     *     可以看出可以分別從網絡、文件、Content、Assets等等地方獲取圖片輸入流
     *  其中extra是傳到DisplayImageOptions.Builder中的extraForDownloader,該參數可以為null
     */
    @Override
    public InputStream getStream(String imageUri, Object extra) throws IOException {
        switch (Scheme.ofUri(imageUri)) {
            case HTTP:
            case HTTPS:
                return getStreamFromNetwork(imageUri, extra);
            case FILE:
                return getStreamFromFile(imageUri, extra);
            case CONTENT:
                return getStreamFromContent(imageUri, extra);
            case ASSETS:
                return getStreamFromAssets(imageUri, extra);
            case DRAWABLE:
                return getStreamFromDrawable(imageUri, extra);
            case UNKNOWN:
            default:
                return getStreamFromOtherSource(imageUri, extra);
        }
    }

    /**
     * 從網絡的uri獲取到InputStream
     * 
     */
    protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
        HttpURLConnection conn = createConnection(imageUri, extra);

        //重定向
        int redirectCount = 0;
        while (conn.getResponseCode() / 100 == 3 && redirectCount < 
        MAX_REDIRECT_COUNT) {
            conn = createConnection(conn.getHeaderField("Location"), extra);
            redirectCount++;
        }

        //通過HttpURLConnection獲取InputStream
        InputStream imageStream;
        try {
            imageStream = conn.getInputStream();
        } catch (IOException e) {
            // Read all data to allow reuse connection (http://bit.ly/1ad35PY)
            IoUtils.readAndCloseStream(conn.getErrorStream());
            throw e;
        }

        if (!shouldBeProcessed(conn)) {
            IoUtils.closeSilently(imageStream);
            throw new IOException("Image request failed with response code " + conn.getResponseCode());
        }

        return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());
    }

    //創建HttpURLConnection連接
    protected HttpURLConnection createConnection(String url, Object extra) throws IOException {
        String encodedUrl = Uri.encode(url, ALLOWED_URI_CHARS);
        HttpURLConnection conn = (HttpURLConnection) new URL(encodedUrl).openConnection();
        conn.setConnectTimeout(connectTimeout);
        conn.setReadTimeout(readTimeout);
        return conn;
    }

    /**
     * 從文件獲取圖片輸入流
     */
    protected InputStream getStreamFromFile(String imageUri, Object extra) throws IOException {
        String filePath = Scheme.FILE.crop(imageUri);
        if (isVideoFileUri(imageUri)) {
            return getVideoThumbnailStream(filePath);
        } else {
            BufferedInputStream imageStream = new BufferedInputStream(new FileInputStream(filePath), BUFFER_SIZE);
            return new ContentLengthInputStream(imageStream, (int) new File(filePath).length());
        }
    }

    ...

}

9.ImageDecoder

ImageDecoder是用于圖片的解碼,說是解碼這么高大上,其實就是將ImageLoader獲取到的圖片InputStream轉換成Bitmap而已,概括來說,就是直接利用BitmapFactory.decodeStream()就能完成該功能。而ImageDecoder則可以理解成將圖片按照需求解碼,主要是按照需求計算采樣率、縮放和旋轉圖片。

主要關注重點:

  1. ImageDecoder只向外提供了一個解碼圖片的接口方法decode(ImageDecodingInfo decodingInfo)
  2. ImageDecodingInfo是一個存儲相關的解碼參數的信息類,跟ImageLoadingInfo一樣只是用于存儲配置參數,并不工作,只是在圖片解碼的過程中提供相應的參數,這些參數對解碼來說都是必不可少的
  3. 根據ImageDecodingInfo對象獲取到設置圖片的采樣率、縮放、旋轉等等屬性參數
  4. 通過decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);獲取到符合采樣率條件的Bitmap
  5. 通過decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation, imageInfo.exif.flipHorizontal);獲取到最終符合縮放和旋轉條件的Bitmap

總結一下ImageDecoder的作用就是將ImageLoader獲取到的圖片的輸入流InputStream轉換成滿足需求的Bitmap對象
ImageLoader+ImageDecoder便可以通過圖片的Url獲取到滿足需求的Bitmap對象

public class BaseImageDecoder implements ImageDecoder {

    ...

    /**
     * 將圖片從URI中解碼出來,其中圖片是符合需要的采樣率、縮放、旋轉等需求的
     */
    @Override
    public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
        Bitmap decodedBitmap;
        ImageFileInfo imageInfo;

        //獲取圖片輸入流
        InputStream imageStream = getImageStream(decodingInfo);
        if (imageStream == null) {
            L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
            return null;
        }
        try {
            //從ImageDecodingInfo對象當中提取解碼參數將其設置到Options當中
            imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
            imageStream = resetStream(imageStream, decodingInfo);
            Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
            //通過將Options獲取到符合條件的Bitmap
            decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
        } finally {
            IoUtils.closeSilently(imageStream);
        }

        if (decodedBitmap == null) {
            L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
        } else {
            //將Bitmap縮放和旋轉成滿足需求的Bitmap
            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                    imageInfo.exif.flipHorizontal);
        }
        return decodedBitmap;
    }


}

下面是ImageDecodingInfo信息類
可以看出其中跟圖片相關的屬性有ImagegeSize,ImageScaleType,Options等等,這些屬性決定了圖片被解碼出來的格式,比如大小,規模,采樣率等等。

public class ImageDecodingInfo {

    private final String imageKey;
    private final String imageUri;
    private final String originalImageUri;
    private final ImageSize targetSize;

    private final ImageScaleType imageScaleType;
    private final ViewScaleType viewScaleType;

    private final ImageDownloader downloader;
    private final Object extraForDownloader;

    private final boolean considerExifParams;
    private final Options decodingOptions;

    public ImageDecodingInfo(String imageKey, String imageUri, String originalImageUri, ImageSize targetSize, ViewScaleType viewScaleType,
                             ImageDownloader downloader, DisplayImageOptions displayOptions) {
        this.imageKey = imageKey;
        this.imageUri = imageUri;
        this.originalImageUri = originalImageUri;
        this.targetSize = targetSize;

        this.imageScaleType = displayOptions.getImageScaleType();
        this.viewScaleType = viewScaleType;

        this.downloader = downloader;
        this.extraForDownloader = displayOptions.getExtraForDownloader();

        considerExifParams = displayOptions.isConsiderExifParams();
        decodingOptions = new Options();
        copyOptions(displayOptions.getDecodingOptions(), decodingOptions);
    }
}


最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,406評論 6 538
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,034評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,413評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,449評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,165評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,559評論 1 325
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,606評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,781評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,327評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,084評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,278評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,849評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,495評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,927評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,172評論 1 291
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,010評論 3 396
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,241評論 2 375

推薦閱讀更多精彩內容