Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> Android-Universal-Image-Loader 源碼解讀

Android-Universal-Image-Loader 源碼解讀

編輯:關於Android編程

Universal-Image-Loader是一個強大而又靈活的用於加載、緩存、顯示圖片的Android庫。它提供了大量的配置選項,使用起來非常方便。
image_1al8q6r8k4sv1v1gqll3ot1bd49.png-332.8kB

基本概念

基本使用

首次配置
在第一次使用ImageLoader時,必須初始化一個全局配置,一般會選擇在Application中配置。

public class MyApplication extends Application {
@Override
public void onCreate() {
    super.onCreate();

    //為ImageLoader初始化一個全局配置
    ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(this)
        ...
        .build();
    ImageLoader.getInstance().init(config);//初始化
    ...
}
}

可選的所有配置如下。

// 不要把這些拷貝到你的項目中! 這裡僅僅是例舉出所有可用的選項,根據自身情況進行配置。。
File cacheDir = StorageUtils.getCacheDirectory(context);
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
    .memoryCacheExtraOptions(480, 800) // default = device screen ,默認為屏幕寬高 dimensions,內存緩存的最大寬高
    .diskCacheExtraOptions(480, 800, null)//磁盤緩存最大寬高,默認不限制
    .taskExecutor(...)//下載圖片的線程池
    .taskExecutorForCachedImages(...);//處理緩存圖片的線程池
    .threadPoolSize(3) // default //線程池數量,只在使用默認線程池有效
    .threadPriority(Thread.NORM_PRIORITY - 2) // default //線程優先級
    .tasksProcessingOrder(QueueProcessingType.FIFO) // default //隊列處理策略
    .denyCacheImageMultipleSizesInMemory() //阻止內存中多尺寸緩存
    .memoryCache(new LruMemoryCache(2 * 1024 * 1024)) //內存緩存
    .memoryCacheSize(2 * 1024 * 1024) //配置緩存大小
    .memoryCacheSizePercentage(13) // default //緩存百分比
    .diskCache(new UnlimitedDiskCache(cacheDir)) // default //磁盤緩存
    .diskCacheSize(50 * 1024 * 1024) //磁盤緩存大小,只在使用默認緩存有效
    .diskCacheFileCount(100)  //磁盤緩存文件數,只在使用默認緩存有效
    .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default //key生成器
    .imageDownloader(new BaseImageDownloader(context)) // default  //圖片下載器
    .imageDecoder(new BaseImageDecoder()) // default  //圖片解碼器
    .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default,這裡配置DisplayImageOptions
    .writeDebugLogs() //打印調試日志
    .build();

配置顯示圖片選項
我們可以給每一次顯示圖片配置一些選項,比如是否可以緩存,采樣大小等等。

 // 不要把這些拷貝到你的項目中! 這裡僅僅是例舉出所有可用的選項,根據自身情況進行配置。。
DisplayImageOptions options = new DisplayImageOptions.Builder()
    .showImageOnLoading(R.drawable.ic_stub) // resource or drawable
    .showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
    .showImageOnFail(R.drawable.ic_error) // resource or drawable
    .resetViewBeforeLoading(false)  // default 僅在沒有配置loading占位圖時生效
    .delayBeforeLoading(1000) //延時加載
    .cacheInMemory(false) // default
    .cacheOnDisk(false) // default
    .preProcessor(...) //bitmap預處理
    .postProcessor(...) //bitmap後處理
    .extraForDownloader(...) //額外的下載器
    .considerExifParams(false) // default //考慮旋轉參數
    .imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default 默認采樣方式
    .bitmapConfig(Bitmap.Config.ARGB_8888) // default  
    .decodingOptions(...) //配置解碼的BitmapFactory.Options
    .displayer(new SimpleBitmapDisplayer()) // default 配置顯示器
    .handler(new Handler()) // default  //配置Handler
    .build();

加載圖片

ImageLoader.getInstance().displayImage(...)//顯示圖片
ImageLoader.getInstance().loadImage(...)//加載圖片

使用疑問

相信這個圖片加載框架是大家最熟悉而又最有疑問的。疑問如下:

這個框架會不會對本地圖片進行磁盤緩存? 內部是怎麼支持Drawable等其他類型的? 怎麼實現多尺寸和單尺寸緩存? 怎麼實現僅在wifi環境下加載圖片? 這個框架可以在ListView的復用中自動取消任務嗎? 怎麼針對ListView進行優化?

源碼解讀

初始化全局配置

我們知道在使用ImageLoader之前,必須進行配置,那麼我們就從ImageLoaderConfiguration這個類入手,該類屬性如下:

public final class ImageLoaderConfiguration {
    final Resources resources;//用來加載drawable圖片
    //內存緩存最大寬高,默認為屏幕尺寸
    final int maxImageWidthForMemoryCache;
    final int maxImageHeightForMemoryCache;
    //磁盤緩存最大寬高,默認為0,不做限制
    final int maxImageWidthForDiskCache;
    final int maxImageHeightForDiskCache;
    //Bitmap處理器,用來處理原始bitmap,返回一個新bitmap
    final BitmapProcessor processorForDiskCache;

