美文网首页Android知识Android进阶之旅Android
Android 图片加载框架Universal-Image-Lo

Android 图片加载框架Universal-Image-Lo

作者: 半岛铁盒里的猫 | 来源:发表于2017-04-03 14:10 被阅读426次

    本篇文章已授权微信公众号 guolin_blog (郭霖)独家发布,来源于我在CSDN发表的一篇博文:
    http://blog.csdn.net/sinat_23092639/article/details/65936005

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

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

    如何同时加载大量图片:采用线程池优化高
    并发
    如何提高加载速度:使用内存、磁盘缓存
    如何避免OOM:加载的图片进行压缩,内存缓存具有相关的淘汰算法
    如何避免ListView的图片错位:将要加载的图片和要显示的ImageView绑定, 在请求过程中判断该ImageView当前绑定的图片是否是当前请求的图片,不是则停止当前请求。

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


    Android 图片加载框架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:任务分发器,负责分发LoadAndDisplayImageTask和ProcessAndDisplayImageTask给具体的线程池去执行。
    ImageAware:显示图片的对象,可以是ImageView等。
    ImageDownloader:图片下载器,负责从图片的各个来源获取输入流。
    Cache:图片缓存,分为MemoryCache和DiskCache两部分。
    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,之所以使用图片尺寸是因为内存缓存的图片同一张图片拥有不同尺寸的版本。

    displayImage方法第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);
        }
    

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

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

    第1行中的waitIfPaused方法:

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

    这里如果engine的pause标志位被置位为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();
    

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

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

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

    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方法的20行:

    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。

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

    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的图片加载任务从任务分发器任务记录中移除。

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

    这里注意下runTask的参数handler,它的来源在ImageLoader类的defineHandler方法:

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

    可以看到首先取显示配置中的Handler,默认为null,所以默认会执行下面的判断,如果调用该方法是在主线程,则创建一个Handler对象,于是改Handler就和主线程绑定一起了(不熟悉Hanlder的朋友可以看下我的另一篇博文:全面分析Handler消息机制
    最后会利用Handler的post方法将图片的显示传递到主线程调用~~
    图片加载过程算是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的缓存处理机制

    总结整个框架的特点:1.支持内存和磁盘两级缓存。2.配置高灵活性,包括缓存算法、线程池属性、显示选项等等。3.支持同步异步加载4.运用多种设计模式,比如模板方法、装饰者、建造者模式等等,具有很高的可扩展性可复用性。

    好了,这个开源框架就讲到这里,希望对大家有帮助,个人水平有限,阅读开源框架源码还不是很多,不足之处请帮忙纠正~~

    参考文章:

    Android 开源框架Universal-Image-Loader完全解析(三)—源代码解读
    Android Universal Image Loader 源码分析
    从源代码分析Universal-Image-Loader中的线程池

    相关文章

      网友评论

        本文标题:Android 图片加载框架Universal-Image-Lo

        本文链接:https://www.haomeiwen.com/subject/ypcgottx.html