ImageLoader源碼解析


title: imageLoader解析
date: 2017-09-02 19:00:47
categories: 源碼解讀
tags: 圖片加載


ImageLoader是最早開源的 Android 圖片緩存庫, 強(qiáng)大的緩存機(jī)制, 早期使用這個(gè)圖片加載框架的android應(yīng)用非常多牡彻, 至今仍然有不少Android 開發(fā)者在使用礁蔗。

ImagerLoader特征

  1. 支持本地、網(wǎng)絡(luò)圖片祭衩,且支持圖片下載的進(jìn)度監(jiān)聽
  2. 支持個(gè)性化配置ImagerLoader嚼摩,如線程池钦讳,內(nèi)存緩存策略,圖片顯示選項(xiàng)等
  3. 三層緩存加快圖片的加載速度
  4. 支持圖片壓縮

開始使用

鑒于這篇是對ImageLoader源碼來進(jìn)行解析枕面,我們首先回顧一下ImageLoader的使用愿卒。
可以通過這里下載universal-imager-loader的jar包,并將其導(dǎo)入到自己的項(xiàng)目中潮秘。
然后可以在Application或者Activity中初始化ImageLoade琼开,參考如下:

public class YourApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        //創(chuàng)建默認(rèn)的ImageLoader配置參數(shù)  
        ImageLoaderConfiguration configuration = ImageLoaderConfiguration  
                .createDefault(this);  
          
        //Initialize ImageLoader with configuration.  
        ImageLoader.getInstance().init(configuration);  
    }
}

當(dāng)然,如果涉及到網(wǎng)絡(luò)操作和磁盤緩存的話枕荞,有或者是在Application中進(jìn)行初始化的話柜候,記得要在Manifest中進(jìn)行申明:

<manifest>  
    <uses-permission android:name="android.permission.INTERNET" />  
    <!-- Include next permission if you want to allow UIL to cache images on SD card -->  
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />  
    ...  
    <application android:name="YourApplication">  
        ...  
    </application>  
</manifest>  

接下來我們就可以愉快的來加載圖片了,如下所示:

ImageLoader.getInstance().displayImage(imageUri, imageView);

當(dāng)然躏精,如果你想添加監(jiān)聽渣刷,可以這么寫:

ImageLoader.getInstance().loadImage(imageUrl, new SimpleImageLoadingListener(){  
  
            @Override  
            public void onLoadingComplete(String imageUri, View view,  
                    Bitmap loadedImage) {  
                super.onLoadingComplete(imageUri, view, loadedImage);  
                mImageView.setImageBitmap(loadedImage);  
            }  
              
        });  

至于更多的用法這里就不介紹了,如果有需要飞主,可以參看這篇博客碌识,了解更多關(guān)于ImageLoader的用法筏餐。下面就開始了源碼的解析之路开泽。

ImageLoaderConfiguration配置實(shí)現(xiàn)

我們首先還是從imageLoader的配置開始開始源碼的探究之旅把。在上面的使用實(shí)例中魁瞪,我們使用createDefault()方法來初始化配置穆律,那么imageLoader的默認(rèn)配置究竟是些什么呢?下面直接上代碼:

public static ImageLoaderConfiguration createDefault(Context context) {
    return new Builder(context).build();
}

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

private ImageLoaderConfiguration(final Builder builder) {
    resources = builder.context.getResources();//程序本地資源訪問器
    maxImageWidthForMemoryCache = builder.maxImageWidthForMemoryCache;//內(nèi)存緩存的圖片最大寬度 
    maxImageHeightForMemoryCache = builder.maxImageHeightForMemoryCache;//內(nèi)存緩存的圖片最大高度 
    maxImageWidthForDiskCache = builder.maxImageWidthForDiskCache;//磁盤緩存的圖片最大寬度 
    maxImageHeightForDiskCache = builder.maxImageHeightForDiskCache;//磁盤緩存的圖片最大高度 
    processorForDiskCache = builder.processorForDiskCache;//圖片處理器导俘,用于處理從磁盤緩存中讀取到的圖片 
    taskExecutor = builder.taskExecutor;//ImageLoaderEngine中用于執(zhí)行從源獲取圖片任務(wù)的 Executor峦耘。
    taskExecutorForCachedImages = builder.taskExecutorForCachedImages;//ImageLoaderEngine中用于執(zhí)行從緩存獲取圖片任務(wù)的 Executor。
    threadPoolSize = builder.threadPoolSize;//上面兩個(gè)默認(rèn)線程池的核心池大小旅薄,即最大并發(fā)數(shù)辅髓。
    threadPriority = builder.threadPriority;//上面兩個(gè)默認(rèn)線程池的線程優(yōu)先級(jí)。
    tasksProcessingType = builder.tasksProcessingType;//上面兩個(gè)默認(rèn)線程池的線程隊(duì)列類型少梁。目前只有 FIFO, LIFO 兩種可供選擇洛口。
    diskCache = builder.diskCache;//圖片磁盤緩存,一般放在 SD 卡凯沪。
    memoryCache = builder.memoryCache;//圖片內(nèi)存緩存第焰。
    defaultDisplayImageOptions = builder.defaultDisplayImageOptions;//圖片顯示的配置項(xiàng)挺举。比如加載前液荸、加載中、加載失敗應(yīng)該顯示的占位圖片文搂,圖片是否需要在磁盤緩存,是否需要在內(nèi)存緩存等常挚。
    downloader = builder.downloader;//圖片下載器贝或。
    decoder = builder.decoder;//圖片解碼器盗忱,內(nèi)部可使用我們常用的BitmapFactory.decode(…)將圖片資源解碼成Bitmap對象慷垮。

    customExecutor = builder.customExecutor;//用戶是否自定義了上面的 taskExecutor汤纸。
    customExecutorForCachedImages = builder.customExecutorForCachedImages;//用戶是否自定義了上面的 taskExecutorForCachedImages。

    networkDeniedDownloader = new NetworkDeniedImageDownloader(downloader);//不允許訪問網(wǎng)絡(luò)的圖片下載器。
    slowNetworkDownloader = new SlowNetworkImageDownloader(downloader);//慢網(wǎng)絡(luò)情況下的圖片下載器令蛉。

    L.writeDebugLogs(builder.writeLogs);
}

上面的代碼有點(diǎn)多,但是很簡單也很清晰,就是一些列初始化的代碼汇鞭。通過一些系列的調(diào)用台囱,在initEmptyFieldsWithDefaultValues方法中對一些沒有配置的進(jìn)行的項(xiàng)進(jìn)行配置,并通過ImageLoaderConfiguration給出默認(rèn)的參數(shù)配置。對于其中的一些配置择懂,在上面的注釋中已經(jīng)表明,ImageLoaderConfiguration中默認(rèn)的配置,可以參考第48-73行要糊。
至于initEmptyFieldsWithDefaultValues中的配置,在這里進(jìn)行簡單的介紹:

  • taskExecutor 從源獲取圖片任務(wù)的線程池
  • taskExecutorForCachedImages 用于執(zhí)行從緩存獲取圖片任務(wù)的線程池
    】】】】】】】
