Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> ImageLoader簡單分析(三)

ImageLoader簡單分析(三)

編輯:關於Android編程

其實對於緩存的實現原理及其流程總的來說都很簡單,無非就是先從網絡加載相關資源,然後用內存緩存或者磁盤緩存把下載到的資源緩存起來;等再次加載相同的資源的時候如果內存緩存或者磁盤緩存還存在就用緩存裡面的資源,否則仍然進行網絡加載,重復此過程而已。

嚴格說來也沒什麼可講的,但是通過研讀ImageLoader的源碼倒是可以學到很多緩存之外的東西:學學別人的代碼怎麼設計,資源加載的異步處理機制的靈活使用等等,甚至也可以吸取到一些東西來運用到自己的項目中去。

就ImageLoader本身來說,也可以讓人了解其工作原理,同時也知道了在配置ImageLoaderConfiguration的時候,那些配置都代表了什麼東西,真正達到了知其然知其所以然的目的。

上篇博客中主要是對ImageLoader讀取內存到最終顯示圖片的分析。最終的結論是不論是異步還是同步,最終都會調用ImageView的setImageBitmap和setImageDrawable方法。其實你也很容易能想到不論是從內存讀取緩存的Bitmap,還是從文件緩存抑或是從網絡加載的圖片資源,最終都是調用setImageBitmap或者setImageDrawable方法來實現的。盡管理論上很簡單,但是本文還是會對讀取文件緩存或者從網絡資源的實現上針對ImageLoader源碼來講解一遍,看看能從裡面學到什麼東西。

下面正式開始講解,當內存緩存中沒有對應的bitmap的時候會執行下面一段代碼:

  //在圖片資源加載的過程到最終顯示的過程中顯示的圖片
if (options.shouldShowImageOnLoading()) {
              imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
            } else if (options.isResetViewBeforeLoading()) {//這個屬性還真沒用到過
                imageAware.setImageDrawable(null);
            }

            //把下載或者讀取文件緩存資源所需的參數組成ImageLoadingInfo對象
            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);
            }

上面的代碼也很簡單,主要執行了兩步操作:
1)在圖片資源從文件緩存或者網絡資源讀取到最終顯示的過程中,顯示一個圖片,以免用戶在此等待期間ImageView什麼都不顯示而顯得界面不友好。當然這需要你創建自己的DisplayImageOptions對象的時候調用showImageOnLoading(Drawable)方法提供一個默認加載中的圖片。
2)把圖片資源地址,以及ImageLoaderConfiguraton最終封裝成LoadAndDisplayImageTask ,顧名思義這個類主要的作用就是加載圖片資源並最終使得ImageView顯示圖片,該類是一個Runnable。如果是同步加載的話就直接當做普通的java對象來執行run方法,否則就交給engine這個ImageLoader內部提供的異步機制對oadAndDisplayImageTask 這個Runnable進行異步執行。
關於ImageLoader的異步處理的方式,會另外開篇博客進行說明。涉及到多線程的東東對我來說也是一個不小的挑戰,所以後面會專門列一篇博客進行說明。
下面的分析其實跟上一篇博客一樣的順序進行了,如果覺得啰嗦的話,各位看官可以繞道而行了,閒言少敘,能用代碼說明的就不要用語言說明!

讀取文件緩存和網絡資源,生成bitmap

LoadAndDisplayImageTask 的run方法取出了異步處理的相關控制之後代碼如下:

           //先從內存緩存中獲取
            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {
                //開始從文件緩存或者網絡中加載圖片資源
                bmp = tryLoadBitmap();
                //如果最終獲取失敗,那麼就返回
                if (bmp == null) return; //注意此時你自定義的
                //在正式使用bitmap之前和放入緩存之前對bitmap進行處理
                if (options.shouldPreProcess()) {
                    bmp = options.getPreProcessor().process(bmp);               
                }

                //如果使用內存緩存的話,就把加載到的bitmap放入緩存
                if (bmp != null && options.isCacheInMemory()) {
                    configuration.memoryCache.put(memoryCacheKey, bmp);
                }
            } else {//標志是從內存緩存中讀取的資源
                loadedFrom = LoadedFrom.MEMORY_CACHE;           
            }

            //如果在加載完成後的圖片仍然需要進行處理的話
            if (bmp != null && options.shouldPostProcess()) {
                bmp = options.getPostProcessor().process(bmp);
            }

        //創建對象DisplayBitmapTask 最終進行顯示。
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        runTask(displayBitmapTask, syncLoading, handler, engine);