    final Executor taskExecutor;//線程池,默認3個線程
    final Executor taskExecutorForCachedImages;//緩存圖片線程池,默認3個線程
    //是否使用了自定義的線程池
    final boolean customExecutor;
    final boolean customExecutorForCachedImages;
    //線程池數量、優先級、排隊類型(FIFO,LIFO)
    final int threadPoolSize;
    final int threadPriority;
    final QueueProcessingType tasksProcessingType;

    final MemoryCache memoryCache;//接口,內存緩存
    final DiskCache diskCache;//接口,磁盤緩存
    final ImageDownloader downloader;//圖片下載器,根據url下載成流
    final ImageDecoder decoder;//圖片解碼器,用於將流解碼成bitmap
    final DisplayImageOptions defaultDisplayImageOptions;//顯示配置

    final ImageDownloader networkDeniedDownloader;//禁止網絡的下載器(只從本地圖片加載圖片,可以用來做只在wifi下加載圖片這個功能)
    final ImageDownloader slowNetworkDownloader;//慢網絡的加載器

注釋寫的很詳細,這裡就不一一介紹了,我們知道構建者模式,需要使用build()來初始化,那麼build()又做了什麼?

        public ImageLoaderConfiguration build() {
            initEmptyFieldsWithDefaultValues();//初始化部分空值
            return new ImageLoaderConfiguration(this);//賦值
        }

可以看出,build()會對一些空值進行初始化,然後在通過ImageLoaderConfiguration的構造方法來賦值參數。ImageLoaderConfiguration的構造方法只是簡單的一些賦值操作,我們就不進去看了。現在來看看initEmptyFieldsWithDefaultValues方法。

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

初始化線程池(taskExecutor,taskExecutorForCachedImages)

根據隊列排隊策略,采用了不同的阻塞隊列來初始化線程池。此外,可以看出核心線程數和最大線程數是一樣的,在ImageLoader中默認開啟3個線程。

    /** Creates default implementation of task executor */
    public static Executor createExecutor(int threadPoolSize, int threadPriority,
            QueueProcessingType tasksProcessingType) {
        //隊列類型
        boolean lifo = tasksProcessingType == QueueProcessingType.LIFO;
        //隊列
        BlockingQueue taskQueue =
                lifo ? new LIFOLinkedBlockingDeque() : new LinkedBlockingQueue();
        //線程池
        return new ThreadPoolExecutor(threadPoolSize, threadPoolSize, 0L, TimeUnit.MILLISECONDS, taskQueue,
                createThreadFactory(threadPriority, "uil-pool-"));
    }

初始化緩存(diskCache,memoryCache)

先來看下磁盤緩存,createReserveDiskCacheDir可以看出根據是否設置了磁盤緩存大小用了不同的DiskCache。當設置了緩存大小時采用LruDiskCache,LruDiskCache會單獨新建一個名為uil-images的目錄用來存放,UnlimitedDiskCache用於不限制緩存大小的情況,直接緩存在根目錄下(當根目錄不可用時,才會選擇獨立目錄)。

    public static DiskCache createDiskCache(Context context, FileNameGenerator diskCacheFileNameGenerator,
            long diskCacheSize, int diskCacheFileCount) {
        File reserveCacheDir = createReserveDiskCacheDir(context);//創建獨立緩存目錄
        if (diskCacheSize > 0 || diskCacheFileCount > 0) {
            //使用獨立的緩存目錄
            File individualCacheDir = StorageUtils.getIndividualCacheDirectory(context);
            try {
                //如果定義了磁盤緩存大小,則返回一個LruDiskCache
                return new LruDiskCache(individualCacheDir, reserveCacheDir, diskCacheFileNameGenerator, diskCacheSize,
                        diskCacheFileCount);
            } catch (IOException e) {
                L.e(e);
                // continue and create unlimited cache
            }
        }
        //獲取緩存根目錄
        File cacheDir = StorageUtils.getCacheDirectory(context);
        //如果沒有定義磁盤緩存大小,則返回一個UnlimitedDiskCache。將根目錄和獨立目錄都傳入
        return new UnlimitedDiskCache(cacheDir, reserveCacheDir, diskCacheFileNameGenerator);
    }

LruDiskCache內部使用了DiskLruCache,DiskLruCache是JakeWharton開源的一個緩存庫,關於DiskLruCache的使用請自行查閱資料,這裡只需知道LruDiskCache中使用了DiskLruCache來進行磁盤緩存。UnlimitedDiskCache這個緩存類不用考慮磁盤緩存大小,這裡也不做介紹了。此外,ImageLoader中還提供了一個LimitedAgeDiskCache可以指定緩存時間。
關於內存緩存比較簡單,如果可以多尺寸緩存使用了LruMemoryCache,否則使用FuzzyKeyMemoryCache。內存緩存都是使用LruCache實現的。這裡不做深究。

初始化下載器(ImageDownloader)

我們知道下載器是用來根據url來下載為InputStream。那麼具體是怎麼實現的呢?

