图片加载框架Android-Universal-Image-Loader使用及解析

这是一个在github上很流行的异步图片加载框架,简单方便实用,主要采用二级缓存来加载缓存图片(不仅仅是网上的,本地也可以),图片加载方式如下。
这里写图片描述
这里写图片描述

每次显示图片时,先读取缓存,当缓存没有时,则读取本地数据,只有以上两者都没有的时候才从网络获取。
图片的二级缓存本质也是使用DiskLruCache进行读取,这个东西可以看看郭大神的介绍http://blog.csdn.net/guolin_blog/article/details/28863651,下面来看一下如何使用它。

使用

只需两步,就能轻松实现图片的加载,


  • 在Application设置全局参数。
public class UILApplication extends Application {

    @Override
public void onCreate() {
    super.onCreate();

    initImageLoader(getApplicationContext());
 }

public static void initImageLoader(Context context) {

    ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
       config.threadPriority(Thread.NORM_PRIORITY - 2)//设置线程的优先权,0-10
      .denyCacheImageMultipleSizesInMemory()//设置之后缓存同一URI不同尺寸的图片时,只缓存最后加载的那一张
      .diskCacheFileNameGenerator(new Md5FileNameGenerator())//图片的命名方式
      .diskCacheSize(50 * 1024 * 1024) // 磁盘可以保存多大的图片
      .tasksProcessingOrder(QueueProcessingType.LIFO)//设置图片处理的顺序为后进先出,默认为FIFO
      .writeDebugLogs(); // 输出log

      //设置完ImageLoaderConfiguration之后,调用Imageloader的init()方法初始化
    ImageLoader.getInstance().init(config.build());
   }
}

通过建造者模式设置ImageLoaderConfiguration的属性,然后调用ImageLoader初始化。

  • 显示图片
    接下来,只需要在调用displayImage方法,来显示图片
    //listener 加载周期的监听
    //progressListener 加载进度监听
    ImageLoader.getInstance().displayImage(String uri, ImageView imageView, DisplayImageOptions options,ImageLoadingListener listener, ImageLoadingProgressListener progressListener)

注意,上面有一个参数DisplayImageOptions ,顾名思义是显示这个图片的参数设置,我们需要自定义它

DisplayImageOptions options = new DisplayImageOptions.Builder()
      .showImageOnLoading(R.drawable.ic_stub)//加载图片时要显示的图片
      .showImageForEmptyUri(R.drawable.ic_empty)//空的url时显示的图片
      .showImageOnFail(R.drawable.ic_error)//加载失败时显示的图片
      .cacheInMemory(true)//是否缓存
      .cacheOnDisk(true)//是否存到磁盘
      .considerExifParams(true)//是否考虑exif格式
      .displayer(new RoundedBitmapDisplayer(20))//图片展示的形状

只需要简单的设置以上两步,就能实现高效的图片加载了。点我,进入官方地址供下载demo

源码实现

整个使用过程中除了displayImage,其他的都是参数配置。现在来解析displayImage究竟怎么实现的。

public synchronized void init(ImageLoaderConfiguration configuration) {
        if (configuration == null) {
            throw new IllegalArgumentException(ERROR_INIT_CONFIG_WITH_NULL);
        }
        if (this.configuration == null) {
            L.d(LOG_INIT_CONFIG);
            engine = new ImageLoaderEngine(configuration);
            this.configuration = configuration;
        } else {
            L.w(WARNING_RE_INIT_CONFIG);
        }
    }

在Application 的init初始化中.也就是赋值了全局变量configuration ,和new了一个ImageLoaderEngine。
ImageLoaderEngine也就是一个线程池类,还记得在ImageLoaderConfiguration 中有taskExecutor(Executor executor) 和taskExecutorForCachedImages(Executor executorForCachedImages)方法吗,该类就在这里操作这两个线程池,我们一般不定义那两个方法,使用默认方法构造它们。

