Android 图片加载框架Universal-Image-Loader源码解析

Universal-Image-Loader(项目地址)可以说是安卓知名图片开源框架中最古老、使用率最高的一个了。一张图片的加载对于安卓应用的开发也许是件简单的事,但是如果要同时加载大量的图片,并且图片用于ListView、GridView、ViewPager等控件,如何防止出现OOM、如何防止图片错位(因为列表的View复用功能)、如何更快地加载、如何让客户端程序员用最简单的操作完成本来十分复杂的图片加载工作,成了全世界安卓应用开发程序员心头的一大难题,所幸有了Universal-Image-Loader,让这一切变得简单,从某种意义来讲,它延长了安卓开发者的寿命~

针对上述几个问题,Universal-Image-Loader可谓是兵来将挡水来土掩,见招拆招:

如何同时加载大量图片:采用线程池优化高并发

如何提高加载速度:使用内存、磁盘缓存

如何避免OOM:加载的图片进行压缩,内存缓存具有相关的淘汰算法

如何避免ListView的图片错位:将要加载的图片和要显示的ImageView绑定, 在请求过程中判断该ImageView当前绑定的图片是否是当前请求的图片,不是则停止当前请求。

首先来看下Universal-Image-Loader整体流程图:

总的来说就是:下载图片——将图片缓存在磁盘中——解码图片成为Bitmap——Bitmap的预处理——缓存在Bitmap内存中——Bitmap的后期处理——显示Bitmap

基本用法:

想必很多朋友也知道了,不过还是“抄袭”下Github说明里的使用方法:

**简单版本:**

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

// Load image, decode it to Bitmap and display Bitmap in ImageView (or any other view
//  which implements ImageAware interface)
imageLoader.displayImage(imageUri, imageView);
// Load image, decode it to Bitmap and return Bitmap to callback

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

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

Bitmap bmp = imageLoader.loadImageSync(imageUri);

**复杂版本**:

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

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

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

ImageSize targetSize = new ImageSize(80, 50);
// result Bitmap will be fit to this size

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

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

ImageSize targetSize = new ImageSize(80, 50);
// result Bitmap will be fit to this size

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

相信各位经验丰富的安卓开发者一看就心照不宣,就不必解释什么了。

和源码见面之前,先和几个重要的类打招呼:

ImageLoaderEngine:任务分发器,负责分发LoadAndDisplayImageTaskProcessAndDisplayImageTask给具体的线程池去执行。

ImageAware:显示图片的对象,可以是ImageView等。

ImageDownloader:图片下载器,负责从图片的各个来源获取输入流。

Cache:图片缓存,分为MemoryCacheDiskCache两部分。

MemoryCache:内存图片缓存,可向内存缓存缓存图片或从内存缓存读取图片。

DiskCache:本地图片缓存,可向本地磁盘缓存保存图片或从本地磁盘读取图片。

ImageDecoder:图片解码器,负责将图片输入流InputStream转换为Bitmap对象。

BitmapProcessor:图片处理器,负责从缓存读取或写入前对图片进行处理。

BitmapDisplayer:将Bitmap对象显示在相应的控件ImageAware上。

LoadAndDisplayImageTask:用于加载并显示图片的任务。

ProcessAndDisplayImageTask:用于处理并显示图片的任务。

DisplayBitmapTask:用于显示图片的任务。

其中有个全局配置的类贯穿整个框架,ImageLoaderConfiguration,可以配置的东西实在有点多:

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

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

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

        L.writeDebugLogs(builder.writeLogs);
    }

主要是图片最大尺寸、线程池、缓存、下载器、解码器等等。

经过前面那么多的铺垫,终于迎来了源码~~

整个框架的源码上万,全部讲完不可能,最好的方式还是按照加载流程走一遍,细枝末节各位可以自己慢慢研究,一旦整体把握好了,其他的一切就水到渠成,切勿只见树木不见森林,迷失在各种代码细节中~~

好了,简单点,讲代码的方式简单点,从最简单的代码切入:

imageLoader.displayImage(imageUri, imageView);

进入方法:

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

ImageAware是ImageView的包装类,持有ImageView对象的弱引用,防止ImageView出现内存泄漏发生。主要是提供了获取ImageView宽度高度和ScaleType等。

最终会执行这一个重载方法:

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);
        }
        //使用默认的Listener
        if (listener == null) {
            listener = defaultListener;
        }
        //使用默认的options
        if (options == null) {
            options = configuration.defaultDisplayImageOptions;
        }
        //uri为空
        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());
        }
        //根据uri和图片尺寸生成缓存的key
        String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
        engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

        listener.onLoadingStarted(uri, imageAware.getWrappedView());
        //从内存缓存取出Bitmap
        Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
        //命中内存缓存的相应的Bitmap
        if (bmp != null && !bmp.isRecycled()) {
            L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
            //进行Bitmap后期处理
            if (options.shouldPostProcess()) {
                //创建一个加载和显示图片任务需要的信息的对象
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                //将内存缓存中取出的Bitmap显示出来
                ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                        defineHandler(options));
                //是否同时加载,否则使用线程池加载
                if (options.isSyncLoading()) {
                    displayTask.run();
                } else {
                    engine.submit(displayTask);
                }
            } else {
                //不进行图片处理则直接显示在ImageView
                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);
            }
        }
    }

这个方法基本描绘出了整个图片加载的流程,重要的地方已经加上注释。

第8行的listener就是上面基本使用说明复杂版本的进度回调接口ImageLoadingListener,大家看下就知道,如果没有配置的话设置为默认,而默认其实啥都没做,方法都是空实现。

第12行也是类似地如果没有配置DisplayImageOptions,即图片显示的配置项,则取默认的。

看下这个图片显示配置类可以配置什么:

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

配置是否内存磁盘缓存以及设置图片预处理和后期处理器(预处理和后期处理器默认为null,留给客户端程序员扩展)等。

displayImage方法第27行里,如果没有专门设置targetSize,即指定图片的尺寸,就取ImageAware的宽高,即包装在里面的ImageView的宽高,如果得到的宽高小于等于0,则设置为最大宽高,如果没有设置内存缓存的最大宽高(maxImageWidthForMemoryCache,maxImageHeightForMemoryCache),则为屏幕的宽高。

然后根据图片uri和图片的尺寸生成一个内存缓存的key,之所以使用图片尺寸是因为内存缓存的图片同一张图片拥有不同尺寸的版本。

第33行中:

engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);

engine指的是任务分发器 ImageLoaderEngine,它持有一个HashMap,作为记录图片加载任务使用,一旦任务停止加载或者加载完毕则会删除对应的任务引用。prepareDisplayTaskFor方法正是将任务的引用添加到该HashMap中,这里以内存缓存的Key为键。

接下来如果拿到的图片需要做后期处理,则创建一个图片显示信息的对象,然后以之前创建的图片显示信息的对象为参数之一创建一个处理显示任务对象ProcessAndDisplayImageTask(实现Runnable),如果是指定同步加载则直接调用它的run方法,不是则将其添加到任务分发器ImageLoaderEngine的线程池中异步执行。

若不需要对图片进行后期处理则调用Displayer去显示显示图片,默认的Displayer为SimpleBitmapDisplayer,只是简单地显示图片到指定的ImageView中。

如果拿不到内存缓存的对应图片,则像之前创建加载显示图片任务对象LoadAndDisplayImageTask,然后执行该任务去磁盘缓存或者网络加载图片。

接下来的关键就是看下LoadAndDisplayImageTask的run方法怎么执行的,就知道整个图片加载怎么运行的了。

public void run() {
        //当前请求是否需要暂停等待,等待期间被中断则停止请求
        if (waitIfPaused()) return;
        //如果当前请求延时加载,延时期间被中断则停止请求
        if (delayIfNeed()) return;
        //得到与图片uri绑定的锁
        ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
        L.d(LOG_START_DISPLAY_IMAGE_TASK, memoryCacheKey);
        if (loadFromUriLock.isLocked()) {
            L.d(LOG_WAITING_FOR_IMAGE_LOADED, memoryCacheKey);
        }
        //锁与图片uri对应是为了防止同时加载同一张图片
        loadFromUriLock.lock();
        Bitmap bmp;
        try {
            //检查当前任务的对应Imageview是否被回收或者复用,是则抛TaskCancelledException结束当前任务
            checkTaskNotActual();
            //疑问:再一次尝试从内存缓存中获取是因为可能此时其他任务加载了相同的图片?
            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {
                //内存缓存获取不到,尝试到磁盘缓存或者网络获取
                bmp = tryLoadBitmap();
                if (bmp == null) return; // listener callback already was fired

                checkTaskNotActual();
                //检查任务是否被Interrupt
                checkTaskInterrupted();
                //图片是否需要预处理
                if (options.shouldPreProcess()) {
                    L.d(LOG_PREPROCESS_IMAGE, memoryCacheKey);
                    bmp = options.getPreProcessor().process(bmp);
                    if (bmp == null) {
                        L.e(ERROR_PRE_PROCESSOR_NULL, memoryCacheKey);
                    }
                }

                if (bmp != null && options.isCacheInMemory()) {
                    L.d(LOG_CACHE_IMAGE_IN_MEMORY, memoryCacheKey);
                    //添加到内存缓存
                    configuration.memoryCache.put(memoryCacheKey, bmp);
                }
            } else {
                loadedFrom = LoadedFrom.MEMORY_CACHE;
                L.d(LOG_GET_IMAGE_FROM_MEMORY_CACHE_AFTER_WAITING, memoryCacheKey);
            }
            //是否需要后期处理
            if (bmp != null && options.shouldPostProcess()) {
                L.d(LOG_POSTPROCESS_IMAGE, memoryCacheKey);
                bmp = options.getPostProcessor().process(bmp);
                if (bmp == null) {
                    L.e(ERROR_POST_PROCESSOR_NULL, memoryCacheKey);
                }
            }
            //再次判断是否要停止当前任务
            checkTaskNotActual();
            checkTaskInterrupted();
        } catch (TaskCancelledException e) {
            fireCancelEvent();
            return;
        } finally {
            loadFromUriLock.unlock();
        }

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

run方法不长,就直接展示了。

这里第3、4行:

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

第1行中的waitIfPaused方法:

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

这里如果engine的pause标志位被置位为true,则会调用engine.getPauseLock()对象(其实就是一个普通的Object对象)的wait方法使当前线程暂停运行。为什么要这样呢?看下engine的pause标志位什么时候会被置位为true,终于在PauseOnScrollListener的重写方法onScrollStateChanged中找到:

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

我们知道ListView滑动时候加载显示图片会使得滑动有卡顿现象。这就是在列表滑动时暂停加载的方式,只要给ListView设置:

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

就可以通过在滑动时候暂停请求任务从而防止ListView滑动带来的卡顿。一旦ListView在滑动状态,所有新增的图片请求都会暂停等待列表滑动停止调用imageLoader.resume()方法唤醒暂停的线程继续加载请求。

waitIfPaused()在线程等待过程中被中断(线程池被停止运行)的时候会返回true,从而终止任务的run方法终止加载请求。它的返回值还由isTaskNotActual方法决定,该方法主要判断显示图片的ImageView是否被回收以及所在的列表项是否被复用,是的话也是终止加载任务。

delayIfNeed方法则是在任务需要延迟的时候让当前线程sleep一会,同样也是遇到中断返回true终止任务。

接下来12行

loadFromUriLock.lock();

这里相同uri的图片具有同一把锁,这也就意味着相同图片的请求同一时间只有一个在网络或者磁盘加载,有效避免了重复的网络或者磁盘缓存加载,一旦加载完其余请求都会从内存缓存中加载图片。

然后尝试从内存缓存中取出图片,一旦成功得到图片,则经过请求是否有效的相关判断之后,创建一个DisplayBitmapTask将图片显示到对应的ImageView中。

关键是如果内存取不到图片的情况,这时候就得看21行:

bmp = tryLoadBitmap();

该方法就是去磁盘缓存取图片,如果没有则取网络图片。

方法也不长,所以也直接拷贝过来:

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;
                //检查当前任务的对应Imageview是否被回收或者复用,是则抛TaskCancelledException结束当前任务
                checkTaskNotActual();
                //解码得到图片
                bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
            }
            if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
                L.d(LOG_LOAD_IMAGE_FROM_NETWORK, memoryCacheKey);
                loadedFrom = LoadedFrom.NETWORK;

                String imageUriForDecoding = uri;
                //磁盘缓存获取失败则从网络获取
                if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                    imageFile = configuration.diskCache.get(uri);
                    if (imageFile != null) {
                        imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                    }
                }
                //仍然检查当前任务的对应Imageview是否被回收或者复用,是则抛TaskCancelledException结束当前任务
                checkTaskNotActual();
                //tryCacheImageOnDisk将图片保存到文件系统。
                //接下来将图片按需求裁剪等处理后加载到内存
                //所以文件保存的是原图或者缩略图,内存保存的是根据ImageView大小、scaletype、方向处理过得图片
                bitmap = decodeImage(imageUriForDecoding);

                if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
                    fireFailEvent(FailType.DECODING_ERROR, null);
                }
            }
        } catch (IllegalStateException e) {
            fireFailEvent(FailType.NETWORK_DENIED, null);
        } catch (TaskCancelledException e) {
            throw e;
        } catch (IOException e) {
            L.e(e);
            fireFailEvent(FailType.IO_ERROR, e);
        } catch (OutOfMemoryError e) {
            L.e(e);
            fireFailEvent(FailType.OUT_OF_MEMORY, e);
        } catch (Throwable e) {
            L.e(e);
            fireFailEvent(FailType.UNKNOWN, e);
        }
        return bitmap;
    }

首先第四行是尝试从磁盘文件系统中寻找对应的图片,如果有的话,调用:

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

进行解码得到对应的Bitmap对象,注意这里将文件路径使用Scheme.FILE进行包装,由于Universal-Image-Loader加载图片有多个来源,比如网络,文件系统,项目文件夹asset等,所以解码的时候进行一个包装,方便解码器BaseImageDecoder在解码的时候识别来源进行相应的获取流处理。

Scheme总共有以下几个:

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

这里的decodeImage方法:

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

根据ImageView的ScaleType以及targetSize等生成一个ImageDecodingInfo对象,传入解码器decoder的decode方法中。

这里的解码器默认为BaseImageDecoder,decode方法:

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 {
            //根据图片的属性生成一个ImageFileInfo对象,指定尺寸和旋转处理
            imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
            // 还原stream
            // 什么叫还原? 因为通过上面的操作,我们的stream游标可能已经
            // 已经不再首部,这时再去读取,还是继续读取,造成了信息不完整
            // 所以这里需要reset一下
            imageStream = resetStream(imageStream, decodingInfo);
            Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
            //解码获取Bitmap对象
            decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
        } finally {
            IoUtils.closeSilently(imageStream);
        }

        if (decodedBitmap == null) {
            L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
        } else {
            //对Bitmap进行裁剪和旋转等操作
            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                    imageInfo.exif.flipHorizontal);
        }
        return decodedBitmap;
    }

这里具体如何裁剪和旋转的代码就不介绍了,有兴趣大家可以去源码看下。最后得到的图片就可以直接使用显示图片任务DisplayBitmapTask将图片显示在ImageView上了。

假如磁盘也获取不到图片,那就需要到网络加载了。

tryLoadBitmap方法的19行:

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

判断是否需要磁盘缓存,需要的话,进入tryCacheImageOnDisk方法:

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方法从网络加载图片,保存到文件系统中。然后判断是否配置了maxImageWidthForDiskCache和maxImageHeightForDiskCache选项,有的话对保存在文件系统的图片进行裁剪压缩得到缩略图。

然后在tryLoadBitmap方法第21行中,取出对应磁盘缓存的图片文件路径,然后使用解码器将图片解码文件为Bitmap。

以上讨论的是需要磁盘缓存的情况,如果不需要呢?看下第30行:

bitmap = decodeImage(imageUriForDecoding);

imageUriForDecoding是图片的uri,该方法上面已经讲过,最终是通过流解码为一个Bitmap对象,也就是从网络加载图片到内存,而前面讲的是从文件系统加载图片到内存。

这里需要注意的是,磁盘缓存的是原图或者其缩略图,内存缓存的是根据ImageView裁剪的图片。

拿到Bitmap对象,然后就是对图片进行预处理了(如果配置为需要的话),处理完毕后添加到内存缓存,然后进行后期处理(如果需要的话),最后将Bitmap交给显示任务DisplayBitmapTask处理。

DisplayBitmapTask的run就十分简单了:

public void run() {
        if (imageAware.isCollected()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else if (isViewWasReused()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else {
            L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);
            displayer.display(bitmap, imageAware, loadedFrom);
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
        }
    }

在确保ImageView没有被回收和复用的情况下,交给显示器displayer处理,displayer默认为SimpleBitmapDisplayer,SimpleBitmapDisplayer的display其实就是直接调用ImageView的setBitmapImage方法将图片显示上去。然后将该ImageView的图片加载任务从任务分发器任务记录中移除。

图片加载过程算是Over了~~

这里谈下一些注意要点:

1.关于线程池:

当加载显示任务要执行的时候,任务分发器的submit对应方法是:

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

其中任务分发线程池taskDistributor为缓存线程池(CacheThreadPoll),用于判断任务请求的图片是否缓存在磁盘中以及分发任务给执行任务线程池。两个实际执行任务的线程池taskExecutorForCachedImages和taskExecutor为可配置的线程池,默认核心和工作线程数都为3,默认配置采用的先进先出的队列,如果是列表图片建议配置为先进后出队列。两个线程池可以根据任务性质自定义配置扩展其他属性。

为什么要专门分为三个线程池而不是一个线程池执行所有任务呢?如果所有任务运行在一个线程池中,所有的任务就都只能采取同一种任务优先级和运行策略。显然果要有更好的性能,在线程数比较多并且线程承担的任务不同的情况下,App中最好还是按任务的类别来划分线程池。

一般来说,任务分为CPU密集型任务,IO密集型任务和混合型任务。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。具体可参见从源代码分析Universal-Image-Loader中的线程池

2.关于下载器:

在加载显示任务类LoadAndDisplayImageTask的获取下载器方法中:

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

默认网络良好的情况下使用BaseImageDownloader。在无网络情况下使用NetworkDeniedImageDownloader,当判断到图片源为网络时抛出异常停止请求。网络不佳情况下使用SlowNetworkDownloader,主要通过重写FilterInputStream的 skip(long n) 函数解决在慢网络情况下 decode image 异常的 Bug。(具体解决原理本人也不太清楚,求教。。)

另外关于缓存机制具体可以看下从源代码分析Android-Universal-Image-Loader的缓存处理机制

好了,这个开源框架就讲到这里,希望对大家有帮助,不足之处请纠正~~

参考文章:

Android 开源框架Universal-Image-Loader完全解析(三)—源代码解读

Android Universal Image Loader 源码分析

从源代码分析Universal-Image-Loader中的线程池

时间: 2024-10-20 02:01:58

Android 图片加载框架Universal-Image-Loader源码解析的相关文章

Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/53939176 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每天都有文章更新. 在本系列的上一篇文章中,我们学习了Glide的基本用法,体验了这个图片加载框架的强大功能,以及它非常简便的API.还没有看过上一篇文章的朋友,建议先去阅读 Android图片加载框架最全解析(一),Glide的基本用法 . 在多数情况下,我们想要在界面上加载并展示一

一起写一个Android图片加载框架

本文会从内部原理到具体实现来详细介绍如何开发一个简洁而实用的Android图片加载缓存框架,并在内存占用与加载图片所需时间这两个方面与主流图片加载框架之一Universal Image Loader做出比较,来帮助我们量化这个框架的性能.通过开发这个框架,我们可以进一步深入了解Android中的Bitmap操作.LruCache.LruDiskCache,让我们以后与Bitmap打交道能够更加得心应手.若对Bitmap的大小计算及inSampleSize计算还不太熟悉,请参考这里:高效加载Bit

Android动态加载框架DL的架构与基本原理解析

转载请注明出处,本文来自[ Mr.Simple的博客 ]. 我正在参加博客之星,点击这里投我一票吧,谢谢~ 前言 最近这一两年,Android App使用插件化技术开发的数量越来越大,其实还是业务地快速膨胀导致,需求越来越多,App越来越臃肿.虽然手机的内存空间不断地的增大,但是太大的安装包给用户也造成了心理压力.于是大家都会想到插件化的开发方式,把App做成一个平台,而不是一个独立的app.平台上可以集成各种各样的功能,功能模块也插件的形式添加进来,这些插件不需要安装,只需要用户按需下载到某个

Android图片异步加载框架Universal Image Loader的源码分析

项目地址:https://github.com/nostra13/android-universal-image-loader 1. 功能介绍 1.1 Android Universal Image Loader Android Universal Image Loader 是一个强大的.可高度定制的图片缓存,本文简称为UIL. 简单的说 UIL 就做了一件事--获取图片并显示在相应的控件上. 1.2 基本使用 1.2.1 初始化 添加完依赖后在Application或Activity中初始化I

Android图片加载框架 Universal-Image-Loader 妙用

Android开源框架Universal-Image-Loader就像图片加载守护者,为我们提供了丰富的功能特性: (1)多线程加载图像(异步或同步): (2)高度可定制化imageloader配置(线程池.图片下载器.解码器.内存和磁盘缓存.显示图像选项等): (3)每一个显示图像有许多自定义选项(存根图片,缓存开关,解码选项,位图处理和显示等): (4)支持内存和磁盘上的图像缓存(设备的文件系统和SD卡): (5)监听加载过程(包括下载进度): 下来我们详解如何配置使用Universal-I

初探Google推荐Android图片加载框架Glide

简介 运行Demo 安装依赖包 现在编译好的依赖 Gradle Maven Proguard 简单使用 设置暂未图和加载失败图 加载封面图 从其他路径加载图片 加载图片到其他控件 调试信息 开启请求响应信息 开启工作流日志 简介 现在在Android上加载图片的框架都已经烂大街了,所以我们这里也不说谁好谁坏,当然也不做比较了,因为得出的结果都是片面的,没有谁好谁坏只有适不适合需求罢了 起因是在泰国举行的谷歌开发者论坛上,谷歌为我们介绍了一个叫Glide 的图片加载库,作者是bumptech.这个

Android 使用图片异步加载框架Universal Image Loader的问题

使用的Jar包 问题:        optionsm = new DisplayImageOptions.Builder() .displayer(new RoundedBitmapDisplayer(20)) .build();   这里.displayer(new RoundedBitmapDisplayer(20))的时候设置圆角报错,.displayer(new SimpleBitmapDisplayer())这样正常显示图片就不报错. 版权声明:本文为博主原创文章,未经博主允许不得转

Android中图片加载框架Glide解析2----从源码的角度理解Glide的执行流程

转载地址:http://blog.csdn.net/guolin_blog/article/details/53939176 在本系列的上一篇文章中,我们学习了Glide的基本用法,体验了这个图片加载框架的强大功能,以及它非常简便的API.还没有看过上一篇文章的朋友,建议先去阅读 Android图片加载框架最全解析(一),Glide的基本用法 . 在多数情况下,我们想要在界面上加载并展示一张图片只需要一行代码就能实现,如下所示: Glide.with(this).load(url).into(i

优雅地实现Android主流图片加载框架封装,可无侵入切换框架

项目开发中,往往会随着需求的改变而切换到其它图片加载框架上去.如果最初代码设计的耦合度太高,那么恭喜你,成功入坑了.至今无法忘却整个项目一行行去复制粘贴被支配的恐惧.:) 那么是否存在一种方式 能够一劳永逸地解决这个痛点呢?下面我们来分析一下图片加载框架面对的现状和解决思路. 问题现状 一个优秀的框架一般在代码设计的时候已经封装很不错了,对于开发者而言框架的使用也是很方便,但是为什么说我们往往还要去做这方面的框架封装呢?原因很简单,实际项目开发中,我们不得不面对着日新月异的需求变化,想要在这个变