    public static ImageDownloader createImageDownloader(Context context) {
        return new BaseImageDownloader(context);
    }

內部返回了BaseImageDownloader,BaseImageDownloader的核心源碼如下:

    @Override
    public InputStream getStream(String imageUri, Object extra) throws IOException {
        switch (Scheme.ofUri(imageUri)) {
            case HTTP:
            case HTTPS:
                return getStreamFromNetwork(imageUri, extra);
            case FILE:
                return getStreamFromFile(imageUri, extra);
            case CONTENT:
                return getStreamFromContent(imageUri, extra);
            case ASSETS:
                return getStreamFromAssets(imageUri, extra);
            case DRAWABLE:
                return getStreamFromDrawable(imageUri, extra);
            case UNKNOWN:
            default:
                return getStreamFromOtherSource(imageUri, extra);
        }
    }

可以看出,根據不同類型使用了不同方法,看到這相信你已經明白該庫是怎麼支持Drawable等其他類型的了,如果你需要支持自定義的類型,只需要重寫getStreamFromOtherSource即可。我們來看看其中兩種類型。

getStreamFromDrawable
將Drawable轉化為流

protected InputStream getStreamFromDrawable(String imageUri, Object extra) {
    String drawableIdString = Scheme.DRAWABLE.crop(imageUri);//提取drawable://後的內容
    int drawableId = Integer.parseInt(drawableIdString);//提取id
    return context.getResources().openRawResource(drawableId);//轉為InputStream
}

getStreamFromNetwork

    protected InputStream getStreamFromNetwork(String imageUri, Object extra) throws IOException {
    HttpURLConnection conn = createConnection(imageUri, extra);
    //..
    //省略了部分源碼
    InputStream imageStream=conn.getInputStream();//獲取流

    //..
    //省略了部分源碼
    return new ContentLengthInputStream(new BufferedInputStream(imageStream, BUFFER_SIZE), conn.getContentLength());//將InputStream包裝為ContentLengthInputStream後返回,可以獲取長度。
}

源碼的思路非常清晰,如果想要擴展的話也是比較簡單的。

初始化解碼器(ImageDecoder)

DefaultConfigurationFactory.createImageDecoder(writeLogs)內部同樣返回了一個BaseImageDecoder,解碼器用來將InputStream解碼成Bitmap,我們來看看內部的核心源碼。

    @Override
    public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
        Bitmap decodedBitmap;
        ImageFileInfo imageInfo;//保存了圖片的大小和旋轉信息

        InputStream imageStream = getImageStream(decodingInfo);//獲取輸入流
        //..
        //省略了部分源碼
         imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);//從輸入流中獲取大小信息和旋轉信息保存起來,采用了inJustDecodeBounds
        imageStream = resetStream(imageStream, decodingInfo);//由於流不能二次讀取,所有這裡進行重置
        //根據獲取到的大小,生成一個BitmapFactory.Options
        Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
        //根據BitmapFactory.Options來解碼bitmap
        decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
        //..
        //省略了部分源碼

        if (decodedBitmap == null) {
            L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
        } else {
            //如果bitmap不為空,現在對bitmap進行旋轉和翻轉操作(如果需要考慮旋轉因素)
            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                    imageInfo.exif.flipHorizontal);
        }
        return decodedBitmap;
    }

整個解碼流程是這樣的,首先從ImageDecodingInfo中獲取輸入流(ImageDecodingInfo內部保存了下載器,通過下載器下載成流),然後采用inJustDecodeBounds來讀取寬高和Exif信息。不同於BitmapFactory.decodeFile,InputStream不能二次讀取,必須重置,讀取到寬高信息後,通過prepareDecodingOptions來計算采樣率,然後解碼返回bitmap,最後對bitmap處理Exif旋轉信息。
ImageDecodingInfo的源碼如下:

public class ImageDecodingInfo {

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

    private final ImageScaleType imageScaleType;//圖片縮放類型,NONE(不縮放),NONE_SAFE(除非超出硬件加速的顯示范圍,否則不縮放),IN_SAMPLE_POWER_OF_2(2次冪縮放),IN_SAMPLE_INT(整數縮放),EXACTLY(縮放到至少寬高有一個等於目標值,原始圖片小於目標大小則不縮放),EXACTLY_STRETCHED(原始圖片小於目標大小仍然縮放)

    private final ViewScaleType viewScaleType;//ImageView的縮放類型(被整理成兩類,FIT_INSIDE和CROP)

    private final ImageDownloader downloader;//圖片下載器
    private final Object extraForDownloader;//輔助下載器

    private final boolean considerExifParams;//考慮旋轉參數
    private final Options decodingOptions;//解碼的BitmapFactory.Options

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

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

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

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

ImageFileInfoExifInfo的源碼如下,可以看出使用了ImageSize來保存寬高,ExifInfo中保存了旋轉角度以及是否水平翻轉等等。
image_1al6rh8q61lfje1kmre1voa1e529.png-43.9kB
讀取旋轉信息用了Android中的ExifInterfaceapi,由於只能從文件獲取Exif信息,所以在defineImageSizeAndRotation中做了相關判斷。