//一般使用这个方法,将imageView包装成ImageViewAware
public void displayImage(String uri, ImageView imageView, DisplayImageOptions options,
            ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        displayImage(uri, new ImageViewAware(imageView), options, listener, progressListener);
}
//最终实现的方法
public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener) {
        //检查ImageLoaderConfiguration 是否为空
        checkConfiguration();
        if (imageAware == null) {
            throw new IllegalArgumentException(ERROR_WRONG_ARGUMENTS);
        }
        if (listener == null) {
            listener = defaultListener;
        }
        if (options == null) {
            options = configuration.defaultDisplayImageOptions;
        }

        if (TextUtils.isEmpty(uri)) {
            //清除map中key为imageAware的hashcode。它和缓存图片的唯一值组成键值对
            engine.cancelDisplayTaskFor(imageAware);
            //回调
            listener.onLoadingStarted(uri, imageAware.getWrappedView());
            //shouldShowImageForEmptyUri在DisplayImageOptions 设置
            if (options.shouldShowImageForEmptyUri()) {
                imageAware.setImageDrawable(options.getImageForEmptyUri(configuration.resources));
            } else {
                imageAware.setImageDrawable(null);
            }
            listener.onLoadingComplete(uri, imageAware.getWrappedView(), null);
            return;
        }
        //一般没有设置目标图片大小,故设置为当前imageview的宽高
        if (targetSize == null) {
            targetSize = ImageSizeUtils.defineTargetSizeForView(imageAware, configuration.getMaxImageSize());
        }
        //根据url和目标图片的宽高组成唯一值
        String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
        //保存到map
        engine.prepareDisplayTaskFor(imageAware, memoryCacheKey);
        //回调
        listener.onLoadingStarted(uri, imageAware.getWrappedView());
        //查看缓存是否有目标图片。
        Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
        if (bmp != null && !bmp.isRecycled()) {
            L.d(LOG_LOAD_IMAGE_FROM_MEMORY_CACHE, memoryCacheKey);
            //shouldPostProcess()由DisplayImageOptions 设置,自定义一个图片处理器,用来
            //下载和解析图片。一般使用系统默认的,他会下载并将图片截到<=imageview长宽。
            if (options.shouldPostProcess()) {
                ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                        options, listener, progressListener, engine.getLockForUri(uri));
                ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                        defineHandler(options));                
                if (options.isSyncLoading()) {//同步加载,在主线程执行处理方法,默认不同步
                    displayTask.run();
                } else {//添加到线程池执行                  
                    engine.submit(displayTask);
                }
            } else {
                //一般没自定义的情况都执行这里,缓存有,是因为图片经过压缩处理才加载到缓存的,所以不需
                //要解析。直接显示
                //调用DisplayBitmapTask 的run方法显示图片,
                //本质也是调用DisplayImageOptions 设置的display(BitmapDisplay display)显示图片
                //框架为BitmapDisplay 接口实现了多个显示形状类
                //位于core/display包
                options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);//回调
            }
        } else {//接下来是缓存没有的情况下,
            if (options.shouldShowImageOnLoading()) {//DisplayImageOptions 设置,加载时显示图片
                imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
            } else if (options.isResetViewBeforeLoading()) {//DisplayImageOptions设置,加载前置为空
                imageAware.setImageDrawable(null);
            }
            //调用LoadAndDisplayImageTask run()方法,执行图片加载和展示。
            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);
            }
        }
    }

由以上方法可知道,先读取缓存,缓存有直接显示,没有的话到LoadAndDisplayImageTask 执行下一步。
接下来,就来看看LoadAndDisplayImageTask 的run方法,看看是怎么执行的。

public void run() {
        //框架为每个url都配备了一个ReentrantLock ,防止同一时间加载同个url多次
        ReentrantLock loadFromUriLock = imageLoadingInfo.loadFromUriLock;
        loadFromUriLock.lock();
        Bitmap bmp;
        try {
            checkTaskNotActual();//检查imageview是否被回收

            bmp = configuration.memoryCache.get(memoryCacheKey);
            if (bmp == null || bmp.isRecycled()) {
                //缓存不存在,加载图片
                bmp = tryLoadBitmap();
                if (bmp == null) return; // listener callback already was fired

                checkTaskNotActual();
                checkTaskInterrupted();
                //注意和shouldPostProcess不同,DisplayImageOptions设置
                if (options.shouldPreProcess()) {
                    bmp = options.getPreProcessor().process(bmp);
                }
                //是否要保存到缓存。取决于DisplayImageOptions 设置,
                if (bmp != null && options.isCacheInMemory()) {
                    configuration.memoryCache.put(memoryCacheKey, bmp);
                }
            } else {//缓存有的话,修改个标示而已
                loadedFrom = LoadedFrom.MEMORY_CACHE;
            }
            //DisplayImageOptions设置
            if (bmp != null && options.shouldPostProcess()) {
                bmp = options.getPostProcessor().process(bmp);              
            }
        } catch (TaskCancelledException e) {
            fireCancelEvent();
            return;
        } finally {
            loadFromUriLock.unlock();
        }
        //设置要显示的图片信息
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        //调用DisplayBitmapTask 的run方法显示图片,
        //本质也是调用DisplayImageOptions 设置的display(BitmapDisplay display)显示图片
        //框架为BitmapDisplay 接口实现了多个显示形状类
        //位于core/display包
        runTask(displayBitmapTask, syncLoading, handler, engine);
    }

上面的实现大体上和display()方法差不多,也是拿到图片并且显示,位于的不同是那图片的方法tryLoadBitmap(),看一下是怎么实现的。

//内存没有图片的时候才会调用该方法的。
private Bitmap tryLoadBitmap() throws TaskCancelledException {
        Bitmap bitmap = null;
        //所以一开始就是去磁盘拿
        File imageFile = configuration.diskCache.get(uri);
        if (imageFile != null && imageFile.exists() && imageFile.length() > 0) {
            //草,终于发现,修改标示,给图片url加个file://前缀,代表本地文件
            //突然发现,拿到图片像素很大,加载的时候内存哗啦啦的掉
            //不行,加载前得缩小它
            //decodeImage有加载图片和裁剪图片的功效
            loadedFrom = LoadedFrom.DISC_CACHE;
            bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
        }
        if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
            //草你大爷,还是没有,上网获取,改个标示
            loadedFrom = LoadedFrom.NETWORK;
            String imageUriForDecoding = uri;
            if (options.isCacheOnDisk() && tryCacheImageOnDisk()) {
                imageFile = configuration.diskCache.get(uri);
                if (imageFile != null) {
                    imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                }
            }

            checkTaskNotActual();
            //解析图片
            bitmap = decodeImage(imageUriForDecoding);

            if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) {
                fireFailEvent(FailType.DECODING_ERROR, null);
            }
        }

        return bitmap;
    }

tryLoadBitmap也就是根据url获取到bitmap的实例,想知道怎么获取的,就看看decodeImage是怎么写的,妈的跳来跳去烦死了

private Bitmap decodeImage(String imageUri) throws IOException {
        //获取图片缩放方式,这里他把所有缩放方法都归为两类CENTER_INSIDE和CROP
        //个人觉得是1等比例缩放到最长边等于imageview的一边
        //2等比例缩放到最短边等于imageview的一边,导致会裁剪到最长边的一部分
        ViewScaleType viewScaleType = imageAware.getScaleType();
        ImageDecodingInfo decodingInfo = new ImageDecodingInfo(memoryCacheKey, imageUri, uri, targetSize, viewScaleType,
                getDownloader(), options);
        //然后调用框架的解析类,
        //这里只有一个解析类位于包core/decode/BaseImageDecoder.java
        //ImageLoaderConfiguration会默认设置的
        return decoder.decode(decodingInfo);
    }

现在,所以的矛头都指向了BaseImageDecoder(默认)类,草蛋的搞完就收工了。

public Bitmap decode(ImageDecodingInfo decodingInfo) throws IOException {
        Bitmap decodedBitmap;
        ImageFileInfo imageInfo;
        //根据url获取输入流,也就是调用core/download/BaseImageDownloader.java
        //这个下载类的getStream方法来实现下载,可以去该类看看
        InputStream imageStream = getImageStream(decodingInfo);
        if (imageStream == null) {
            return null;
        }
        try {
            //下面这段代码想表达的意思,设置BitmapFactory.Option的inSampleSize
            //减少加载时内存的消耗,
            imageInfo = defineImageSizeAndRotation(imageStream, decodingInfo);
            imageStream = resetStream(imageStream, decodingInfo);
            Options decodingOptions = prepareDecodingOptions(imageInfo.imageSize, decodingInfo);
            decodedBitmap = BitmapFactory.decodeStream(imageStream, null, decodingOptions);
        } finally {
            IoUtils.closeSilently(imageStream);
        }

        if (decodedBitmap == null) {
            L.e(ERROR_CANT_DECODE_IMAGE, decodingInfo.getImageKey());
        } else {
            //在这里设置图片的旋转和缩放,inSampleSize不一定缩放到满意的大小
            decodedBitmap = considerExactScaleAndOrientatiton(decodedBitmap, decodingInfo, imageInfo.exif.rotation,
                    imageInfo.exif.flipHorizontal);
        }
        return decodedBitmap;
    }

在这里需要将一下prepareDecodingOptions方法

protected Options prepareDecodingOptions(ImageSize imageSize, ImageDecodingInfo decodingInfo) {
        //这个ImageScaleType 在DisplayImageOptions里面设置。
        //默认是ImageScaleType.IN_SAMPLE_POWER_OF_2这个参数的含义是什么,看下面
        ImageScaleType scaleType = decodingInfo.getImageScaleType();
        int scale;
        if (scaleType == ImageScaleType.NONE) {
            scale = 1;
        } else if (scaleType == ImageScaleType.NONE_SAFE) {
            scale = ImageSizeUtils.computeMinImageSampleSize(imageSize);
        } else {
            ImageSize targetSize = decodingInfo.getTargetSize();
            //IN_SAMPLE_POWER_OF_2的话powerOf2 设为true
            boolean powerOf2 = scaleType == ImageScaleType.IN_SAMPLE_POWER_OF_2;
            //关于inSampleSize 的计算都在下面的方法中,咬咬牙进去看一下
            scale = ImageSizeUtils.computeImageSampleSize(imageSize, targetSize, decodingInfo.getViewScaleType(), powerOf2);
        }
        Options decodingOptions = decodingInfo.getDecodingOptions();
        decodingOptions.inSampleSize = scale;
        return decodingOptions;
    }

计算BitmapFactory.Option inSampleSize 的时候有原图片长宽已经目标图片长宽,目标图片时根封装ImageView的ImageViewAmare计算出来的,通过这些信息,计算出符合的图片长宽

public static int computeImageSampleSize(ImageSize srcSize, ImageSize targetSize, ViewScaleType viewScaleType,
            boolean powerOf2Scale) {
        final int srcWidth = srcSize.getWidth();
        final int srcHeight = srcSize.getHeight();
        final int targetWidth = targetSize.getWidth();
        final int targetHeight = targetSize.getHeight();

        int scale = 1;
        //这个viewScaleType是在ImageViewAware中定义,他将图片的scaletype只是分成两类
        switch (viewScaleType) {
            //第一类,等比例缩放图片长宽最大值小于等于imageview长宽,这样图片会全部显示,
            //imageview不一定被全部占用
            case FIT_INSIDE:
                //如果设置ImageScaleType.IN_SAMPLE_POWER_OF_2的话,inSampleSize 缩放值为2的平方数
                //以5*10的imageview显示100*100的图片为例,如果设置了,scale 为16,没有设置的话,scale 为20
                if (powerOf2Scale) {
                    final int halfWidth = srcWidth / 2;
                    final int halfHeight = srcHeight / 2;
                    while ((halfWidth / scale) > targetWidth || (halfHeight / scale) > targetHeight) { 
                        scale *= 2;
                    }
                } else {
                    scale = Math.max(srcWidth / targetWidth, srcHeight / targetHeight); // max
                }
                break;
            //第二类,等比例缩放图片长宽最小值小于等于imageview长宽,这样imageview会全部占用,
            //图片可能会被裁剪部分,默认使用这个参数
            case CROP:
                //以5*10的imageview显示100*100的图片为例,如果设置了,scale 为8,没有设置的话,scale 为10
                if (powerOf2Scale) {
                    final int halfWidth = srcWidth / 2;
                    final int halfHeight = srcHeight / 2;
                    while ((halfWidth / scale) > targetWidth && (halfHeight / scale) > targetHeight) { 
                        scale *= 2;
                    }
                } else {
                    scale = Math.min(srcWidth / targetWidth, srcHeight / targetHeight); // min
                }
                break;
        }

        if (scale < 1) {
            scale = 1;
        }
        //根据最大允许长宽值在设置一下scale 
        scale = considerMaxTextureSize(srcWidth, srcHeight, scale, powerOf2Scale);

        return scale;
    }