上面的代碼邏輯調理也很清晰,從上面可以得到如下的結論:
1)在從文件緩存或者網絡獲取的圖片資源Bitmap在放入緩存之前,如果你的Options對象創建時build了preProcessor(BitmapProcessor),那麼那就可以對此Bitmap進行處理,然後把處理過的bitmap放入緩存;
2)如果你的Options允許你對圖片進行內存緩存,那麼就將(處理過的)bitmap放入memory cache中。
3)如果仍然需要對bmp進行處理,那麼就要你為Options配置postProcessor了
4)最終跟讀取內存緩存一樣,DisplayBitmapTask 進行圖片的顯示了。
獲取你會說,上面的代碼沒有體現出從文件緩存或者網絡資源加載的過程啊?!

馬上就開始講,上面的代碼中調用了tryLoadBitmap()方法中做的就是這個活兒!

真正讀取disk cache或者網絡資源的方法:tryLoadBitmap

private Bitmap tryLoadBitmap() throws TaskCancelledException {
        Bitmap bitmap = null;
        1)從文件緩存中讀取uri對應的資源。
        File imageFile = this.cache.get(uri);
        if (imageFile != null && imageFile.exists()) {//文件緩存存在
                //標明是從文件緩存中進行讀取的。
                loadedFrom = LoadedFrom.DISC_CACHE;
                //生成bitmap對象
                bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
        }
       if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {//進入網絡加載
                //標明是從網絡中加載的圖片資源
                loadedFrom = LoadedFrom.NETWORK;

                String imageUriForDecoding = uri;
                if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {//如果需要文件緩存並且緩存成功
                    imageFile = this.cache.get(uri);
                    if (imageFile != null) {
                        imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                    }
                }
                //生成bitmap對象
                bitmap = decodeImage(imageUriForDecoding);

    }

        return bitmap;
    }

正如你想象的哪樣,先從disk cache裡面獲取bitmap,如果獲取失敗則從網路中獲取圖片起源,並且根據是否需要文件緩存來進行來緩存。對於這一點有個地方需要注意的地方(專門把代碼提出來):

String imageUriForDecoding = uri;
                if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {//如果需要文件緩存並且緩存成功
                    imageFile = this.cache.get(uri);
                    if (imageFile != null) {
                        imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                    }
                }
                //生成bitmap對象
                bitmap = decodeImage(imageUriForDecoding);

上面的這段代碼主要做了下面幾件事兒:
1)首先定義一個imageUriForDecoding 初始值為uri
2)如果DisplayImageOptions允許使用文件緩存,那麼就會調用tryCacheImageOnDisk()方法對圖片資源進行下載和保存。如果保存成功則修改imageUriForDecoding 的值為Scheme.FILE.wrap(imageFile.getAbsolutePath());
3)如果不允許文件緩存或者說tryCacheImageOnDisk()失敗,那麼imageUriForDecoding 仍然等於uri.
4)最終調用decodeImage對imageUriForDecoding生成bitmap對象。

在此先不講解tryCacheImageOnDisk這個方法或者說我們假設不允許文件緩存,那麼看看decodeImage解析uri都做了些神馬!然後在對tryCacheImageOnDisk方法進行簡單講解。

decodeImage的生成bitmap過程

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

decodeImage方法會把相關參數配置成ImageDecodingInfo 對象然後交給LoadAndDisplayImageTask的decoder引用所代表的對象進行處理。
decoder是一個ImageDecoder(為接口),初始化代碼如下:

public LoadAndDisplayImageTask(ImageLoaderEngine engine, ImageLoadingInfo imageLoadingInfo, Handler handler) {
        decoder = configuration.decoder;
}

很明顯這個對象的初始化也是從configuration(ImageLoaderConfiguation)裡面獲取。我們知道ImageLoaderConfiguration的組建是通過其嵌套類Builder來一步步構建的,那麼不用說configuration裡面的decoder也是有Builder裡面組裝而來,並且我們在使用ImageLoderConfiguration的時候一般不會配置這個參數,所以Builder必然對其設置了默認值,在上篇博客中就提高Builder類裡面有一個方法initEmptyFieldsWithDefaultValues()就是專門提供一些默認值用的,口說無憑,還是讓代碼說話在initEmptyFieldsWithDefaultValues():方法裡面有這麼一段代碼:

if (decoder == null) {
                decoder = DefaultConfigurationFactory.createImageDecoder(writeLogs);
            }

public static ImageDecoder createImageDecoder(boolean loggingEnabled) {
        return new BaseImageDecoder(loggingEnabled);
    }

從上面的代碼我們知道最終我們的decoder為BaseImageDecoder,千呼萬喚始出來的感覺!所以馬不停蹄的看看他的decode方法吧:

public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
        Bitmap decodedBitmap;
        ImageFileInfo imageInfo;
        //獲取一個輸入流
        InputStream imageStream = getImageStream(decodingInfo);
        try {
            imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
            imageStream = resetStream(imageStream, decodingInfo);
            Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
            //最終生成decodedBitmap的地方
            decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
        } finally {
            IoUtils.closeSilently(imageStream);
        }

        if (decodedBitmap == null) {
            decodingInfo.getImageKey());
        } else {//對bitmap的在處理
            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                    imageInfo.exif.flipHorizontal);
        }
        return decodedBitmap;
    }

這個decoder方法說簡單也不簡單,裡面來回調用的方法也不少,在這裡就不多說了(一是偷個懶,而是博主的水平也不是那麼6,就不誤解讀者了)。
那麼bitmap的生產過程到此就結束,繼續回到之前說過的對tryCacheImageOnDisk()的講解:這個方法主要是用來從網絡中加載數據並把數據保存到文件緩存的。
我屮艸芔茻,不經意間時間過得可真快,一不小心居然凌晨00:40分了!再接再厲,一鼓作氣寫完再睡覺吧!

圖片資源的文件緩存過程

先貼出來tryCacheImageOnDisk的代碼:

private boolean tryCacheImageOnDisk() throws TaskCancelledException {
        boolean loaded;
        //下載圖片
            loaded = downloadImage();
            if (loaded) {
                int width = configuration.maxImageWidthForDiskCache;//文件緩存的寬度最大值
                int height = configuration.maxImageHeightForDiskCache;//文件緩存的高度最大值
                if (width > 0 || height > 0) {
                    //重新設置大小並且進行保存
                    resizeAndSaveImage(width, height); 
                }
            }
        return loaded;
    }

從上面的代碼可以看出先調用downloadImage()方法對圖片資源進行下載:

private boolean downloadImage() throws IOException {
        //獲取輸入流
        InputStream is = getDownloader().getStream(uri, options.getExtraForDownloader());
        //調用save方法進行保存
        return this.cache.save(uri, is, this);
    }

上面兩個工作也很簡單,顯示生成輸入流,然後調用cache的save方法進行緩存。
因為ImageLoader對生成輸入流也不是三言兩語能完成的事兒,所以為了不影響這篇博客的結構,對於getStream的講解還得另外開篇博客,敬請期待。
看看這個save方法都做了些什麼,上面代碼中的cache的初始化的地方為:

private BaseDiscCache cache;
public LoadAndDisplayImageTask(。。。) {

        if(options.isNeedReflection() && (null != options.getReflection())) {
            this.cache = options.getReflection();
        }else {
            this.cache = (BaseDiscCache) configuration.diskCache;
        }
    }