核心線程數(shù) 最大線程數(shù) 空閑線程等待時(shí)間 容器
3 3 0s 2

前面兩個(gè)線程池的參數(shù)如下:

核心線程數(shù) 最大線程數(shù) 空閑線程等待時(shí)間 容器
3 3 0s 2

前面兩個(gè)線程池如果用戶自定義的相應(yīng)的線程池來實(shí)現(xiàn)的話,就會(huì)將customExecutor置為true,或?qū)?code>customExecutorForCachedImages置為true苇经。其實(shí)customExecutor存在的意義就在于判斷用戶有沒有自定義從源獲取圖片任務(wù)的線程池,customExecutorForCachedImages存在的意義判斷在于用戶判斷用戶有沒有重寫從緩存獲取圖片的線程池中捆。

  • diskCacheFileNameGenerator 默認(rèn)實(shí)現(xiàn)為HashCodeFileNameGenerator扮碧,即用mageUri.hashCode()值當(dāng)前圖片名字。
  • diskCache用于表示圖片磁盤的緩存赖淤,默認(rèn)實(shí)現(xiàn)為createDiskCache绷耍,默認(rèn)的算法為LruDiskCache算法诸典,緩存的目錄為SD卡下的/data/data/" + context.getPackageName() + "/cache/uil-images目錄下益缠。
  • memoryCache用于表示圖片內(nèi)存的緩存宋欺,默認(rèn)實(shí)現(xiàn)為createMemoryCache,默認(rèn)使用的算法為LruMemoryCache骂租。
  • denyCacheImageMultipleSizesInMemorytrue時(shí)但汞,表示內(nèi)存緩存不允許緩存一張圖片的多個(gè)尺寸私蕾。這個(gè)時(shí)候用通過FuzzyKeyMemoryCache來構(gòu)建memoryCache
  • downloader表示圖片下載器,默認(rèn)實(shí)現(xiàn)為createImageDownloader自脯,最終通過BaseImageDownloader構(gòu)建下載器,其下載器中重要的兩個(gè)參數(shù)分別為:連接超時(shí)時(shí)間connectTimeout默認(rèn)值為5分鐘,讀取超時(shí)時(shí)間readTimeout默認(rèn)值為20分鐘龟糕。
  • decoder 表示圖片解碼器,默認(rèn)實(shí)現(xiàn)為createImageDecoder缓艳,最終通過BaseImageDecoder實(shí)現(xiàn)互妓。
  • defaultDisplayImageOptions 表示默認(rèn)參數(shù)澈蚌,最終回調(diào)到DisplayImageOptions方法中交胚,里面設(shè)計(jì)相關(guān)的參數(shù)初始化裸影。這里就不展開了荡澎。

加載配置

我們首先看ApplicationimgaerLoader設(shè)置配置的方法彤委。

 ImageLoader.getInstance().init(configuration);

接下來我們繼續(xù)分析上面的代碼是如何將配置應(yīng)用到ImageLoader中的。首先是ImageLoader.getInstance()實(shí)例化一個(gè)ImageLoader,通過代碼來看實(shí)例化的過程:

public static ImageLoader getInstance() {
    if (instance == null) {
        synchronized (ImageLoader.class) {
            if (instance == null) {
                instance = new ImageLoader();
            }
        }
    }
    return instance;
}

可以看出來彬呻,getInstance就是獲取一個(gè)ImageLoader實(shí)例,運(yùn)用了一個(gè)雙重鎖的單利模式蒲跨,很簡單藏姐,就不做解釋了。
重點(diǎn)看init方法理澎。具體在ImageLoader類中的實(shí)現(xiàn)如下:

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);
        }
    }

可以看出來,init的實(shí)現(xiàn)也是非常簡單的。首先判斷傳入的configuration參數(shù)是否為空峦嗤,為空就直接拋出一個(gè)異常装黑,不為空就判斷當(dāng)前類屬性configuration是否為空,類中configuration屬性為空時(shí)調(diào)用ImageLoaderEngine構(gòu)建engine對象箕别,否則就打印警告日志。所以整個(gè)方法中最重要的一個(gè)語句就是new ImageLoaderEngine(configuration);。這里首先介紹一個(gè)ImageLoaderEngine類的作用清寇。簡單描述就是ImageLoaderEngine是任務(wù)分發(fā)器盔夜,負(fù)責(zé)分發(fā)LoadAndDisplayImageTaskProcessAndDisplayImageTask給具體的線程池去執(zhí)行椭微。具體實(shí)現(xiàn)后面會(huì)講到。

加載圖片

通過上面兩個(gè)步驟,imgaeLoder的參數(shù)配置已經(jīng)設(shè)置完畢,接下來我們就可以用imageLoader加載圖片了。下面是三種加載圖片的方式:
加載方式一,異步加載并顯示圖片到對應(yīng)的imagerAware上

ImageLoader.getInstance().displayImage(imageUrl,imageView);

加載方式二,異步加載圖片并執(zhí)行回調(diào)接口

ImageLoader.getInstance().loadImage(imageUrl,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) {

    }
});

加載方式三,同步加載圖片

ImageLoader.getInstance().loadImageSync(imageUrl);

針對上面三種方法,我們先分析第一種加載圖片的方法,其余的兩種加載圖片的分析也差不多鳄炉,后面就不具體分析了杜耙,只是簡單的體現(xiàn)其不同點(diǎn)。
我們來看displayImage方法在ImageLoader類中的實(shí)現(xiàn)

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

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

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

public void displayImage(String uri, ImageView imageView, DisplayImageOptions options,
        ImageLoadingListener listener) {
    displayImage(uri, imageView, options, listener, null);
}

我們看到上面的displayImage有很多中重載的方法拂盯,最終他們都會(huì)調(diào)用到下面的這個(gè)displayImage方法中來泥技。

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
        ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    //省略了部分判空代碼
    
    ...
    if (targetSize == null) {
        targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
    }
    String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
    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);

        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 {
            options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
        }
    } else {
        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));
        LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                defineHandler(options));
        if (options.isSyncLoading()) {
            displayTask.run();
        } else {
            engine.submit(displayTask);
        }
    }
}

從第6行開始看店茶,當(dāng)沒有傳入targetSize目標(biāo)尺寸時(shí),會(huì)通過第6行的代碼產(chǎn)生一個(gè)合適的尺寸拢操。具體邏輯為栖茉,當(dāng)image沒有尺寸時(shí)就采用測量出來的最大尺寸苍鲜,當(dāng)image有尺寸時(shí)就用image本身的尺寸乏德。獲取最大尺寸的邏輯為:

ImageSize getMaxImageSize() {
        //獲取屏幕像素
        DisplayMetrics displayMetrics = resources.getDisplayMetrics();

        int width = maxImageWidthForMemoryCache;//最大的圖片內(nèi)存緩存寬度
        if (width <= 0) {
            width = displayMetrics.widthPixels;//屏幕寬度
        }
        int height = maxImageHeightForMemoryCache;//最大的圖片內(nèi)存緩存高度
        if (height <= 0) {
            height = displayMetrics.heightPixels;//屏幕高度
        }
        return new ImageSize(width, height);
    }

即最大的尺寸為:如設(shè)置了maxImageWidthForMemoryCache值且該值大于0,則最大尺寸為其設(shè)置的值,否則屏幕寬度悍引。在高度上也一樣,就不贅述了吗浩。綜上炕倘,我們可以知道要顯示圖片的大小的邏輯溯警,我們設(shè)置了圖片顯示的尺寸氧敢,則圖片尺寸為我們設(shè)置的尺寸。否則圖片的本身有尺寸的時(shí)候,顯示的就是自己本身的尺寸拉宗,否則就顯示最大的圖片尺寸。當(dāng)最大圖片內(nèi)存緩存尺寸大于0時(shí),最大圖片尺寸即為最大圖片內(nèi)存尺寸怠益,否則為屏幕尺寸谣沸。
分析了那么久祭犯,其實(shí)還只是分析了displayImage方法的一個(gè)方法锋叨,下面我們繼續(xù)看displayImage中的實(shí)現(xiàn)蛇更。在計(jì)算好目標(biāo)圖片的尺寸之后掌逛,利用generateKey方法生成一個(gè)memoryCacheKey妒穴,這里的memoryCacheKey的組成形式為URI + size南缓,用于表示要加載到內(nèi)存中的圖片荧呐。通過第10行代碼汉形,將要加載的圖片加入到cacheKeysForImageAwares隊(duì)列中,他是一個(gè)Collections.synchronizedMap(new HashMap<Integer, String>())類型的隊(duì)列,他用來記錄正在加載的任務(wù)倍阐,加載圖片的時(shí)候會(huì)將ImageViewid和圖片的url加上尺寸加入到HashMap中概疆,加載完成之后會(huì)將其移除。然后通過第12行的代碼回調(diào)onLoadingStarted方法峰搪,這個(gè)方法就是我們在使用時(shí)的onLoadingStarted方法回調(diào),具體參考上面的加載方式二岔冀,異步加載圖片并執(zhí)行回調(diào)接口的使用實(shí)例。
對于最終調(diào)用的displayImage方法代碼很重要概耻,所以我們繼續(xù)往下分析其中的代碼使套。以下的代碼已經(jīng)省略前面已經(jīng)分析的代碼罐呼,完整代碼參考前面的代碼。

Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
if (bmp != null && !bmp.isRecycled()) {//本地能獲取到圖片
    L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

    if (options.shouldPostProcess()) {
        ---
        //缺失的代碼片段1
        ---
    } else {
        options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
        listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
    }
} else {
    ---
    這里實(shí)現(xiàn)從網(wǎng)絡(luò)上獲取圖片的邏輯
     //缺失的代碼片段2
    ---
}

首先從內(nèi)存中拿出將要加載的圖片(bitmap格式)侦高,然后在圖片不為空且沒被回收的基礎(chǔ)上開始加載圖片的邏輯嫉柴。第5行中有一個(gè)判斷,我們?nèi)绻?code>DisplayImageOptions中設(shè)置了postProcessor就進(jìn)入true邏輯奉呛,不過默認(rèn)postProcessor是為null的差凹,BitmapProcessor接口主要是對Bitmap進(jìn)行處理,這個(gè)框架并沒有給出相對應(yīng)的實(shí)現(xiàn)侧馅,如果我們有自己的需求的時(shí)候可以自己實(shí)現(xiàn)BitmapProcessor接口(比如將圖片設(shè)置成圓形的)危尿。我們先分析默認(rèn)情況,即shouldPostProcessfalse的情況下執(zhí)行的第16-17行代碼馁痴。第16行代碼就將Bitmap設(shè)置到ImageView上面,這里我們可以在DisplayImageOptions中配置顯示需求displayer谊娇,默認(rèn)使用的是SimpleBitmapDisplayer,直接將Bitmap設(shè)置到ImageView上面罗晕。代碼如下:

public final class SimpleBitmapDisplayer implements BitmapDisplayer {
    @Override
    public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
        imageAware.setImageBitmap(bitmap);
    }
}

當(dāng)然济欢,ImageLoader也為我們提供了其他顯示的方式,如CircleBitmapDisplayer(),FadeInBitmapDisplayer,RoundeBitmapDisplayer三種顯示方式小渊。第17行代碼很好理解法褥,就是回調(diào)到onLoadingComplete方法,提供給用戶的回調(diào)酬屉。
接下來我們來看當(dāng)用于設(shè)置了postProcessor下情況的邏輯半等,即上面缺失的代碼片段1:

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);
            }
        } 

這里代碼執(zhí)行的情況就是用于需要回調(diào)圖片記載的進(jìn)度時(shí)執(zhí)行,即用戶指定了postProcessor對象呐萨,而postProcessor主要用于表示緩存在內(nèi)存之后的處理程序杀饵。
其中的ImageLoadingInfo主要用來加載和顯示圖片任務(wù)需要的信息,ProcessAndDisplayImageTask主要用于處理并顯示圖片的任務(wù)谬擦,他實(shí)現(xiàn)了Runnable接口切距。然后通過isSyncLoading判斷是同步還是異步,當(dāng)isSyncLoading為ture時(shí)表示當(dāng)前是同步執(zhí)行惨远。這里還有一個(gè)點(diǎn)需要特別說明以下:我們看第1行代碼中的engine.getLockForUri(uri)谜悟,這個(gè)方法主要是用來給圖片的URl加鎖的,那么給URL要傳入這個(gè)一個(gè)參數(shù)給ImageLoadingInfo呢北秽?其實(shí)主要是實(shí)現(xiàn)對圖片的復(fù)用葡幸,考慮這樣一種場景,在一個(gè)LitView中羡儿,某個(gè)Item正在獲取圖片的過程中礼患,我們將這個(gè)item滾出界面后又將其滾進(jìn)來是钥,滾進(jìn)來如果沒有加鎖掠归,該item又會(huì)去加載一次圖片缅叠,為了避免多次對同一個(gè)URL重復(fù)請求,有必要對正在加載的URL加鎖虏冻,當(dāng)圖片加載完成之后肤粱,就將鎖釋放掉。
我們在來分析同步執(zhí)行的情況厨相,直接執(zhí)行run(),通過displayTask任務(wù)來執(zhí)行领曼,我們來了解ProcessAndDisplayImageTaskrun()方法里面的實(shí)現(xiàn):