通过计算得出inSampleSize ,就能大幅度减少图片加载消耗的内存。
注意有ViewScaleType 和 ImageScaleType ,ViewScaleType 是在DisplayImageOptions里面设置。
用来决定decodingOptions.inSampleSize的计算方法,而ImageScaleType 是在ImageViewAware中定义,用来决定scaleType缩放类型,只有等比例缩放和等比例裁剪.

图片的大致加载流程就这样结束了,当然还有很多小细节没有提到,但我已疲软,只能如此了。


总结

  • 内存缓存
    虽然是自己写的,和LruCache很像,也是使用LinkedHashMap来实现Lru缓存,系统默认使用,如想使用其他缓存方式,可在cache/memory/包查看
  • 磁盘缓存
    也就是使用DiskLruCache,也是使用LinkedHashMap来实现Lru缓存,系统默认使用,如想使用其他缓存方式,可在cache/memory/包查看

  • DisplayImageOption方法介绍

    showImageOnLoading(int/drawable) 加载的时候显示的图片

    showImageForEmptyUri(int/drawable) url为空时显示的图片

    showImageOnFail(int/drawable) 加载失败显示的图片

    resetViewBeforeLoading(boolean) 默认为false,当没有设置showImageOnLoading的时候生效,加载图片中的时候imageview图片是否设为空

    cacheInMemory(boolean) 默认为false 是否缓存到内存

    cacheOnDisk(boolean cacheOnDisk) 默认为false 是否缓存到磁盘

    imageScaleType(ImageScaleType) 设置scaletype,与图片的inSampleSize由关系, 默认为 ImageScaleType.IN_SAMPLE_POWER_OF_2,将inSampleSiz设置为2的平方数,一般默认值

    bitmapConfig(Bitmap.Config) 图片config默认Bitmap.Config#ARGB_8888

    decodingOptions(Options ) 设置图片Option,一般使用imageScaleType配置来动态设置。

    delayBeforeLoading(int ) 加载延时

    extraForDownloader(Object ) 在ImageDownloader.getStream调用这个参数,但没使用到它,不知何意

    considerExifParams(boolean ) 是否设置Exif格式,关于该格式可以百度之

    preProcessor(BitmapProcessor) 自定义个图片处理Process,在LoadAndDisplayImageTask 加载完图片之后(下载+设置大小)会调用该方法,修改完之后放到内存中

    postProcessor(BitmapProcessor ) 同上,不同的是内存中已经存在,只是拿出来临时修改

    displayer(BitmapDisplayer ) 设置imageview显示形状 ,在core/display/包可看类型,默认正常形状加载

    syncLoading(boolean ) 加载的时候是否同步,默认异步加载

    handler(Handler ) 自定义一个handle来显示图片,默认不需要

  • ImageLoaderConfiguration介绍

    memoryCacheExtraOptions(int , int ) 设置内存缓存的最大长宽度,默认不处理

    diskCacheExtraOptions(int , int ,BitmapProcessor ) 设置磁盘缓存的最大长宽度,默认不处理

    taskExecutor(Executor ) 定义线程池存储图片加载线程,默认不处理,取默认值

    taskExecutorForCachedImages(Executor ) 定义线程池存储图片加载线程,默认不处理,取默认值

    threadPoolSize(int ) 线程池的线程数量,缺省为DEFAULT_THREAD_POOL_SIZE = 3

    threadPriority(int ) 线程权限,缺省为 DEFAULT_THREAD_PRIORITY = Thread.NORM_PRIORITY - 2

    denyCacheImageMultipleSizesInMemory()同一url图片有不同尺寸时,只缓存最后一个,默认为全部缓存

    tasksProcessingOrder(QueueProcessingType )任务处理顺序,有先进先出和后进先出

    memoryCacheSize(int ) 内存缓存大小。默认为当前app可用内存的1/8

    Builder memoryCacheSizePercentage(int )内存缓存大小占比,1-100

    memoryCache(MemoryCache )内存缓存模式,默认为LruCache

    diskCacheFileCount(int ) 磁盘缓存文件数量上限

    diskCacheFileNameGenerator(FileNameGenerator )文件名生成器,默认为hashcode 生成

    imageDownloader(ImageDownloader ) 图片下载器,默认使用缺省值BaseImageDownLoader

    imageDecoder(ImageDecoder )图片解析器,默认使用缺省值BaseImageDecoder

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值