    protected ExifInfo defineExifOrientation(String imageUri) {
        int rotation = 0;
        boolean flip = false;
        try {
            ExifInterface exif = new ExifInterface(Scheme.FILE.crop(imageUri));
            int exifOrientation = exif.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL);//讀取旋轉信息。默認為ORIENTATION_NORMAL
            switch (exifOrientation) {
                case ExifInterface.ORIENTATION_FLIP_HORIZONTAL:
                    flip = true;
                case ExifInterface.ORIENTATION_NORMAL:
                    rotation = 0;
                    break;
                case ExifInterface.ORIENTATION_TRANSVERSE:
                    flip = true;
                case ExifInterface.ORIENTATION_ROTATE_90:
                    rotation = 90;
                    break;
                case ExifInterface.ORIENTATION_FLIP_VERTICAL:
                    flip = true;
                case ExifInterface.ORIENTATION_ROTATE_180:
                    rotation = 180;
                    break;
                case ExifInterface.ORIENTATION_TRANSPOSE:
                    flip = true;
                case ExifInterface.ORIENTATION_ROTATE_270:
                    rotation = 270;
                    break;
            }
        } catch (IOException e) {
            L.w("Can't read EXIF tags from file [%s]", imageUri);
        }
        return new ExifInfo(rotation, flip);
    }

最後將旋轉信息應用到bitmap中。可以看出,使用了Matrix進行旋轉縮放。

    protected Bitmap considerExactScaleAndOrientatiton(Bitmap subsampledBitmap, ImageDecodingInfo decodingInfo,
            int rotation, boolean flipHorizontal) {
        Matrix m = new Matrix();

        //獲取采樣錯放類型
        ImageScaleType scaleType = decodingInfo.getImageScaleType();
        if (scaleType == ImageScaleType.EXACTLY || scaleType == ImageScaleType.EXACTLY_STRETCHED) {
            ImageSize srcSize = new ImageSize(subsampledBitmap.getWidth(), subsampledBitmap.getHeight(), rotation);
            //計算縮放率
            float scale = ImageSizeUtils.computeImageScale(srcSize, decodingInfo.getTargetSize(), decodingInfo
                    .getViewScaleType(), scaleType == ImageScaleType.EXACTLY_STRETCHED);
            //縮放
            if (Float.compare(scale, 1f) != 0) {
                m.setScale(scale, scale);
                }
            }
        }
        // Flip bitmap if need
        if (flipHorizontal) {//水平翻轉
            m.postScale(-1, 1);

        }
        //旋轉
        if (rotation != 0) {
            m.postRotate(rotation);
        }

         //創建了一個新bitmap返回
        Bitmap finalBitmap = Bitmap.createBitmap(subsampledBitmap, 0, 0, subsampledBitmap.getWidth(), subsampledBitmap
                .getHeight(), m, true);
        if (finalBitmap != subsampledBitmap) {
            subsampledBitmap.recycle();
        }
        return finalBitmap;
    }

看到這裡,我們明白了,uri通過下載器下載成InputStream,然後解碼器讀取圖片的寬高和旋轉信息,采樣InputStream解碼成bitmap,最後處理了旋轉信息並返回。

初始化顯示選項(DisplayImageOptions)

在初始化配置中使用了createSimple來創建了一個默認顯示選項。

if (defaultDisplayImageOptions == null) {//創建默認的顯示配置
                defaultDisplayImageOptions = DisplayImageOptions.createSimple();
            }

關於DisplayImageOptions,下一小節會詳細介紹,createSimple只是直接調用了build用了默認值而已。

配置顯示圖片選項(DisplayImageOptions)

DisplayImageOptions同樣也使用了構建者模式,按照老規矩,先來看看該類的屬性。

public final class DisplayImageOptions {
     //=============各種占位圖 START===============
    private final int imageResOnLoading;
    private final int imageResForEmptyUri;
    private final int imageResOnFail;
    private final Drawable imageOnLoading;
    private final Drawable imageForEmptyUri;
    private final Drawable imageOnFail;
    //=============各種占位圖 END===============
    private final boolean resetViewBeforeLoading;//加載前重置
    private final boolean cacheInMemory;//內存緩存?
    private final boolean cacheOnDisk;//磁盤緩存?
    private final ImageScaleType imageScaleType;//采樣縮放類型
    private final Options decodingOptions;//解碼時的BitmapFactory.Options
    private final int delayBeforeLoading;//延時加載
    private final boolean considerExifParams;//考慮旋轉參數
    private final Object extraForDownloader;//輔助的下載器
    //bitmap處理器接口,用來處理原始bitmap,返回一個新bitmap
    private final BitmapProcessor preProcessor;//預處理(磁盤中加載出來,放入內存之前)
    private final BitmapProcessor postProcessor;//後處理(顯示之前)
    private final BitmapDisplayer displayer;//圖片顯示器
    private final Handler handler;//用於切換線程
    private final boolean isSyncLoading;//是否異步加載

我們知道構建者模式一般通過build來初始化,那我們來看看一些默認值。
image_1al6tvfcn6qc1nv17jj1me1fgim.png-67.2kB
可以看出,默認沒有采用任何緩存策略。縮放類型采用了二次冪采樣。
默認的BitmapDisplayer如下:

    /** Creates default implementation of {@link BitmapDisplayer} - {@link SimpleBitmapDisplayer} */
    public static BitmapDisplayer createBitmapDisplayer() {
        return new SimpleBitmapDisplayer();
    }

可以看出內部采用了SimpleBitmapDisplayer

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

ImageAware保存View的寬高、View的哈希值標識以及View本身等信息,主要用來將圖像設置到控件中。
LoadedFrom是一個枚舉類,用來標識從內存、磁盤、網絡中加載。

此外,還有FadeInBitmapDisplayer、RoundedBitmapDisplayer、CircleBitmapDisplayer等等。
CircleBitmapDisplayer的源碼如下,可以看出唯一不同的是加載了CircleDrawable(自定義的Drawable類,使用BitmapShader來切圓),只要你喜歡,你可以自定義出各種各樣形狀的顯示器。

    @Override
    public void display(Bitmap bitmap, ImageAware imageAware, LoadedFrom loadedFrom) {
        if (!(imageAware instanceof ImageViewAware)) {
            throw new IllegalArgumentException("ImageAware should wrap ImageView. ImageViewAware is expected.");
        }

        imageAware.setImageDrawable(new CircleDrawable(bitmap, strokeColor, strokeWidth));
    }

用ImageAware包裝的好處在於內部使用了弱引用,可以避免內存洩漏。

加載/顯示圖片(loadImage/displayImage)

終於講到正題了——加載/顯示圖片,我們來看看ImageLoader是怎麼將下載器、解碼器、顯示器等結合起來了的吧。在分析之前,來認識一下ImageLoader這個類中的屬性。
image_1al6vp8bs1ici1papuek1rho1vmd13.png-40kB
出乎意料的簡潔,getInstance采用了單例模式。ImageLoadingListener加載監聽大家應該很清楚,這裡不做贅述。ImageLoaderConfiguration也已經介紹過了。但是ImageLoaderEngine這個是什麼鬼呢?
大家還記得 ImageLoader.getInstance().init(config);//初始化這一句嗎?沒錯,將ImageLoaderConfiguration傳入了進去。<喎?/kf/ware/vc/" target="_blank" class="keylink">vcD4NCjxwcmUgY2xhc3M9"brush:java;"> */ 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);//用ImageLoaderEngine包裝了起來 this.configuration = configuration;//同時也賦值給configuration一份 } else { L.w(WARNING_RE_INIT_CONFIG); } }

可以看出ImageLoaderEngine用來包裝了ImageLoaderConfiguration。那麼ImageLoaderEngine到底是來干嘛的?既然取名為ImageLoader引擎,可以想象到其核心地位。ImageLoaderEngine主要負責執行加載和顯示圖片等任務的引擎(LoadAndDisplayImageTask,ProcessAndDisplayImageTask)。

該類屬性如下。

class ImageLoaderEngine {

    final ImageLoaderConfiguration configuration;//配置

    private Executor taskExecutor;//任務執行者(下載圖片的線程池)
    private Executor taskExecutorForCachedImages;//處理緩存的線程池
    private Executor taskDistributor;//任務分配者(由它來控制把任務往哪個線程池提交)

    private final Map cacheKeysForImageAwares = Collections
            .synchronizedMap(new HashMap());//key為View的哈希值,value為請求的網址(後面會追加寬高)
    private final Map uriLocks = new WeakHashMap();//uri鎖map

    private final AtomicBoolean paused = new AtomicBoolean(false);
    private final AtomicBoolean networkDenied = new AtomicBoolean(false);
    private final AtomicBoolean slowNetwork = new AtomicBoolean(false);

    private final Object pauseLock = new Object();//暫停鎖

    ImageLoaderEngine(ImageLoaderConfiguration configuration) {
        this.configuration = configuration;

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

        taskDistributor = DefaultConfigurationFactory.createTaskDistributor();
    }
    //..
    //省略部分源碼

}

ImageLoaderEngine有兩個提交方法。一種處理本地/磁盤加載,一種處理內存加載。

    //
    void submit(final LoadAndDisplayImageTask task) {
        taskDistributor.execute(new Runnable() {
            @Override
            public void run() {
                //首先磁盤中獲取
                File image = configuration.diskCache.get(task.getLoadingUri());
                boolean isImageCachedOnDisk = image != null && image.exists();
                initExecutorsIfNeed();
                if (isImageCachedOnDisk) {
                    //如果磁盤存在就提交到緩存線程池
                    taskExecutorForCachedImages.execute(task);
                } else {
                    //提交到下載線程池
                    taskExecutor.execute(task);
                }
            }
        });
    }

    /** Submits task to execution pool */
    //ProcessAndDisplayImageTask提交到緩存線程池
    void submit(ProcessAndDisplayImageTask task) {
        initExecutorsIfNeed();
        taskExecutorForCachedImages.execute(task);
    }

顯示圖片(displayImage)

現在再來看看平時用的最多的displayImage吧。

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

可以看出,用ImageViewAware包裝了ImageView,displayImage最終調用的重載方法如下

public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {

        checkConfiguration();//檢查ImageLoaderConfiguration有沒有初始化。
        if (imageAware == null) {//ImageAware不可為空
            throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
        }
        if (listener == null) {//加載監聽
            listener = defaultListener;
        }
        if (options == null) {//顯示選項
            options = configuration.defaultDisplayImageOptions;
        }

       //=================如果是個空url直接設置占位圖 START====
        if (TextUtils.isEmpty(uri)) {
            engine.cancelDisplayTaskFor(imageAware);//引擎取消顯示任務(從map中移除)
            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;//返回
        }

        //=================如果是個空url直接設置占位圖 END====

        if (targetSize == null) {//如果沒有定義顯示目標大小,就根據ImageView自動獲取
            targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware,configuration.getMaxImageSize());
        }
        String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);//生成內存緩存的key(`[imageUri]_[width]x[height]`的形式)

        engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);//引擎准備顯示任務(放入map中)

        listener.onLoadingStarted(uri, imageAware.getWrappedView());//加載開始監聽

        //=================從內存中取 START====
        Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);//從內存中取
        if (bmp != null && !bmp.isRecycled()) {//如果內存中取到
            L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);

            if (options.shouldPostProcess()) {//是否需要後處理?
                //engine.getLockForUri(uri),獲取當前url的鎖
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                //ProcessAndDisplayImageTask是一個Runable對象,處理再顯示
                ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                        defineHandler(options));
                if (options.isSyncLoading()) {//如果是同步加載,則直接執行Runable中的run()方法
                    displayTask.run();
                } else {
                    engine.submit(displayTask);//異步加載,直接使用引擎提交到線程池中
                }
            } else {
                //如果不需要後處理bitmap,直接獲取BitmapDisplayer進行顯示
                options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//加載完成監聽
            }
        } else {

   //=================從內存中取 END====
   //=================從磁盤/網絡中取 START====
           //內存中沒有
            if (options.shouldShowImageOnLoading()) {//設置占位圖
                imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
            } else if (options.isResetViewBeforeLoading()) {
                imageAware.setImageDrawable(null);
            }
            //engine.getLockForUri(uri),獲取當前url的鎖
            ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                    options, listener, progressListener, engine.getLockForUri(uri));
            //LoadAndDisplayImageTask也是一個Runable對象,加載然後顯示
            LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                    defineHandler(options));
            if (options.isSyncLoading()) {//如果是同步,直接執行run
                displayTask.run();
            } else {
                engine.submit(displayTask);//否則通過引擎提交到線程池中
            }
        }

//=================從磁盤/網絡中取 END====
    }

源碼有點長,我們慢慢來。引擎取消任務和准備任務的源碼如下。

    void cancelDisplayTaskFor(ImageAware imageAware) {
        cacheKeysForImageAwares.remove(imageAware.getId());//從map中移除
    }

   void prepareDisplayTaskFor(ImageAware imageAware, String memoryCacheKey) {
        //key為View的hashcode,value為請求url(加上寬高)
        //保存到map中
        cacheKeysForImageAwares.put(imageAware.getId(), memoryCacheKey);
    }

ImageLoadingInfo用於保存圖片加載時所需要的信息

final class ImageLoadingInfo {

    final String uri;//原始的url
    final String memoryCacheKey; //加上寬高的url
    final ImageAware imageAware;
    final ImageSize targetSize;
    final DisplayImageOptions options;
    final ImageLoadingListener listener;
    final ImageLoadingProgressListener progressListener;
    final ReentrantLock loadFromUriLock; //uri鎖

    //構造方法中會傳入url鎖
    public ImageLoadingInfo(String uri, ImageAware imageAware, ImageSize targetSize, String memoryCacheKey,
            DisplayImageOptions options, ImageLoadingListener listener,
            ImageLoadingProgressListener progressListener, ReentrantLock loadFromUriLock) {
        this.uri = uri;
        this.imageAware = imageAware;
        this.targetSize = targetSize;
        this.options = options;
        this.listener = listener;
        this.progressListener = progressListener;
        this.loadFromUriLock = loadFromUriLock;
        this.memoryCacheKey = memoryCacheKey;
    }
}

如果內存緩存中存在bitmap,此時應該使用ProcessAndDisplayImageTask,ProcessAndDisplayImageTask是一個Runable對象,從名字可以看出,這個任務主要處理bitmap然後進行顯示。run方法如下:


    @Override
    public void run() {
        //獲取後處理器
        BitmapProcessor processor = imageLoadingInfo.options.getPostProcessor();
        //處理bitmap
        Bitmap processedBitmap = processor.process(bitmap);
        //將新bitmap給DisplayBitmapTask,DisplayBitmapTask是一個用來顯示的Runable
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(processedBitmap, imageLoadingInfo, engine,
                LoadedFrom.MEMORY_CACHE);

        //然後調用LoadAndDisplayImageTask來執行任務
        LoadAndDisplayImageTask.runTask(displayBitmapTask, imageLoadingInfo.options.isSyncLoading(), handler, engine);
    }

如果是異步,我們就需要通過引擎把ProcessAndDisplayImageTask提交到線程池中。

    void submit(ProcessAndDisplayImageTask task) {
        initExecutorsIfNeed();
        taskExecutorForCachedImages.execute(task);//提交到執行緩存的線程池中
    }

如果內存中沒有讀到bitmap,此時應該使用LoadAndDisplayImageTask來加載bitmap,LoadAndDisplayImageTask也是一個Runable對象,run方法如下:

@Override
    public void run() {
        if (waitIfPaused()) return;//如果暫停了就掛起等待
        if (delayIfNeed()) return; //如果延時就休眠等待

        ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;//獲取url鎖
        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();//判讀View是否被GC回收或者被復用,如果是就拋出異常
            //再次從內存取(為什麼再次取呢?因為有可能之前有個取的時候,已經有個任務提交到後台,現在正好加載完。)
            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {
                bmp = tryLoadBitmap();//如果內存中真的沒有,就去磁盤/網絡中取
                if (bmp == null) return; // listener callback already was fired

                checkTaskNotActual();//判讀View是否被GC回收或者被復用,如果是就拋出異常
                checkTaskInterrupted();//判讀線程是否被中斷,如果是就拋出異常

                if (options.shouldPreProcess()) {//是否預處理?
                    L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
                    bmp = options.getPreProcessor().process(bmp);//預處理
                    if (bmp == null) {
                        L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
                    }
                }

                //預處理完畢後,如果允許內存緩存,就放入內存中
                if (bmp != null && options.isCacheInMemory()) {
                    L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
                    configuration.memoryCache.put(memoryCacheKey, bmp);
                }
            } else {
                //如果內存中存在,就打個標識
                loadedFrom = LoadedFrom.MEMORY_CACHE;
                L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
            }

            //是否需要後處理?(之前直接從內存中取也詢問了是否後處理,忘記的回頭看一下源碼)
            if (bmp != null && options.shouldPostProcess()) {
                L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
                bmp = options.getPostProcessor().process(bmp);//處理
                if (bmp == null) {
                    L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
                }
            }
            checkTaskNotActual();//判讀View是否被GC回收或者被復用,如果是就拋出異常
            checkTaskInterrupted();//判讀線程是否被中斷,如果是就拋出異常
        } catch (TaskCancelledException e) {
            fireCancelEvent();//這裡捕獲異常,然後回調取消監聽
            return;
        } finally {
            loadFromUriLock.unlock();//釋放鎖
        }

       //顯示Bitmap的任務
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        //執行runtask。
        runTask(displayBitmapTask, syncLoading, handler, engine);
    }

我們先不看tryLoadBitmap,只需知道tryLoadBitmap是從磁盤或者網絡中讀取圖片即可。現在來看看DisplayBitmapTask中的run方法如下:

    @Override
    public void run() {
        if (imageAware.isCollected()) {//是否被回收?
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else if (isViewWasReused()) {//是否被重用?
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else {
            //最後才是調用displayer來顯示
            displayer.display(bitmap, imageAware, loadedFrom);//顯示bitmap
            engine.cancelDisplayTaskFor(imageAware);//取消任務
            listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);//監聽
        }
    }

LoadAndDisplayImageTask中的runTask源碼如下:

    static void runTask(Runnable r, boolean sync, Handler handler, ImageLoaderEngine engine) {
        if (sync) {
            r.run();//同步就直接執行run
        } else if (handler == null) {
            engine.fireCallback(r);//如果Handler為空,就提交到另起線程執行
        } else {
            handler.post(r);//使用handler切換到主線程
        }
    }

看完上面,應該已經知道了怎麼切換線程去顯示圖片的吧。

現在再來看看tryLoadBitmap相關源碼,ImageLoader是怎麼從磁盤或者網絡中加載圖片的呢?

    private Bitmap tryLoadBitmap() throws TaskCancelledException {
        Bitmap bitmap = null;
        try {
            //首先從磁盤中讀取
            File imageFile = configuration.diskCache.get(uri);
            if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
                //如果磁盤中有
                L.d(LOG_LOAD_IMAGE_FROM_DISK_CACHE, memoryCacheKey);
                loadedFrom = LoadedFrom.DISC_CACHE;

                checkTaskNotActual();//View是否被回收,是否被重用,是就拋出異常?
                //解碼成bitmap
                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;//url
                if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                   //可以磁盤緩存時就使用tryCacheImageOnDisk()下載到磁盤
                    imageFile = configuration.diskCache.get(uri);//然後再從磁盤讀
                    if (imageFile != null) {
                        //只要保存成功,url將被替換成file://類型
                        imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                    }
                }

                checkTaskNotActual();//View是否被回收,是否被重用?
                bitmap = decodeImage(imageUriForDecoding);//根據url解碼(如果是從磁盤中讀的,全部為file://開頭)

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

tryCacheImageOnDisk從磁盤中加載圖片,其實內部的核心源碼就是downloadImage(),如果指定了磁盤最大緩存尺寸,還會進行重新調整下Bitmap大小。

    private boolean tryCacheImageOnDisk() throws TaskCancelledException {
        L.d(LOG_CACHE_IMAGE_ON_DISK, memoryCacheKey);
        boolean loaded;
        try {
            loaded = downloadImage();
            if (loaded) {
                int width = configuration.maxImageWidthForDiskCache;
                int height = configuration.maxImageHeightForDiskCache;
                //如果指定了磁盤緩存尺寸大小,就調整下尺寸
                if (width > 0 || height > 0) {
                    L.d(LOG_RESIZE_CACHED_IMAGE_FILE, memoryCacheKey);
                    resizeAndSaveImage(width, height); // TODO : process boolean result
                }
            }
        } catch (IOException e) {
            L.e(e);
            loaded = false;
        }
        return loaded;
    }

downloadImage()的相關源碼如下

    private boolean downloadImage() throws IOException {
        //下載成InputStream
        //getDownloader()會根據設置獲取三種類型的下載器(基本的、禁止網絡的、慢網絡的)
        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);
            }
        }
    }

在成功下載到磁盤之後,下一步就該進行解碼了。就是執行decodeImage這個方法:

    private Bitmap decodeImage(String imageUri) throws IOException {
        ViewScaleType viewScaleType = imageAware.getScaleType();//獲取View的縮放類型
        //將uri,緩存key,下載器全部封裝成ImageDecodingInfo。
        ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
                getDownloader(), options);
        return decoder.decode(decodingInfo);//調用解碼器進行解碼
    }

加載圖片(loadImage)

介紹完displayImage後,再來看一下它的兄弟方法loadImage。可以看出內部也是調用了displayImage,只不過用了NonViewAware來包裝。

    public void loadImage(String uri, ImageSize targetImageSize, DisplayImageOptions options,
            ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        checkConfiguration();
        if (targetImageSize == null) {
            targetImageSize = configuration.getMaxImageSize();
        }
        if (options == null) {
            options = configuration.defaultDisplayImageOptions;
        }
         //使用NonViewAware來包裝
        NonViewAware imageAware = new NonViewAware(uri, targetImageSize, ViewScaleType.CROP);
        //最終也是調用了displayImage
        displayImage(uri, imageAware, options, listener, progressListener);
    }

那麼NonViewAware跟ImageViewAware有什麼區別呢?

    @Override
    public boolean setImageDrawable(Drawable drawable) { // Do nothing
        return true;
    }

    @Override
    public boolean setImageBitmap(Bitmap bitmap) { // Do nothing
        return true;
    }

可以看出,setImageDrawable和setImageBitmap不做任何事,其他方面和displayImage沒有半毛錢區別。

整個加載和顯示的流程如下圖所示:
首先通過下載器下載圖片,然後緩存到磁盤一份(可選),接著通過解碼器將流解碼成bitmap,放入內存之前先對bitmap進行預處理(可選),然後放入內存(可選),在顯示之前對bitmap進行處理(可選),最後調用顯示器來進行顯示圖片。
image_1al9lm0r7fub1pc7ldc1s1emfq9.png-224.7kB

最後

這個框架會不會對本地圖片進行磁盤緩存?
從源碼可以看出,只要你允許磁盤緩存,任何流到會寫入到磁盤內,包括本地圖片及Drawable圖片。

ImageLoader是怎麼實現多尺寸緩存的?那麼怎麼禁止多尺寸緩存?
多尺寸緩存的核心在於緩存key的格式為[imageUri]_[width]x[height],這樣每種尺寸一個key,然後放入內存中。那麼ImageLoader怎麼禁止多尺寸緩存呢?
很簡單,只需配置denyCacheImageMultipleSizesInMemory即可,那麼在存放bitmap時會截取url進行遍歷比較,如果存在,就移除舊圖片。
https://www.android5.online/Android/UploadFiles_5356/201702/2017022311364188.png

怎麼實現僅在wifi環境下加載圖片?
很簡單,下面一句代碼就行。這樣在getDownloader()就會返回禁止加載網絡圖片的下載器。

ImageLoader.getInstance().denyNetworkDownloads(true);

image_1al9mqbta153214gd1eno12gnkcg13.png-22kB
NetworkDeniedImageDownloader的相關源碼如下。
image_1al9kvonvd0a166osbr1ali1a8d9.png-42.2kB

這個框架可以在ListView的復用中自動取消任務嗎?
從源碼角度來看是可以的。許多地方都加入了checkTaskNotActual()來檢查View是否被回收或者復用。
具體判斷的源碼讀者自行閱讀即可。

怎麼針對ListView進行優化?
針對ListView添加監聽即可。

listView.setOnScrollListener(new PauseOnScrollListener(...));

PauseOnScrollListener的核心源碼如下。可以看出滾動時會停止加載圖片。
image_1al9lcqmvcuu1fccht1gi4e3qm.png-45.5kB

怎麼針對生命周期優化?
在生命周期的相關代碼中加入如下代碼即可。

ImageLoader.getInstance().resume();
ImageLoader.getInstance().pause();

該開源庫地址:https://github.com/nostra13/Android-Universal-Image-Loader
本期解讀到此結束,如有錯誤之處,歡迎指出。

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