Android 圖片加載框架Universal-Image-Loader源碼解析

本篇文章已授權(quán)微信公眾號 guolin_blog (郭霖)獨(dú)家發(fā)布,來源于我在CSDN發(fā)表的一篇博文:
http://blog.csdn.net/sinat_23092639/article/details/65936005

Universal-Image-Loader(項(xiàng)目地址)可以說是安卓知名圖片開源框架中最古老、使用率最高的一個(gè)了。一張圖片的加載對于安卓應(yīng)用的開發(fā)也許是件簡單的事,但是如果要同時(shí)加載大量的圖片,并且圖片用于ListView、GridView、ViewPager等控件,如何防止出現(xiàn)OOM、如何防止圖片錯(cuò)位(因?yàn)榱斜淼腣iew復(fù)用功能)、如何更快地加載、如何讓客戶端程序員用最簡單的操作完成本來十分復(fù)雜的圖片加載工作,成了全世界安卓應(yīng)用開發(fā)程序員心頭的一大難題,所幸有了Universal-Image-Loader,讓這一切變得簡單,從某種意義來講,它延長了安卓開發(fā)者的壽命~

針對上述幾個(gè)問題,Universal-Image-Loader可謂是兵來將擋水來土掩,見招拆招:

如何同時(shí)加載大量圖片:采用線程池優(yōu)化高
并發(fā)
如何提高加載速度:使用內(nèi)存、磁盤緩存
如何避免OOM:加載的圖片進(jìn)行壓縮,內(nèi)存緩存具有相關(guān)的淘汰算法
如何避免ListView的圖片錯(cuò)位:將要加載的圖片和要顯示的ImageView綁定, 在請求過程中判斷該ImageView當(dāng)前綁定的圖片是否是當(dāng)前請求的圖片,不是則停止當(dāng)前請求。

首先來看下Universal-Image-Loader整體流程圖:


圖片發(fā)自簡書App

總的來說就是:下載圖片——將圖片緩存在磁盤中——解碼圖片成為Bitmap——Bitmap的預(yù)處理——緩存在Bitmap內(nèi)存中——Bitmap的后期處理——顯示Bitmap

基本用法:
想必很多朋友也知道了,不過還是“抄襲”下Github說明里的使用方法:

**簡單版本:**

ImageLoader imageLoader = ImageLoader.getInstance(); // Get singleton instance

// Load image, decode it to Bitmap and display Bitmap in //ImageView (or any other view 
//  which implements ImageAware interface)

imageLoader.displayImage(imageUri, imageView);

// Load image, decode it to Bitmap and return Bitmap to callback

imageLoader.loadImage(imageUri, new SimpleImageLoadingListener() {
    @Override
    public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
        // Do whatever you want with Bitmap
    }
});

// Load image, decode it to Bitmap and return Bitmap //synchronously

Bitmap bmp = imageLoader.loadImageSync(imageUri);


**復(fù)雜版本**:

// Load image, decode it to Bitmap and display Bitmap in ImageView (or any other view 
//  which implements ImageAware interface)

imageLoader.displayImage(imageUri, imageView, options, new ImageLoadingListener() {
    @Override
    public void onLoadingStarted(String imageUri, View view) {
        ...
    }
    @Override
    public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
        ...
    }
    @Override
    public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
        ...
    }
    @Override
    public void onLoadingCancelled(String imageUri, View view) {
        ...
    }
}, new ImageLoadingProgressListener() {
    @Override
    public void onProgressUpdate(String imageUri, View view, int current, int total) {
        ...
    }
});

// Load image, decode it to Bitmap and return Bitmap to callback

ImageSize targetSize = new ImageSize(80, 50); 

// result Bitmap will be fit to this size

imageLoader.loadImage(imageUri, targetSize, options, new SimpleImageLoadingListener() {
    @Override
    public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
        // Do whatever you want with Bitmap
    }
});

// Load image, decode it to Bitmap and return Bitmap 
//synchronously

ImageSize targetSize = new ImageSize(80, 50); 

// result Bitmap will be fit to this size

Bitmap bmp = imageLoader.loadImageSync(imageUri, targetSize, options);

相信各位經(jīng)驗(yàn)豐富的安卓開發(fā)者一看就心照不宣,就不必解釋什么了。

和源碼見面之前,先和幾個(gè)重要的類打招呼:

ImageLoaderEngine:任務(wù)分發(fā)器,負(fù)責(zé)分發(fā)LoadAndDisplayImageTask和ProcessAndDisplayImageTask給具體的線程池去執(zhí)行。
ImageAware:顯示圖片的對象,可以是ImageView等。
ImageDownloader:圖片下載器,負(fù)責(zé)從圖片的各個(gè)來源獲取輸入流。
Cache:圖片緩存,分為MemoryCache和DiskCache兩部分。
MemoryCache:內(nèi)存圖片緩存,可向內(nèi)存緩存緩存圖片或從內(nèi)存緩存讀取圖片。
DiskCache:本地圖片緩存,可向本地磁盤緩存保存圖片或從本地磁盤讀取圖片。
ImageDecoder:圖片解碼器,負(fù)責(zé)將圖片輸入流InputStream轉(zhuǎn)換為Bitmap對象。
BitmapProcessor:圖片處理器,負(fù)責(zé)從緩存讀取或?qū)懭肭皩D片進(jìn)行處理。
BitmapDisplayer:將Bitmap對象顯示在相應(yīng)的控件ImageAware上。
LoadAndDisplayImageTask:用于加載并顯示圖片的任務(wù)。
ProcessAndDisplayImageTask:用于處理并顯示圖片的任務(wù)。
DisplayBitmapTask:用于顯示圖片的任務(wù)。

其中有個(gè)全局圖片加載配置類貫穿整個(gè)框架,ImageLoaderConfiguration,可以配置的東西實(shí)在有點(diǎn)多:

private ImageLoaderConfiguration(final Builder builder) {
        resources = builder.context.getResources();
        maxImageWidthForMemoryCache = builder.maxImageWidthForMemoryCache;
        maxImageHeightForMemoryCache = builder.maxImageHeightForMemoryCache;
        maxImageWidthForDiskCache = builder.maxImageWidthForDiskCache;
        maxImageHeightForDiskCache = builder.maxImageHeightForDiskCache;
        processorForDiskCache = builder.processorForDiskCache;
        taskExecutor = builder.taskExecutor;
        taskExecutorForCachedImages = builder.taskExecutorForCachedImages;
        threadPoolSize = builder.threadPoolSize;
        threadPriority = builder.threadPriority;
        tasksProcessingType = builder.tasksProcessingType;
        diskCache = builder.diskCache;
        memoryCache = builder.memoryCache;
        defaultDisplayImageOptions = builder.defaultDisplayImageOptions;
        downloader = builder.downloader;
        decoder = builder.decoder;

        customExecutor = builder.customExecutor;
        customExecutorForCachedImages = builder.customExecutorForCachedImages;

        networkDeniedDownloader = new NetworkDeniedImageDownloader(downloader);
        slowNetworkDownloader = new SlowNetworkImageDownloader(downloader);

        L.writeDebugLogs(builder.writeLogs);
    }

主要是圖片最大尺寸、線程池、緩存、下載器、解碼器等等。

經(jīng)過前面那么多的鋪墊,終于迎來了源碼~~

整個(gè)框架的源碼上萬,全部講完不可能,最好的方式還是按照加載流程走一遍,細(xì)枝末節(jié)各位可以自己慢慢研究,一旦整體把握好了,其他的一切就水到渠成,切勿只見樹木不見森林,迷失在各種代碼細(xì)節(jié)中~~

好了,簡單點(diǎn),講代碼的方式簡單點(diǎn),從最簡單的代碼切入:

imageLoader.displayImage(imageUri, imageView);

進(jìn)入方法:

public void displayImage(String uri, ImageView imageView) {
        displayImage(uri, new ImageViewAware(imageView), null, null, null);
    }

ImageAware是ImageView的包裝類,持有ImageView對象的弱引用,防止ImageView出現(xiàn)內(nèi)存泄漏發(fā)生。主要是提供了獲取ImageView寬度高度和ScaleType等。

最終會執(zhí)行這一個(gè)重載方法:

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        checkConfiguration();
        if (imageAware == null) {
            throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
        }
        //使用默認(rèn)的Listener
        if (listener == null) {
            listener = defaultListener;
        }
        //使用默認(rèn)的options
        if (options == null) {
            options = configuration.defaultDisplayImageOptions;
        }
        //uri為空
        if (TextUtils.isEmpty(uri)) {
            //則移除當(dāng)前請求
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingStarted(uri, imageAware.getWrappedView());
            if (options.shouldShowImageForEmptyUri()) {
                imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
            } else {
                imageAware.setImageDrawable(null);
            }
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
            return;
        }
        //使用默認(rèn)的圖片尺寸
        if (targetSize == null) {
            targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
        }
        //根據(jù)uri和圖片尺寸生成緩存的key
        String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
        engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

        listener.onLoadingStarted(uri, imageAware.getWrappedView());
        //從內(nèi)存緩存取出Bitmap
        Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
        //命中內(nèi)存緩存的相應(yīng)的Bitmap
        if (bmp != null && !bmp.isRecycled()) {
            L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
            //進(jìn)行Bitmap后期處理
            if (options.shouldPostProcess()) {
                //創(chuàng)建一個(gè)加載和顯示圖片任務(wù)需要的信息的對象
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                //將內(nèi)存緩存中取出的Bitmap顯示出來
                ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                        defineHandler(options));
                //是否同時(shí)加載,否則使用線程池加載
                if (options.isSyncLoading()) {
                    displayTask.run();
                } else {
                    engine.submit(displayTask);
                }
            } else {
                //不進(jìn)行圖片處理則直接顯示在ImageView
                options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
            }
        } else {
            //如果內(nèi)存緩存拿不到圖片
            if (options.shouldShowImageOnLoading()) {
                imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
            } else if (options.isResetViewBeforeLoading()) {
                imageAware.setImageDrawable(null);
            }

            ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                    options, listener, progressListener, engine.getLockForUri(uri));
            //創(chuàng)建加載顯示圖片任務(wù)
            LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                    defineHandler(options));
            //也是分同步和異步兩種
            if (options.isSyncLoading()) {
                displayTask.run();
            } else {
                engine.submit(displayTask);
            }
        }
    }

這個(gè)方法基本描繪出了整個(gè)圖片加載的流程,重要的地方已經(jīng)加上注釋。

第8行的listener就是上面基本使用說明復(fù)雜版本的進(jìn)度回調(diào)接口ImageLoadingListener,大家看下就知道,如果沒有配置的話設(shè)置為默認(rèn),而默認(rèn)其實(shí)啥都沒做,方法都是空實(shí)現(xiàn)。

第12行也是類似地如果沒有配置DisplayImageOptions,即圖片顯示的配置項(xiàng),則取默認(rèn)的。
看下這個(gè)圖片顯示配置類可以配置什么:

/** Sets all options equal to incoming options */
        public Builder cloneFrom(DisplayImageOptions options) {
            imageResOnLoading = options.imageResOnLoading;
            imageResForEmptyUri = options.imageResForEmptyUri;
            imageResOnFail = options.imageResOnFail;
            imageOnLoading = options.imageOnLoading;
            imageForEmptyUri = options.imageForEmptyUri;
            imageOnFail = options.imageOnFail;
            resetViewBeforeLoading = options.resetViewBeforeLoading;
            cacheInMemory = options.cacheInMemory;
            cacheOnDisk = options.cacheOnDisk;
            imageScaleType = options.imageScaleType;
            decodingOptions = options.decodingOptions;
            delayBeforeLoading = options.delayBeforeLoading;
            considerExifParams = options.considerExifParams;
            extraForDownloader = options.extraForDownloader;
            preProcessor = options.preProcessor;
            postProcessor = options.postProcessor;
            displayer = options.displayer;
            handler = options.handler;
            isSyncLoading = options.isSyncLoading;
            return this;
        }

配置是否內(nèi)存磁盤緩存以及設(shè)置圖片預(yù)處理和后期處理器(預(yù)處理和后期處理器默認(rèn)為null,留給客戶端程序員擴(kuò)展)等。

displayImage方法第27行里,如果沒有專門設(shè)置targetSize,即指定圖片的尺寸,就取ImageAware的寬高,即包裝在里面的ImageView的寬高,如果得到的寬高小于等于0,則設(shè)置為最大寬高,如果沒有設(shè)置內(nèi)存緩存的最大寬高(maxImageWidthForMemoryCache,maxImageHeightForMemoryCache),則為屏幕的寬高。

然后根據(jù)圖片uri和圖片的尺寸生成一個(gè)內(nèi)存緩存的key,之所以使用圖片尺寸是因?yàn)閮?nèi)存緩存的圖片同一張圖片擁有不同尺寸的版本。

displayImage方法第33行中:

engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

engine指的是任務(wù)分發(fā)器 ImageLoaderEngine,它持有一個(gè)HashMap,作為記錄圖片加載任務(wù)使用,一旦任務(wù)停止加載或者加載完畢則會刪除對應(yīng)的任務(wù)引用。prepareDisplayTaskFor方法正是將任務(wù)的引用添加到該HashMap中,這里以內(nèi)存緩存的Key為鍵。

接下來如果拿到的圖片需要做后期處理,則創(chuàng)建一個(gè)圖片顯示信息的對象,然后以之前創(chuàng)建的圖片顯示信息的對象為參數(shù)之一創(chuàng)建一個(gè)處理顯示任務(wù)對象ProcessAndDisplayImageTask(實(shí)現(xiàn)Runnable),如果是指定同步加載則直接調(diào)用它的run方法,不是則將其添加到任務(wù)分發(fā)器ImageLoaderEngine的線程池中異步執(zhí)行。

若不需要對圖片進(jìn)行后期處理則調(diào)用Displayer去顯示顯示圖片,默認(rèn)的Displayer為SimpleBitmapDisplayer,只是簡單地顯示圖片到指定的ImageView中。

如果拿不到內(nèi)存緩存的對應(yīng)圖片,則創(chuàng)建加載顯示圖片任務(wù)對象LoadAndDisplayImageTask,然后執(zhí)行該任務(wù)去磁盤緩存或者網(wǎng)絡(luò)加載圖片。

接下來的關(guān)鍵就是看下LoadAndDisplayImageTask的run方法怎么執(zhí)行的,就知道整個(gè)圖片加載怎么運(yùn)行的了。

public void run() {
        //當(dāng)前請求是否需要暫停等待,等待期間被中斷則停止請求
        if (waitIfPaused()) return;
        //如果當(dāng)前請求延時(shí)加載,延時(shí)期間被中斷則停止請求
        if (delayIfNeed()) return;
        //得到與圖片uri綁定的鎖
        ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
        L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
        if (loadFromUriLock.isLocked()) {
            L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
        }
        //鎖與圖片uri對應(yīng)是為了防止同時(shí)加載同一張圖片
        loadFromUriLock.lock();
        Bitmap bmp;
        try {
            //檢查當(dāng)前任務(wù)的對應(yīng)Imageview是否被回收或者復(fù)用,是則拋TaskCancelledException結(jié)束當(dāng)前任務(wù)
            checkTaskNotActual();
            //疑問:再一次嘗試從內(nèi)存緩存中獲取是因?yàn)榭赡艽藭r(shí)其他任務(wù)加載了相同的圖片?
            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {
                //內(nèi)存緩存獲取不到,嘗試到磁盤緩存或者網(wǎng)絡(luò)獲取
                bmp = tryLoadBitmap();
                if (bmp == null) return; // listener callback already was fired

                checkTaskNotActual();
                //檢查任務(wù)是否被Interrupt
                checkTaskInterrupted();
                //圖片是否需要預(yù)處理
                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);
                    //添加到內(nèi)存緩存
                    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);
                }
            }
            //再次判斷是否要停止當(dāng)前任務(wù)
            checkTaskNotActual();
            checkTaskInterrupted();
        } catch (TaskCancelledException e) {
            fireCancelEvent();
            return;
        } finally {
            loadFromUriLock.unlock();
        }

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

un方法不長,就直接展示了。
這里第3、5行:

if (waitIfPaused()) return;
if (delayIfNeed()) return;

第1行中的waitIfPaused方法:

private boolean waitIfPaused() {
        AtomicBoolean pause = engine.getPause();
        if (pause.get()) {
            synchronized (engine.getPauseLock()) {
                if (pause.get()) {
                    L.d(LOG_WAITING_FOR_RESUME, memoryCacheKey);
                    try {
                        engine.getPauseLock().wait();
                    } catch (InterruptedException e) {
                        L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
                        return true;
                    }
                    L.d(LOG_RESUME_AFTER_PAUSE, memoryCacheKey);
                }
            }
        }
        return isTaskNotActual();
    }

這里如果engine的pause標(biāo)志位被置位為true,則會調(diào)用engine.getPauseLock()對象(其實(shí)就是一個(gè)普通的Object對象)的wait方法使當(dāng)前線程暫停運(yùn)行。為什么要這樣呢?看下engine的pause標(biāo)志位什么時(shí)候會被置位為true,終于在PauseOnScrollListener的重寫方法onScrollStateChanged中找到:

public void onScrollStateChanged(AbsListView view, int scrollState) {
        switch (scrollState) {
            case OnScrollListener.SCROLL_STATE_IDLE:
                imageLoader.resume();
                break;
            case OnScrollListener.SCROLL_STATE_TOUCH_SCROLL:
                if (pauseOnScroll) {
                    imageLoader.pause();
                }
                break;
            case OnScrollListener.SCROLL_STATE_FLING:
                if (pauseOnFling) {
                    imageLoader.pause();
                }
                break;
        }

我們知道ListView滑動時(shí)候加載顯示圖片會使得滑動有卡頓現(xiàn)象。這就是在列表滑動時(shí)暫停加載的方式,只要給ListView設(shè)置:

ListView.setOnScrollListener(new PauseOnScrollListener(pauseOnScroll, pauseOnFling ))

就可以通過在滑動時(shí)候暫停請求任務(wù)從而防止ListView滑動帶來的卡頓。一旦ListView在滑動狀態(tài),所有新增的圖片請求都會暫停等待列表滑動停止調(diào)用imageLoader.resume()方法喚醒暫停的線程繼續(xù)加載請求。

waitIfPaused()在線程等待過程中被中斷(線程池被停止運(yùn)行)的時(shí)候會返回true,從而終止任務(wù)的run方法終止加載請求。它的返回值還由isTaskNotActual方法決定,該方法主要判斷顯示圖片的ImageView是否被回收以及所在的列表項(xiàng)是否被復(fù)用,是的話也是終止加載任務(wù)。

delayIfNeed方法則是在任務(wù)需要延遲的時(shí)候讓當(dāng)前線程sleep一會,同樣也是遇到中斷返回true終止任務(wù)。

接下來12行:

loadFromUriLock.lock();

這里的loadFromUriLock是一個(gè)ReentrantLock對象,是和要加載的圖片uri綁定一起的,所以相同uri的圖片具有同一把鎖,這也就意味著相同圖片的請求同一時(shí)間只有一個(gè)在網(wǎng)絡(luò)或者磁盤加載,有效避免了重復(fù)的網(wǎng)絡(luò)或者磁盤緩存加載,一旦加載完其余請求都會從內(nèi)存緩存中加載圖片。

然后嘗試從內(nèi)存緩存中取出圖片,一旦成功得到圖片,則經(jīng)過請求是否有效的相關(guān)判斷之后,創(chuàng)建一個(gè)DisplayBitmapTask將圖片顯示到對應(yīng)的ImageView中。

關(guān)鍵是如果內(nèi)存取不到圖片的情況,這時(shí)候就得看run中的22行:

bmp = tryLoadBitmap();

該方法就是去磁盤緩存取圖片,如果沒有則取網(wǎng)絡(luò)圖片。
方法也不長,所以也直接拷貝過來:

private Bitmap tryLoadBitmap() throws TaskCancelledException {
        Bitmap bitmap = null;
        try {
            File imageFile = configuration.diskCache.get(uri);
            //從文件系統(tǒng)中取到圖片
            if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
                L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
                loadedFrom = LoadedFrom.DISC_CACHE;
                //檢查當(dāng)前任務(wù)的對應(yīng)Imageview是否被回收或者復(fù)用,是則拋TaskCancelledException結(jié)束當(dāng)前任務(wù)
                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;
                //磁盤緩存獲取失敗則從網(wǎng)絡(luò)獲取
                if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                    imageFile = configuration.diskCache.get(uri);
                    if (imageFile != null) {
                        imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                    }
                }
                //仍然檢查當(dāng)前任務(wù)的對應(yīng)Imageview是否被回收或者復(fù)用,是則拋TaskCancelledException結(jié)束當(dāng)前任務(wù)
                checkTaskNotActual();
                //tryCacheImageOnDisk將圖片保存到文件系統(tǒng)。
                //接下來將圖片按需求裁剪等處理后加載到內(nèi)存
                //所以文件保存的是原圖或者縮略圖,內(nèi)存保存的是根據(jù)ImageView大小、scaletype、方向處理過得圖片
                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;
    }

首先第四行是嘗試從磁盤文件系統(tǒng)中尋找對應(yīng)的圖片,如果有的話,調(diào)用:

bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));

進(jìn)行解碼得到對應(yīng)的Bitmap對象,注意這里將文件路徑使用Scheme.FILE進(jìn)行包裝,由于Universal-Image-Loader加載圖片有多個(gè)來源,比如網(wǎng)絡(luò),文件系統(tǒng),項(xiàng)目文件夾asset等,所以解碼的時(shí)候進(jìn)行一個(gè)包裝,方便解碼器BaseImageDecoder在解碼的時(shí)候識別來源進(jìn)行相應(yīng)的獲取流處理。

Scheme總共有以下幾個(gè):

HTTP("http"), HTTPS("https"), FILE("file"), CONTENT("content"), ASSETS("assets"), DRAWABLE("drawable"), UNKNOWN

這里的decodeImage方法:

private Bitmap decodeImage(String imageUri) throws IOException {
        ViewScaleType viewScaleType = imageAware.getScaleType();
        ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
                getDownloader(), options);
        return decoder.decode(decodingInfo);
    }

根據(jù)ImageView的ScaleType以及targetSize等生成一個(gè)ImageDecodingInfo對象,傳入解碼器decoder的decode方法中。

這里的解碼器默認(rèn)為BaseImageDecoder,decode方法:

public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
        Bitmap decodedBitmap;
        ImageFileInfo imageInfo;
        //獲得圖片對應(yīng)的流
        InputStream imageStream = getImageStream(decodingInfo);
        if (imageStream == null) {
            L.e(ERROR_NO_IMAGE_STREAM, decodingInfo.getImageKey());
            return null;
        }
        try {
            //根據(jù)圖片的屬性生成一個(gè)ImageFileInfo對象,指定尺寸和旋轉(zhuǎn)處理
            imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
            // 還原stream
            // 為什么還原呢? 因?yàn)橥ㄟ^上面的操作,我們的stream游標(biāo)可能
            // 已經(jīng)不在首部,這時(shí)再去讀取會造成了信息不完整
            // 所以這里需要reset一下
            imageStream = resetStream(imageStream, decodingInfo);
            Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
            //解碼獲取Bitmap對象
            decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
        } finally {
            IoUtils.closeSilently(imageStream);
        }

        if (decodedBitmap == null) {
            L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
        } else {
            //對Bitmap進(jìn)行裁剪和旋轉(zhuǎn)等操作
            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                    imageInfo.exif.flipHorizontal);
        }
        return decodedBitmap;
    }

這里具體如何裁剪和旋轉(zhuǎn)的代碼就不介紹了,有興趣大家可以去源碼看下。最后得到的圖片就可以直接使用顯示圖片任務(wù)DisplayBitmapTask將圖片顯示在ImageView上了。

假如磁盤也獲取不到圖片,那就需要到網(wǎng)絡(luò)加載了。
tryLoadBitmap方法的20行:

if (options.isCacheOnDisk() && tryCacheImageOnDisk())

判斷是否需要磁盤緩存,需要的話,進(jìn)入tryCacheImageOnDisk方法:

private boolean tryCacheImageOnDisk() throws TaskCancelledException {
        L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);

        boolean loaded;
        try {
            //從網(wǎng)絡(luò)加載圖片并緩存到文件系統(tǒng)中
            loaded = downloadImage();
            if (loaded) {
                //將原圖轉(zhuǎn)化為縮略圖(疑問:不可以先判斷是否壓縮,再緩存圖片到文件系統(tǒng)?)
                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;
    }

主要就是通過downloadImage方法從網(wǎng)絡(luò)加載圖片,保存到文件系統(tǒng)中。然后判斷是否配置了maxImageWidthForDiskCache和maxImageHeightForDiskCache選項(xiàng),有的話對保存在文件系統(tǒng)的圖片進(jìn)行裁剪壓縮得到縮略圖。

然后在tryLoadBitmap方法第21行中,取出對應(yīng)磁盤緩存的圖片文件路徑,然后使用解碼器將圖片解碼文件為Bitmap。

以上討論的是需要磁盤緩存的情況,如果不需要呢?看下tryLoadBitmap方法第31行:

bitmap = decodeImage(imageUriForDecoding);

imageUriForDecoding是圖片的uri,該方法上面已經(jīng)講過,最終是通過流解碼為一個(gè)Bitmap對象,也就是從網(wǎng)絡(luò)加載圖片到內(nèi)存,而前面講的是從文件系統(tǒng)加載圖片到內(nèi)存。

這里需要注意的是,磁盤緩存的是原圖或者其縮略圖,內(nèi)存緩存的是根據(jù)ImageView裁剪的圖片。

拿到Bitmap對象,然后就是對圖片進(jìn)行預(yù)處理了(如果配置為需要的話),處理完畢后添加到內(nèi)存緩存,然后進(jìn)行后期處理(如果需要的話),最后將Bitmap交給顯示任務(wù)DisplayBitmapTask處理。

DisplayBitmapTask的run就十分簡單了:

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);
            displayer.display(bitmap, imageAware, loadedFrom);
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
        }
    }

在確保ImageView沒有被回收和復(fù)用的情況下,交給顯示器displayer處理,displayer默認(rèn)為SimpleBitmapDisplayer,SimpleBitmapDisplayer的display其實(shí)就是直接調(diào)用ImageView的setBitmapImage方法將圖片顯示上去。然后將該ImageView的圖片加載任務(wù)從任務(wù)分發(fā)器任務(wù)記錄中移除。

DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        runTask(displayBitmapTask, syncLoading, handler, engine);

這里注意下runTask的參數(shù)handler,它的來源在ImageLoader類的defineHandler方法:

private static Handler defineHandler(DisplayImageOptions options) {
        Handler handler = options.getHandler();
        if (options.isSyncLoading()) {
            handler = null;
        } else if (handler == null && Looper.myLooper() == Looper.getMainLooper()) {
            handler = new Handler();
        }
        return handler;
    }

可以看到首先取顯示配置中的Handler,默認(rèn)為null,所以默認(rèn)會執(zhí)行下面的判斷,如果調(diào)用該方法是在主線程,則創(chuàng)建一個(gè)Handler對象,于是改Handler就和主線程綁定一起了(不熟悉Hanlder的朋友可以看下我的另一篇博文:全面分析Handler消息機(jī)制
最后會利用Handler的post方法將圖片的顯示傳遞到主線程調(diào)用~~
圖片加載過程算是Over了~~

這里談下一些注意要點(diǎn):

1.關(guān)于線程池:

當(dāng)加載顯示任務(wù)要執(zhí)行的時(shí)候,任務(wù)分發(fā)器的submit對應(yīng)方法是:

void submit(final LoadAndDisplayImageTask task) {
        //任務(wù)分發(fā)的線程池
        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);
                }
            }
        });
    }

其中任務(wù)分發(fā)線程池taskDistributor為緩存線程池(CacheThreadPoll),用于判斷任務(wù)請求的圖片是否緩存在磁盤中以及分發(fā)任務(wù)給執(zhí)行任務(wù)線程池。兩個(gè)實(shí)際執(zhí)行任務(wù)的線程池taskExecutorForCachedImages和taskExecutor為可配置的線程池,默認(rèn)核心和工作線程數(shù)都為3,默認(rèn)配置采用的先進(jìn)先出的隊(duì)列,如果是列表圖片建議配置為先進(jìn)后出隊(duì)列。兩個(gè)線程池可以根據(jù)任務(wù)性質(zhì)自定義配置擴(kuò)展其他屬性。

為什么要專門分為三個(gè)線程池而不是一個(gè)線程池執(zhí)行所有任務(wù)呢?如果所有任務(wù)運(yùn)行在一個(gè)線程池中,所有的任務(wù)就都只能采取同一種任務(wù)優(yōu)先級和運(yùn)行策略。顯然果要有更好的性能,在線程數(shù)比較多并且線程承擔(dān)的任務(wù)不同的情況下,App中最好還是按任務(wù)的類別來劃分線程池。

一般來說,任務(wù)分為CPU密集型任務(wù),IO密集型任務(wù)和混合型任務(wù)。CPU密集型任務(wù)配置盡可能小的線程,如配置Ncpu+1個(gè)線程的線程池。IO密集型任務(wù)則由于線程并不是一直在執(zhí)行任務(wù),則配置盡可能多的線程,如2*Ncpu。具體可參見從源代碼分析Universal-Image-Loader中的線程池

2.關(guān)于下載器:在加載顯示任務(wù)類LoadAndDisplayImageTask的獲取下載器方法中:

private ImageDownloader getDownloader() {
        ImageDownloader d;
        if (engine.isNetworkDenied()) {
            d = networkDeniedDownloader;
        } else if (engine.isSlowNetwork()) {
            d = slowNetworkDownloader;
        } else {
            d = downloader;
        }
        return d;
    }

默認(rèn)網(wǎng)絡(luò)良好的情況下使用BaseImageDownloader。在無網(wǎng)絡(luò)情況下使用NetworkDeniedImageDownloader,當(dāng)判斷到圖片源為網(wǎng)絡(luò)時(shí)拋出異常停止請求。網(wǎng)絡(luò)不佳情況下使用SlowNetworkDownloader,主要通過重寫FilterInputStream的 skip(long n) 函數(shù)解決在慢網(wǎng)絡(luò)情況下 decode image 異常的 Bug。(具體解決原理本人也不太清楚,求教。。)

另外關(guān)于緩存機(jī)制具體可以看下從源代碼分析Android-Universal-Image-Loader的緩存處理機(jī)制

總結(jié)整個(gè)框架的特點(diǎn):1.支持內(nèi)存和磁盤兩級緩存。2.配置高靈活性,包括緩存算法、線程池屬性、顯示選項(xiàng)等等。3.支持同步異步加載4.運(yùn)用多種設(shè)計(jì)模式,比如模板方法、裝飾者、建造者模式等等,具有很高的可擴(kuò)展性可復(fù)用性。

好了,這個(gè)開源框架就講到這里,希望對大家有幫助,個(gè)人水平有限,閱讀開源框架源碼還不是很多,不足之處請幫忙糾正~~

參考文章:

Android 開源框架Universal-Image-Loader完全解析(三)—源代碼解讀
Android Universal Image Loader 源碼分析
從源代碼分析Universal-Image-Loader中的線程池

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

推薦閱讀更多精彩內(nèi)容