@Override
    public void run() {
        L.d(LOG_POSTPROCESS_IMAGE, imageLoadingInfo.memoryCacheKey);

        BitmapProcessor processor = imageLoadingInfo.options.getPostProcessor();
        //圖片處理器,用于處理從磁盤緩存中讀取到的圖片蛮穿。
        Bitmap processedBitmap = processor.process(bitmap);
        //處理圖片
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(processedBitmap, imageLoadingInfo, engine,
                LoadedFrom.MEMORY_CACHE);
                //構(gòu)建圖片實(shí)現(xiàn)的任務(wù)
        LoadAndDisplayImageTask.runTask(displayBitmapTask, imageLoadingInfo.options.isSyncLoading(), handler, engine);
                //執(zhí)行圖片顯示任務(wù)
    }

可以看到庶骄,在從本地讀取到圖片的顯示邏輯還是很簡單的,run方法核心只有四行代碼践磅,首先對圖片進(jìn)行相對應(yīng)的處理单刁,然后構(gòu)建圖片顯示的任務(wù),最后執(zhí)行圖片顯示的任務(wù)就OK了府适。我們來看DisplayBitmapTask中具體做了什么:

final class DisplayBitmapTask implements Runnable {

    ...
    private final Bitmap bitmap;
    private final String imageUri;
    private final ImageAware imageAware;
    private final String memoryCacheKey;
    private final BitmapDisplayer displayer;
    private final ImageLoadingListener listener;
    private final ImageLoaderEngine engine;
    private final LoadedFrom loadedFrom;

    public DisplayBitmapTask(Bitmap bitmap, ImageLoadingInfo imageLoadingInfo, ImageLoaderEngine engine,
            LoadedFrom loadedFrom) {
        this.bitmap = bitmap;
        imageUri = imageLoadingInfo.uri;
        imageAware = imageLoadingInfo.imageAware;
        memoryCacheKey = imageLoadingInfo.memoryCacheKey;
        displayer = imageLoadingInfo.options.getDisplayer();
        listener = imageLoadingInfo.listener;
        this.engine = engine;
        this.loadedFrom = loadedFrom;
    }

    @Override
    public void run() {
        if (imageAware.isCollected()) {//如果要顯示的圖片已經(jīng)被GC回收
            //回調(diào)onLoadingCancelled接口
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else if (isViewWasReused()) {//如果
             //回調(diào)onLoadingCancelled接口
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else {
             //正在顯示圖片的邏輯
            displayer.display(bitmap, imageAware, loadedFrom);
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
        }
    }

    /** Checks whether memory cache key (image URI) for current ImageAware is actual */
    //檢查內(nèi)存中當(dāng)前圖片的key是否是真實(shí)存在的
    private boolean isViewWasReused() {
        String currentCacheKey = engine.getLoadingUriForView(imageAware);
        return !memoryCacheKey.equals(currentCacheKey);
    }
}

上面run方法中的邏輯也比較清晰羔飞,首先對是否能進(jìn)行圖片顯示的環(huán)境做一定的判斷,在當(dāng)前環(huán)境可以顯示圖片的前提下檐春,利用BitmapDisplayer中的display方法顯示圖片逻淌,然后通過cancelDisplayTaskFor方法將當(dāng)前顯示的圖片從cacheKeysForImageAwares隊(duì)列中移除。這里的cacheKeysForImageAwares指的是ImageAware與內(nèi)存緩存key對應(yīng)的map疟暖,keyImageAwareid卡儒,value為內(nèi)存緩存的key。完成之后就回調(diào)onLoadingComplete方法俐巴。
但是注意到朋贬,在ProcessAndDisplayImageTask中,并沒有直接將displayBitmapTask通過start或者是run方法將其執(zhí)行窜骄,而是通過一個(gè)LoadAndDisplayImageTask中的runTask方法锦募,我們來看其實(shí)現(xiàn):

    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);
        }
    }

從實(shí)現(xiàn)上來說,還是比較簡單的邻遏。如果是同步加載的話糠亩,就直接調(diào)用run方法,否則(異步執(zhí)行)就調(diào)用handler調(diào)用post方法將其投遞到主線程中去執(zhí)行准验,這個(gè)handler的實(shí)現(xiàn)在ImageLoader中赎线。如果handler為空的話,就取消圖片顯示糊饱,直接處理善后工作垂寥。這個(gè)handlerImageLoader中的實(shí)現(xiàn)如下:

    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的創(chuàng)建也只會(huì)在異步加載的時(shí)候才會(huì)創(chuàng)建,同步情況下不會(huì)創(chuàng)建handler滞项。

從網(wǎng)絡(luò)上加載圖片

分析完本地加載圖片后狭归,我們來分析上面displayImage中缺失的代碼片段2,即本地?zé)o法獲取圖片的圖片加載邏輯文判,我們先來看其中的代碼:

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
    ...
        Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
        if (bmp != null && !bmp.isRecycled()) {
            ...
        } else {
            //這下面的代碼就是在本地?zé)o法獲取圖片的情況下加載圖片的邏輯
            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));
            LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                    defineHandler(options));
            if (options.isSyncLoading()) {
                displayTask.run();
            } else {
                engine.submit(displayTask);
            }
        }
    }

可以看到过椎,在無法獲取本地圖片情況下加載圖片的邏輯稍微比本地加載圖片的邏輯稍微多一點(diǎn),但實(shí)現(xiàn)上很多方法是相同的戏仓,我們一點(diǎn)一點(diǎn)來開始分析:

  1. 首先利用shouldShowImageOnLoading方法判斷在加載的過程中是否需要顯示圖片疚宇,當(dāng)用戶設(shè)置了imageResOnLoading占位圖片資源id,或者設(shè)置了加載中占位圖片drawable對象時(shí)其返回值為ture赏殃,即執(zhí)行在圖片加載過程中顯示占位圖片的邏輯敷待。
  2. 在用戶沒有設(shè)置占位圖片的情況下,會(huì)繼續(xù)判斷是否需要重設(shè)圖片仁热,若需要重設(shè)圖片讼撒,就將圖片設(shè)為null。
  3. 至于這里的ImageLoadingInfo(加載和顯示圖片任務(wù)需要的信息)和前面的實(shí)現(xiàn)一樣股耽,這里就不重復(fù)介紹了根盒。
  4. 這里的LoadAndDisplayImageTask,為下載和顯示圖片任務(wù)物蝙,用于從網(wǎng)絡(luò)炎滞、文件系統(tǒng)或者內(nèi)存獲取圖片并解析,然后調(diào)用DisplayBitmapTaskImageAware中顯示圖片诬乞。
  5. 在同步加載的情況下册赛,直接運(yùn)行displayTask
  6. 異步加載的情況下震嫉,將displayTask提交到taskDistributor線程池中運(yùn)行森瘪。
    接下來,我們就具體分析LoadAndDisplayImageTask中的run方法票堵。下面是LoadAndDisplayImageTask類中run的具體實(shí)現(xiàn):
@Override
public void run() {
    if (waitIfPaused()) return;
    if (delayIfNeed()) return;

    ---
    暫時(shí)神略
}

可以看到扼睬,LoadAndDisplayImageTask中的run()方法里面的邏輯還是稍微有點(diǎn)復(fù)雜的。我們一點(diǎn)一點(diǎn)來分析悴势;
首先看前面兩個(gè)方法的實(shí)現(xiàn)窗宇,即3-4行的代碼實(shí)現(xiàn),他們在LoadAndDisplayImageTask類中的實(shí)現(xiàn)代碼如下:

//主要用于判斷當(dāng)前線程是否被打斷特纤,被打斷返回ture军俊,否則返回isTaskNotActual()的返回值
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();
}

//主要用于判斷是否需要預(yù)處理,不要要返回false捧存,需要返回isTaskNotActual()的返回值
private boolean delayIfNeed() {
    if (options.shouldDelayBeforeLoading()) {
        L.d(LOG_DELAY_BEFORE_LOADING, options.getDelayBeforeLoading(), memoryCacheKey);
        try {
            Thread.sleep(options.getDelayBeforeLoading());
        } catch (InterruptedException e) {
            L.e(LOG_TASK_INTERRUPTED, memoryCacheKey);
            return true;
        }
        return isTaskNotActual();
    }
    return false;
}

//主要用于判斷imageVire是否被回收和重用粪躬,滿足其中一個(gè)條件返回ture担败,否則返回false
private boolean isTaskNotActual() {
    return isViewCollected() || isViewReused();
}

//主要用于判斷ImageView是否被GC回收了,回收了返回ture镰官,否者返回false
private boolean isViewCollected() {
    if (imageAware.isCollected()) {
        L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
        return true;
    }
    return false;
}

//主要用于判斷imageView是否被重用提前,被重用返回true,否則返回false
private boolean isViewReused() {
    String currentCacheKey = engine.getLoadingUriForView(imageAware);
    // Check whether memory cache key (image URI) for current ImageAware is actual.
    // If ImageAware is reused for another task then current task should be cancelled.
    boolean imageAwareWasReused = !memoryCacheKey.equals(currentCacheKey);
    if (imageAwareWasReused) {
        L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
        return true;
    }
    return false;
}

看起來好像也有點(diǎn)復(fù)雜朋魔,但是并不難岖研,我們直接一路看過去就好了卿操。每個(gè)方法的作用我都已經(jīng)在代碼中添加了注釋了警检,這里我們來整理一下思路:通過上面的源碼,我們基本上可以確定他們的作用是什么了害淤,但是為什么需要他們呢扇雕?我們試想這樣一種場景,在使用ListView來顯示圖片時(shí)窥摄,在手指滑動(dòng)的時(shí)候一般不會(huì)去加載圖片镶奉,因?yàn)樵谶@個(gè)過程中很多圖片是沒有必要加載的。這個(gè)時(shí)候我們就可以通過PauseOnScrollListener(ImageLoader imageLoader, boolean pauseOnScroll, boolean pauseOnFling)來控制在滑動(dòng)過程中圖片的加載崭放。第一個(gè)參數(shù)用來控制手指按著滑動(dòng)情況下的是否加載圖片哨苛,第二個(gè)參數(shù)用來控制手指松開后時(shí)候加載圖片。至于中間參數(shù)的參數(shù)和值的傳遞比較簡單币砂,這里就不全部給出來了建峭,可以自行通過查看源碼了解pauseOnScroll是如何改變waitIfPaused方法中pause的值的(默認(rèn)為false)。
至于isViewReused的方法存在的意義就更好理解了决摧,在ListView中存在一種復(fù)用的優(yōu)化策略亿蒸,即在ListView在滑動(dòng)時(shí),會(huì)復(fù)用Item掌桩,為了避免圖片顯示時(shí)的錯(cuò)位情況边锁,在ImageLoader就通過isViewReused來解決這個(gè)問題。

接下來我們繼續(xù)看LoadAndDisplayImageTask中的run()方法中剩下的代碼:

@Override
public void run() {
    ...
    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();//判斷當(dāng)前請求是否是可實(shí)現(xiàn)的波岛,(當(dāng)imageView被GC回收或者此次請求的URL無法獲取imageView時(shí)時(shí)為不可實(shí)現(xiàn)的請求)

        bmp = configuration.memoryCache.get(memoryCacheKey);//嘗試從內(nèi)存中加載圖片
        if (bmp == null || bmp.isRecycled()) {
            bmp = tryLoadBitmap();//嘗試從文件中加載圖片茅坛,如果沒有再去網(wǎng)絡(luò)中獲取,然后將bitmap保存在文件系統(tǒng)中则拷。
            //這個(gè)方法是重點(diǎn)灰蛙,后面會(huì)進(jìn)行講到
            if (bmp == null) return; // listener callback already was fired

            checkTaskNotActual();
            checkTaskInterrupted();//用于判斷當(dāng)前任務(wù)有沒有被打斷,被打斷直接拋出異常

            if (options.shouldPreProcess()) {//默認(rèn)為ture隔躲,表示緩存在內(nèi)存之前沒有要處理的程序
                L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
                bmp = options.getPreProcessor().process(bmp);//對bitmap進(jìn)行適當(dāng)?shù)募舨?                if (bmp == null) {
                    L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
                }
            }

            if (bmp != null && options.isCacheInMemory()) {//如果有必要緩存到內(nèi)存中的話
                L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
                configuration.memoryCache.put(memoryCacheKey, bmp);//將圖片保存到內(nèi)存緩存中去
            }
        } 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);//自定義的bitmap操作會(huì)在這里進(jìn)行
            if (bmp == null) {
                L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
            }
        }
        checkTaskNotActual();
        checkTaskInterrupted();
    } catch (TaskCancelledException e) {
        fireCancelEvent(); //解移除的監(jiān)聽 上面很多方法會(huì)拋出異常都需要這個(gè)方法來移除監(jiān)聽
        return;
    } finally {
        loadFromUriLock.unlock();//釋放鎖
    }

    DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom); //構(gòu)建顯示任務(wù)
    runTask(displayBitmapTask, syncLoading, handler, engine);//將圖片顯示到指定的imageView上
}

上面的代碼也有點(diǎn)多摩梧,但是在添加了先關(guān)的注解之后,詳細(xì)閱讀起來還是比較簡單的宣旱。這里在梳理一下整個(gè)LoadAndDisplayImageTask中的run方法的相關(guān)邏輯仅父。首先會(huì)判斷當(dāng)前是否是可以加載圖片的狀態(tài),不可以加載圖片的話就直接返回,什么都不做笙纤。在可以加載圖片的前提下耗溜,會(huì)給以下的核心邏輯代碼添加一個(gè)鎖:【首先嘗試從內(nèi)存中獲取圖片,沒有對應(yīng)的圖片就會(huì)從磁盤中尋找省容,如果磁盤中也找不到抖拴,那么就只能從網(wǎng)絡(luò)中去需找,在找到圖片后將其存在文件系統(tǒng)中腥椒,如果用戶定義了圖片的預(yù)處理阿宅,就會(huì)執(zhí)行用戶定義的圖片預(yù)處理,如果需要緩存到內(nèi)存就會(huì)緩存到內(nèi)存中笼蛛,繼而執(zhí)行用戶定義的圖片后處理(提前是用戶定義了圖片后處理)洒放,最后判斷一下當(dāng)前狀態(tài)是否還可以顯示圖片,若當(dāng)前狀態(tài)不能顯示圖片就會(huì)直接拋出異常滨砍,在catch語句中移除相關(guān)的監(jiān)聽往湿。如果當(dāng)前狀態(tài)還可以顯示圖片,在finally語句中釋放鎖 】以此保障多線程的可靠性惋戏,然后執(zhí)行圖片顯示任務(wù)將圖片顯示到圖片上领追,到此完成了整個(gè)圖片的加載。用流程圖表示如下:

圖片加載流程

下面我們來分析上面run()方法中最重要的一個(gè)方法tryLoadBitmap()响逢,他的實(shí)現(xiàn)也在LoadAndDisplayImageTask類中绒窑,實(shí)現(xiàn)代碼如下:

private Bitmap tryLoadBitmap() throws TaskCancelledException {
    Bitmap bitmap = null;
    try {
        File imageFile = configuration.diskCache.get(uri);//先判斷文件中有沒有該文件
        if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {//如果文件中有該文件,就直接調(diào)用decodeImage去解碼圖片
            L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
            loadedFrom = LoadedFrom.DISC_CACHE;

            checkTaskNotActual();//判斷當(dāng)前是否具有加載圖片的狀態(tài)龄句,這個(gè)方法在前面已經(jīng)解析過了
            bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));//解碼圖片
        }
        if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
        //表示文件中沒有找到圖片回论,就會(huì)指定到網(wǎng)絡(luò)上獲取bitmap,
            L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
            loadedFrom = LoadedFrom.NETWORK;

            String imageUriForDecoding = uri;
            if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
            //options.isCacheOnDisk()用來表是否需要將圖片緩存到文件系統(tǒng)中分歇,默認(rèn)為fasle傀蓉。
                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;
}

上面的代碼雖然看起來有點(diǎn)多,但是邏輯還是很清晰的职抡,我在關(guān)鍵的地方都添加了注釋葬燎,相信閱讀起來很簡單。這里再次梳理一下tryLoadBitmap的邏輯吧缚甩。首先從嘗試從文件中去獲取圖片谱净,如果能從文件中獲取圖片的話,就判斷當(dāng)前狀態(tài)是否可以加載圖片擅威,然后通過decodeImage方法將圖片解碼成可以顯示的格式壕探。如果文件中沒有要顯示的圖片,在設(shè)置了從網(wǎng)絡(luò)獲取圖片的前提下就會(huì)利用tryCacheImageOnDisk方法從網(wǎng)絡(luò)上獲取圖片郊丛,然后將圖片解碼成要顯示的格式李请,可以參考下面的流程圖:

tryLoadBitmap流程圖

在上面的流程中瞧筛,我們對其中兩個(gè)重要的方法來進(jìn)一步的探究其實(shí)現(xiàn),一個(gè)方法是decodeImage导盅,另一個(gè)是tryCacheImageOnDisk()较幌。著兩個(gè)方法的實(shí)現(xiàn)源碼如下,他們都在LoadAndDisplayImageTask類中白翻。

//解碼圖片
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);
}

/** @return <b>true</b> - if image was downloaded successfully; <b>false</b> - otherwise */
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
                //解碼成bitmap圖片乍炉,并保存他。關(guān)于這個(gè)方法就不在深入了滤馍。
            }
        }
    } catch (IOException e) {
        L.e(e);
        loaded = false;
    }
    return loaded;
}

//負(fù)責(zé)下載圖片岛琼,并將其保存到文件緩存中
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);
        }
    }
}

這里就不對上面的代碼進(jìn)行解釋了,我們直接看decode方法在BaseImageDecoder中的具體實(shí)現(xiàn)纪蜒,至于其他的方法衷恭,請參考注釋此叠。

@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 {
        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 {
        decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                imageInfo.exif.flipHorizontal);
    }
    return decodedBitmap;
}

綜上纯续,我們暫時(shí)分析完了run()方法中邏輯和主要方法。接下來我們繼續(xù)分析異步的情況灭袁,這里再次貼出之前displayImage的主要流程代碼猬错。因?yàn)橹暗拇a隔得有點(diǎn)遠(yuǎn)了。

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
      ---
Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
    if (bmp != null && !bmp.isRecycled()) {
        if (options.shouldPostProcess()) {
           ...
           if (options.isSyncLoading()) {
                displayTask.run();
            } else {
                engine.submit(displayTask);
            }
        }else {
            options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
        }
    }else{
       ...
       缺失的代碼片段2
    }
}

我們之前已經(jīng)分析了displayTask.run();的主要流程茸歧,接下來我們分析異步的執(zhí)行engine.submit(displayTask);的主要流程和方法倦炒,他的實(shí)現(xiàn)主要在ImageLoaderEngine中,submit的實(shí)現(xiàn)如下:

void submit(ProcessAndDisplayImageTask task) {
    initExecutorsIfNeed();
    taskExecutorForCachedImages.execute(task);
}

方法很簡單软瞎,首先只有兩個(gè)方法調(diào)用逢唤,第一行代碼從名字分析就應(yīng)該是用來初始化Executor的(有必要的話),然后執(zhí)行將此次任務(wù)提交到線程池中運(yùn)行涤浇。在線程池中的執(zhí)行也會(huì)執(zhí)行調(diào)用之前的run方法鳖藕,這里就不再分析了。我們分析一下第一行代碼只锭,驗(yàn)證一下我們的猜想是不是正確的著恩。

private void initExecutorsIfNeed() {
    if (!configuration.customExecutor && ((ExecutorService) taskExecutor).isShutdown()) {
        taskExecutor = createTaskExecutor();
    }
    if (!configuration.customExecutorForCachedImages && ((ExecutorService) taskExecutorForCachedImages)
            .isShutdown()) {
        taskExecutorForCachedImages = createTaskExecutor();
    }
}

從上面代碼分析:首先判斷當(dāng)前的taskExecutor是不是關(guān)閉了,如果處于關(guān)閉狀態(tài)就創(chuàng)建一個(gè)新的Executor蜻展,這里的taskExecutor指的是用與執(zhí)行從源獲取圖片任務(wù)的線程池喉誊。然后判斷taskExecutorForCachedImages是不是就緒,如果他被關(guān)閉的話就創(chuàng)建一個(gè)新的線程池taskExecutorForCachedImages纵顾,用于執(zhí)行從緩存獲取圖片任務(wù)的線程池伍茄。綜上,源碼驗(yàn)證了我們之前的猜測施逾,initExecutorsIfNeed方法的確是用來初始化相關(guān)線程池的敷矫。

displayImage方法總結(jié)

從上面的流程中贞盯,可以明顯看出來,displayImage方法就是imageLoader加載圖片的核心沪饺,我們在這里在來總結(jié)一下整個(gè)displayImage的邏輯躏敢,先將整個(gè)displayImage代碼完整的貼上來:

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);
    }
    if (listener == null) {
        listener = defaultListener;
    }
    if (options == null) {
        options = configuration.defaultDisplayImageOptions;
    }

    if (TextUtils.isEmpty(uri)) {
        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;
    }

    if (targetSize == null) {
        targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
    }
    String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
    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);

        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 {
            options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
        }
    } else {
        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));
        LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                defineHandler(options));
        if (options.isSyncLoading()) {
            displayTask.run();
        } else {
            engine.submit(displayTask);
        }
    }
}

將上面代碼用流程圖可以表示為以下的表現(xiàn)形式。


displayImage
displayImage

<p align="center">(原創(chuàng)圖整葡,敬請批評(píng)指正)</p>

可以看到件余,在上面流程圖算比較復(fù)雜,但是邏輯很清晰遭居,基本上所有的功能集中在displayImage中進(jìn)行調(diào)度使用啼器,所以給我們分析ImageLoader降低了不少的難度。
至于上面流程圖中沒有具體體現(xiàn)的任務(wù)可以參考前面的分析俱萍。

針對上面三種顯示圖片的方法端壳,最終都會(huì)通過調(diào)用displayImage來實(shí)現(xiàn),只是對其中的參數(shù)進(jìn)行了一定的設(shè)置枪蘑,這里就不在詳細(xì)介紹了损谦,有興趣的可以自己查閱源碼。

LRUCache和DisLruCacher分析

LRUCache

LruCache是android 3.1所提供的一個(gè)緩存類岳颇,他是一個(gè)泛型類照捡,他內(nèi)部采用一個(gè)LinkedHashMap以強(qiáng)引用的方式存儲(chǔ)外界的緩存對象,其提供了getput方法來完成緩存的獲取和添加屬性话侧,當(dāng)緩存滿時(shí)栗精,LruCache會(huì)移除較早使用的緩存對象,然后在添加新的緩存對象瞻鹏。
構(gòu)造方法如下:

 public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }

可以看到悲立,LruCache的構(gòu)造方法非常簡單,只需要傳入一個(gè)maxSize設(shè)置最大的緩存對象即可新博,然后實(shí)例化map對象薪夕。
這里也附上get和put的源碼:

public final V get(K key) {
    if (key == null) {
        throw new NullPointerException("key == null");
    }

    V mapValue;
    synchronized (this) {
        mapValue = map.get(key);
        if (mapValue != null) {
            hitCount++;
            return mapValue;
        }
        missCount++;
    }
    
  V createdValue = create(key);
    if (createdValue == null) {
        return null;
    }

    synchronized (this) {
        createCount++;
        mapValue = map.put(key, createdValue);

        if (mapValue != null) {
            // There was a conflict so undo that last put
            map.put(key, mapValue);
        } else {
            size += safeSizeOf(key, createdValue);
        }
    }

    if (mapValue != null) {
        entryRemoved(false, key, createdValue, mapValue);
        return mapValue;
    } else {
        trimToSize(maxSize);
        return createdValue;
    }
}
    

public final V put(K key, V value) {
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

    V previous;
    synchronized (this) {
        putCount++;
        size += safeSizeOf(key, value);
        previous = map.put(key, value);
        if (previous != null) {
            size -= safeSizeOf(key, previous);
        }
    }

    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }

    trimToSize(maxSize);
    return previous;
}

更多的分析可以參考這里

DisLruCache

DisLruCache用于實(shí)現(xiàn)存儲(chǔ)設(shè)置緩存叭披,即磁盤緩存寥殖,他通過將緩存對象寫入文件系統(tǒng)從而實(shí)現(xiàn)緩存的效果。
我們在這里對其創(chuàng)建涩蜘,緩存添加和移除緩存進(jìn)行簡單的分析册烈。
創(chuàng)建過程:

  private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
   ...簡單的賦值克饶,就不貼出來了
  }

其中的四個(gè)參數(shù)分別是:

  • directory表示磁盤存在文件系統(tǒng)中的存儲(chǔ)路徑肺素;
  • appVersion 表示應(yīng)用的版本號(hào)壮锻,一般設(shè)置1就可;
  • valueCount 表示單個(gè)節(jié)點(diǎn)鎖對應(yīng)的數(shù)據(jù)的個(gè)數(shù)误窖,一般設(shè)為1就可以了叮盘;
  • maxSize 表示緩存的總大小秩贰,比如50MB,當(dāng)緩存大小超過這個(gè)設(shè)置值后柔吼,DisLruCache會(huì)清除一些緩存從而保證總大小不大于這個(gè)設(shè)定值毒费。
    當(dāng)然,DiskLruCache提供了open方法來創(chuàng)建自身:
  public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
      throws IOException {
    if (maxSize <= 0) {
      throw new IllegalArgumentException("maxSize <= 0");
    }
    if (valueCount <= 0) {
      throw new IllegalArgumentException("valueCount <= 0");
    }

    // If a bkp file exists, use it instead.
    File backupFile = new File(directory, JOURNAL_FILE_BACKUP);
    if (backupFile.exists()) {
      File journalFile = new File(directory, JOURNAL_FILE);
      // If journal file also exists just delete backup file.
      if (journalFile.exists()) {
        backupFile.delete();
      } else {
        renameTo(backupFile, journalFile, false);
      }
    }

DisLruCacher的緩存添加:
DisLruCache的緩存操作通過Editor完成的愈魏,Editor表示一個(gè)緩存對象的編輯對象觅玻。在ImageLoader的運(yùn)用,首先需要獲取圖片的URL所對用的Key培漏,然后根據(jù)Key就可以通過edit()方法來獲取Editor對象溪厘,如果這個(gè)緩存正在被編輯,那么edit會(huì)返回null牌柄,即DisLrucache不允許同時(shí)編輯一個(gè)緩存對象畸悬。之所以要把url轉(zhuǎn)換成key,是因?yàn)閡rl中可能有特殊字符珊佣,這將影響url在Adnroid中的直接使用蹋宦,一般采用url的md5值作為key。

DisLruCacher的緩存查找:
緩存查找過程也需要將url轉(zhuǎn)換成key彩扔,然后通過DisLrache的get方法得到一個(gè)snapshot對象即可得到緩存的文件輸入流妆档,進(jìn)而得到Bitmap對象僻爽。為了避免加載圖片過程中導(dǎo)致的OOM問題虫碉,一般建議不直接加載原始圖片,建議先對圖片進(jìn)行壓縮之后在去加載胸梆。下面是get方法的實(shí)現(xiàn)邏輯:

 public synchronized Value get(String key) throws IOException {
    checkNotClosed();
    Entry entry = lruEntries.get(key);
    if (entry == null) {
      return null;
    }

    if (!entry.readable) {
      return null;
    }

    for (File file : entry.cleanFiles) {
        // A file must have been deleted manually!
        if (!file.exists()) {
            return null;
        }
    }

    redundantOpCount++;
    journalWriter.append(READ);
    journalWriter.append(' ');
    journalWriter.append(key);
    journalWriter.append('\n');
    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return new Value(key, entry.sequenceNumber, entry.cleanFiles, entry.lengths);
  }

DisLruCacher的緩存刪除:
DisLruCacher提供了remove,delete方法來進(jìn)行磁盤的刪除操作敦捧。刪除通過需要將url轉(zhuǎn)換成key,然后從lruEntriesLinkedHashMap對象中獲取該對象碰镜,在對象存在的前提下兢卵,刪除文件中對應(yīng)的文件,然后移除lruEntries對應(yīng)的key值绪颖。代碼實(shí)現(xiàn)如下:

 public synchronized boolean remove(String key) throws IOException {
    checkNotClosed();
    Entry entry = lruEntries.get(key);
    if (entry == null || entry.currentEditor != null) {
      return false;
    }

    for (int i = 0; i < valueCount; i++) {
      File file = entry.getCleanFile(i);
      if (file.exists() && !file.delete()) {
        throw new IOException("failed to delete " + file);
      }
      size -= entry.lengths[i];
      entry.lengths[i] = 0;
    }

    redundantOpCount++;
    journalWriter.append(REMOVE);
    journalWriter.append(' ');
    journalWriter.append(key);
    journalWriter.append('\n');

    lruEntries.remove(key);

    if (journalRebuildRequired()) {
      executorService.submit(cleanupCallable);
    }

    return true;
  }

雜談

下面是我對imageLoader源碼分析之后的一些感悟和一些總結(jié)秽荤,有一些還是面試時(shí)被問到的問題,這里一并記錄下來柠横。

ImageLoader運(yùn)用的設(shè)計(jì)模式

從源碼分析上來看窃款,最明顯的就是建造者模式和單例模式,這兩種模式在實(shí)際項(xiàng)目中也是運(yùn)行最廣的設(shè)計(jì)模式牍氛。還使用了工廠模式晨继,裝飾者模式,代理模式搬俊,策略模式等等紊扬。設(shè)計(jì)模式參考

當(dāng)ListView顯示圖片蜒茄,滾動(dòng)時(shí)ImageLoader是如何避免OOM的?

首先是對緩存進(jìn)行管理餐屎,具體管理內(nèi)存的方法是LruCache檀葛,實(shí)現(xiàn)算法是LRU:通過優(yōu)先淘汰最近最少使用的緩存對象,保證總緩存大小不高于限定值腹缩。

LRUCacher算法的具體實(shí)現(xiàn)

他內(nèi)部采用一個(gè)LinkedhashMap以強(qiáng)引用的方式存儲(chǔ)外界的緩存對象驻谆,其提供了get和put方法來完成緩存的獲取和添加操作,當(dāng)緩存滿時(shí)庆聘,LRUcacher會(huì)移除較早使用的緩存對象胜臊,然后再添加新的緩存對象。

ImagerLoader的為什么會(huì)被淘汰

  • 首先相對于Gilde來說伙判,ImagerLoader的配置相對繁瑣象对,需要對其中的參數(shù)有比較詳細(xì)的了解才能比較好的駕馭ImageLoader,而Gilde簡單易用宴抚,沒有繁瑣復(fù)雜的配置勒魔;
  • Gilde中的內(nèi)存管理比ImageLoader做的更好,雖然ImageLoader也說有三層緩存菇曲,但是實(shí)際上是兩層冠绢,一個(gè)磁盤,一個(gè)內(nèi)存緩存常潮。而Gilde中的內(nèi)存管理做到了兩級(jí)內(nèi)存緩存弟胀,更加可靠;
  • 在網(wǎng)絡(luò)請求方面喊式,ImageLoader采用的是HttpConnection孵户,而Gilde默認(rèn)采用更加高效的okhttp,雖然兩者都支持自定義下載器岔留,但是明顯Gilde的支持更好夏哭。

參考鏈接

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市献联,隨后出現(xiàn)的幾起案子竖配,更是在濱河造成了極大的恐慌,老刑警劉巖里逆,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件进胯,死亡現(xiàn)場離奇詭異,居然都是意外死亡运悲,警方通過查閱死者的電腦和手機(jī)龄减,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來班眯,“玉大人希停,你說我怎么就攤上這事烁巫。” “怎么了宠能?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵亚隙,是天一觀的道長。 經(jīng)常有香客問我违崇,道長阿弃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任羞延,我火速辦了婚禮渣淳,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘伴箩。我一直安慰自己入愧,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布嗤谚。 她就那樣靜靜地躺著棺蛛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪巩步。 梳的紋絲不亂的頭發(fā)上旁赊,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天,我揣著相機(jī)與錄音椅野,去河邊找鬼终畅。 笑死,一個(gè)胖子當(dāng)著我的面吹牛鳄橘,可吹牛的內(nèi)容都是我干的声离。 我是一名探鬼主播,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼瘫怜,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了本刽?” 一聲冷哼從身側(cè)響起鲸湃,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎子寓,沒想到半個(gè)月后暗挑,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡斜友,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年炸裆,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鲜屏。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡烹看,死狀恐怖国拇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情惯殊,我是刑警寧澤酱吝,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布,位于F島的核電站土思,受9級(jí)特大地震影響务热,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜己儒,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一崎岂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧闪湾,春花似錦该镣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至娘纷,卻和暖如春嫁审,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背赖晶。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工律适, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人遏插。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓捂贿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親胳嘲。 傳聞我的和親對象是個(gè)殘疾皇子厂僧,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

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