先分析方法裡面吧,根據前面前面所寫,既然cache是在configuration裡面生成的,那麼就還在initEmptyFieldsWithDefaultValues裡面裡找找這個東西是神馬鬼:

if (diskCache == null) {
                if (diskCacheFileNameGenerator == null) {
                    diskCacheFileNameGenerator = DefaultConfigurationFactory.createFileNameGenerator();
                }
                diskCache = DefaultConfigurationFactory
                        .createDiskCache(context, diskCacheFileNameGenerator, diskCacheSize, diskCacheFileCount);
            }

最終會調用createDiskCache方法,該方法最終會生成一個UnlimitedDiscCache類的對象,該類是BaseDiscCache的子類,並且沒有重寫save方法,所以我們只要看BaseDiscCache的save放實現就可以了:

public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        //獲取文件
        File imageFile = getFile(imageUri);
        File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
        boolean loaded = false;
        /初始化文件輸出流
        OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
     //像緩存文件中寫入緩存數據。        
    loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);

        } 
        return loaded;
    }

到此為止,文件緩存的工作過程算是草草分析完畢! downloadImage()分析完畢後,我們知道ImageLoaderConfiguration是可以對圖片緩存文件的最大寬高進行設定的,如果你在builder的過程中對maxImageWidthForDiskCache或者maxImageHeightForDiskCache進行了配置,那麼會繼續調用resizeAndSaveImage方法來resize後對resize後的bitmap進行保存工作,此時調用的是save的另外一個重載方法:

/** Decodes image file into Bitmap, resize it and save it back */
    private boolean resizeAndSaveImage(int maxWidth, int maxHeight) throws IOException {
        // Decode image file, compress and re-save it
        boolean saved = false;
        File targetFile = this.cache.get(uri);
        if (targetFile != null && targetFile.exists()) {
            ImageSize targetImageSize = new ImageSize(maxWidth, maxHeight);
        .....
            Bitmap bmp = decoder.decode(decodingInfo);
            //如果需要對文件緩存的bitmap進行處理
            if (bmp != null && configuration.processorForDiskCache != null) {       
                bmp = configuration.processorForDiskCache.process(bmp);

            }
            if (bmp != null) {
                saved = this.cache.save(uri, bmp);
                bmp.recycle();
            }
        }
        return saved;
    }

從前面的講解中我們知道,在把圖片放入memory的時候時候可以通過BitmapProcessor對bitmap進行處理,同樣的在上面的代碼中也可以看出來,在保存disk cache之前我們也可以調用configuration.processorForDiskCache.process(bmp);對bitmap 進行處理後然後在進行save,該save方法也很簡單,下面簡單的貼一下代碼,不做詳細解釋:

public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        File imageFile = getFile(imageUri);
        File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
        //文件輸出流
        OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
        boolean savedSuccessfully = false;
        try {
        //對bitmap 進行文件緩存
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        } 
        //回收
        bitmap.recycle();
        return savedSuccessfully;
    }

到此為止,關於ImageLoader讀取disk cache或者網絡資源的過程分析完畢,簡單做個總結如下:
1)如果調用tryLoadBitmap()從文件緩存或者網絡中加載圖片資源,如果需要把bitmap放入memory cache ,那麼在放入memory cache你可以用BitmapProcessor對bitmap處理後再放入緩存!
2)如果disk cache中有存在相關資源使用之,如果不存在那麼就開啟網絡加載。在這裡有個需要注意的地方:如果在你的options配置裡面不需要文件緩存,那麼tryCacheImageOnDisk()方法是不會執行的。此時就跟你平時用ImageView顯示網絡資源的用法類似,不會走下載保存邏輯!
)tryCacheImageOnDisk()這個方法主要用來對圖片資源的下載保存操作。
簡單總結這麼多吧,本篇篇幅貌似有點長,寫的也有點亂,如果有錯誤的地方歡迎批評指正,共同學習!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved