Android xUtils3源码解析之图片模块

来源:互联网 发布:naca4412翼型数据 编辑:程序博客网 时间:2024/05/21 03:19

本文已授权微信公众号《非著名程序员》原创首发,转载请务必注明出处。

xUtils3源码解析系列

一. Android xUtils3源码解析之网络模块
二. Android xUtils3源码解析之图片模块
三. Android xUtils3源码解析之注解模块
四. Android xUtils3源码解析之数据库模块

初始化

x.Ext.init(this);public static void init(Application app) {    TaskControllerImpl.registerInstance();    if (Ext.app == null) {        Ext.app = app;    }}public final class TaskControllerImpl implements TaskController {    public static void registerInstance() {        if (instance == null) {            synchronized (TaskController.class) {                if (instance == null) {                    instance = new TaskControllerImpl();                }            }        }        x.Ext.setTaskController(instance);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

获取ApplicationContext,实例化TaskControllerImpl对象,并设置为异步任务的真正管理类。

初始化ImageOptions

        ImageOptions imageOptions = new ImageOptions.Builder()                .setSize(DensityUtil.dip2px(120), DensityUtil.dip2px(120))                .setRadius(DensityUtil.dip2px(5))                // 如果ImageView的大小不是定义为wrap_content, 不要crop.                .setCrop(true) // 很多时候设置了合适的scaleType也不需要它.                // 加载中或错误图片的ScaleType                //.setPlaceholderScaleType(ImageView.ScaleType.MATRIX)                .setImageScaleType(ImageView.ScaleType.CENTER_CROP)                .setLoadingDrawableId(R.mipmap.ic_launcher)                .setFailureDrawableId(R.mipmap.ic_launcher)                .build();
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

上段代码来自xUtils3 sample。运用建造者(builder)模式实例化了一些初始参数,例如:图片大小、缩放模式、占位图、失败图等等。最后使用build()方法返回了一个ImageOptions对象。代码比较长,而且几乎都是get/set所以就不贴了。加载图片的所有设置都在这里,感兴趣的同学还请自行查看。

绑定图片的几种方式见下列代码,当然,里面几种CallBack是可以依据需求自己设置的:

x.image().bind(imageView, url, imageOptions);// assets filex.image().bind(imageView, "assets://test.gif", imageOptions);// local filex.image().bind(imageView, new File("/sdcard/test.gif").toURI().toString(), imageOptions);x.image().bind(imageView, "/sdcard/test.gif", imageOptions);x.image().bind(imageView, "file:///sdcard/test.gif", imageOptions);x.image().bind(imageView, "file:/sdcard/test.gif", imageOptions);x.image().bind(imageView, url, imageOptions, new Callback.CommonCallback<Drawable>() {...});x.image().loadDrawable(url, imageOptions, new Callback.CommonCallback<Drawable>() {...});x.image().loadFile(url, imageOptions, new Callback.CommonCallback<File>() {...});
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

没错,上面代码片段依旧来自xUtils3 README。下文以sample中的方式进行分析。

x.image().bind(holder.imgItem,                    imgSrcList.get(position),                    imageOptions,                    new CustomBitmapLoadCallBack(holder));
  • 1
  • 2
  • 3
  • 4
  • 1
  • 2
  • 3
  • 4

首次加载图片流程分析

x.image()

public final class x {    public static ImageManager image() {        if (Ext.imageManager == null) {            ImageManagerImpl.registerInstance();        }        return Ext.imageManager;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

和初始化的套路一样,实例化ImageManagerImpl对象,并设置为图片加载的管理器。之后调用ImageManagerImpl.bind()方法,跟进。

ImageManagerImpl.bind()

public final class ImageManagerImpl implements ImageManager {    public void bind(final ImageView view, final String url, final ImageOptions options, final Callback.CommonCallback<Drawable> callback) {        x.task().autoPost(new Runnable() {            @Override            public void run() {                ImageLoader.doBind(view, url, options, callback);            }        });    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

bind()方法内部调用了x.task().autoPost()。x.task()返回的是TaskController对象,实际上在初始化的时候TaskController被实例化的是TaskControllerImpl,向上转型的一个过程。所以实际上调用的还是TaskControllerImpl.aotoPost()。

TaskControllerImpl.aotoPost()

public final class TaskControllerImpl implements TaskController {    public void autoPost(Runnable runnable) {        if (runnable == null) return;        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {            runnable.run();        } else {            TaskProxy.sHandler.post(runnable);        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

autoPost()方法中首先判断是否是主线程,如果是,直接执行runnable.run()。如果不是,那么通过获取了MainLooper的Handler(sHandler)post到主线程中运行。所以不论x.image().bind()在主线程还是子线程调用,其内部调用的ImageLoader.doBind(view, url, options, callback)总是会在主线程中运行。

ImageLoader.doBind(view, url, options, callback)

为了方便阅读源码,我们以xml设置ImageView的宽高进行阅读。先阅读第一次从网络加载流程,之后分析缓存加载流程。

/*package*/ final class ImageLoader implements        Callback.PrepareCallback<File, Drawable>,        Callback.CacheCallback<Drawable>,        Callback.ProgressCallback<Drawable>,        Callback.TypedCallback<Drawable>,        Callback.Cancelable {    /*package*/    static Cancelable doBind(final ImageView view,                             final String url,                             final ImageOptions options,                             final Callback.CommonCallback<Drawable> callback) {        // check params        ImageOptions localOptions = options;        {            ...            localOptions.optimizeMaxSize(view);        }        if (memDrawable != null) { // has mem cache            ...        } else {            // load from Network or DiskCache            return new ImageLoader().doLoad(view, url, localOptions, callback);        }        return null;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

ImageLoader实现了五种CallBack(真特么多哇),这也就意味着等会请求的中间会回调ImageLoader的各种方法。首先是效验各种参数,因为是在xml设置了ImageView具体宽高。所以在localOptions.optimizeMaxSize(view)中会根据ImageView的宽高设置ImageOptions中的width、maxWidth属性的值等于ImageView的宽高。之后会从内存缓存中查找图片,由于是第一次加载所以不会执行里面的代码,之后会再次回到这里查看从缓存中查找图片相关逻辑。

ImageLoader.doLoad(view, url, options, callback)

/*package*/ final class ImageLoader implements ...{    private Cancelable doLoad(ImageView view,                              String url,                              ImageOptions options,                              Callback.CommonCallback<Drawable> callback) {        this.viewRef = new WeakReference<ImageView>(view);        this.options = options;        this.key = new MemCacheKey(url, options);        this.callback = callback;        if (callback instanceof Callback.ProgressCallback) {            this.progressCallback = (Callback.ProgressCallback<Drawable>) callback;        }        ...        // set loadingDrawable        Drawable loadingDrawable = null;        if (options.isForceLoadingDrawable()) {            loadingDrawable = options.getLoadingDrawable(view);            view.setScaleType(options.getPlaceholderScaleType());            view.setImageDrawable(new AsyncDrawable(this, loadingDrawable));        } else {            loadingDrawable = view.getDrawable();            view.setImageDrawable(new AsyncDrawable(this, loadingDrawable));        }        // request        RequestParams params = createRequestParams(url, options);        if (view instanceof FakeImageView) {            synchronized (FAKE_IMG_MAP) {                FAKE_IMG_MAP.put(url, (FakeImageView) view);            }        }        return cancelable = x.http().get(params, this);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

为了方便在后面请求的时候获取各种参数的引用,所以首先是各种赋值。之后设置等待加载的占位图(LoadingDrawable)。options的forceLoadingDrawable默认为true,placeholderScaleType属性默认为ImageView.ScaleType.CENTER_INSIDE。最后创建一个RequestParams对象之后开始加载图片的网络请求。这里先看下创建请求的过程。

网络请求

请求参数的创建

    private static RequestParams createRequestParams(String url, ImageOptions options) {        RequestParams params = new RequestParams(url);        // 设置缓存目录        params.setCacheDirName(DISK_CACHE_DIR_NAME);        // 设置超时时间        params.setConnectTimeout(1000 * 8);        // 设置优先级(最低)        params.setPriority(Priority.BG_LOW);        // 指定加载图片的线程池        params.setExecutor(EXECUTOR);        // 设置立即取消        params.setCancelFast(true);        params.setUseCookie(false);        if (options != null) {            ImageOptions.ParamsBuilder paramsBuilder = options.getParamsBuilder();            if (paramsBuilder != null) {                params = paramsBuilder.buildParams(params, options);            }        }        return params;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

请求参数的构造过程注释比较清晰了。这里需要注意的是线程池Executor EXECUTOR = new PriorityExecutor(10, false),核心线程数为10,FILO(first in last out)类型。假设在RecyclerView滑动后加载图片,首先要加载的肯定是正在展示给用户的图片,即最后实例化的runnable,所以这里是FILO类型。options在这里的作用是看有没有自定义的ImageOptions.ParamsBuilder,通过实现ImageOptions.ParamsBuilder接口,可以自己构建请求参数。默认是没有的,所以这里不用管。之后就进入了网络加载请求的过程。

网络加载图片

x.http().get(params, this)
这里的网络请求流程和 xUtils3源码解析之网络模块中差不多,这里主要讲两者的区别,建议先去看下上篇博文的分析。

由于ImageLoader实现了五种CallBack所以相应的回调实例会很多。在构造请求参数的过程中指定了EXECUTOR,所以不再使用默认的HTTP_EXEUTOR。在TaskProxy中首先会调用progressCallback.onStarted()(主线程),接着调用HttpTask.doBackground()。在HttpTask.doBackground()中调用resolveLoadType(),由于泛型是Drawable,所以loadType为File.class。即相对于普通网络请求实例化的是HttpRequest,但是实例化的Loader为FileLoader。如果这个过程不明白,强烈建议阅读 xUtils3源码解析之网络模块之后再回来看这篇。

FileLoader.load()
与StringLoader不同的是,FileLoader中加入了很多创建文件、读写文件相关的代码。如果只是简单的首次加载而且不考虑缓存的话,FileLoader中从网络中下载图片,期间调用progressHandler.updateProgress(total, current, true)更新进度,最后转换成Drawable,在ImageLoader.prepare()中压缩图片,并在ImageLoader.onSuccess(),将压缩后的Drawable资源设置给ImageView。

/*package*/ final class ImageLoader implements ... {    public void onSuccess(Drawable result) {        if (!validView4Callback(!hasCache)) return;        if (result != null) {            setSuccessDrawable4Callback(result);            if (callback != null) {                callback.onSuccess(result);            }        }    }    private void setSuccessDrawable4Callback(final Drawable drawable) {        final ImageView view = viewRef.get();        if (view != null) {            view.setScaleType(options.getImageScaleType());            if (drawable instanceof GifDrawable) {                if (view.getScaleType() == ImageView.ScaleType.CENTER) {                    view.setScaleType(ImageView.ScaleType.CENTER_INSIDE);                }                view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);            }            if (options.getAnimation() != null) {                ImageAnimationHelper.animationDisplay(view, drawable, options.getAnimation());            } else if (options.isFadeIn()) {                ImageAnimationHelper.fadeInDisplay(view, drawable);            } else {                view.setImageDrawable(drawable);            }        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

这当然不是关注的重点!!!

重点在两个地方:

  1. 图片的压缩
  2. 图片的缓存

图片的压缩

无论是从网络中加载图片还是从磁盘缓存中加载图片(内存缓存中的图片已经被压缩过,所以无需再次压缩),在正式加载图片之前。在HttpTask.doBackground()中会调用prepareCallback.prepare(rawResult),实际上调用的是ImageLoader.prepare()。

    public Drawable prepare(File rawData) {        if (!validView4Callback(true)) return null;        try {            Drawable result = null;            if (prepareCallback != null) {                result = prepareCallback.prepare(rawData);            }            if (result == null) {                result = ImageDecoder.decodeFileWithLock(rawData, options, this);            }            if (result != null) {                if (result instanceof ReusableDrawable) {                    ((ReusableDrawable) result).setMemCacheKey(key);                    MEM_CACHE.put(key, result);                }            }            return result;        } catch (IOException ex) {            IOUtil.deleteFileOrDir(rawData);            LogUtil.w(ex.getMessage(), ex);        }        return null;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24

这个方法的主要作用有两个:

  1. 压缩图片
  2. 将压缩后的图片存入内存缓存中

这里我们先看下压缩图片相关的代码,图片缓存相关在后文会讲。

ImageDecoder.decodeFileWithLock()

public final class ImageDecoder {    static {        int cpuCount = Runtime.getRuntime().availableProcessors();        BITMAP_DECODE_MAX_WORKER = cpuCount > 4 ? 2 : 1;    }    static Drawable decodeFileWithLock(final File file,                                       final ImageOptions options,                                       final Callback.Cancelable cancelable) throws IOException {        ...        Drawable result = null;        if (!options.isIgnoreGif() && isGif(file)) {            ...        } else {            Bitmap bitmap = null;            { // decode with lock                try {                    synchronized (bitmapDecodeLock) {                    ...                    if (bitmap == null) {                        bitmap = decodeBitmap(file, options, cancelable);                        if (bitmap != null && options.isCompress()) {                            final Bitmap finalBitmap = bitmap;                            THUMB_CACHE_EXECUTOR.execute(new Runnable() {                                @Override                                public void run() {                                    saveThumbCache(file, options, finalBitmap);                                }                            });                        }                        ...                    }                } finally {                    ...                }            }            if (bitmap != null) {                result = new ReusableBitmapDrawable(x.app().getResources(), bitmap);            }        }        return result;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

首先通过decodeBitmap()方法解析下载的图片文件,接着将解析出来的bitmap包装成ReusableBitmapDrawable对象返回。之后还会保存缩略图,这个过程跟下文保存磁盘缓存相同,这里先不分析这些。感兴趣的同学还请自行查看。跟进decodeBitmap()。

    public static Bitmap decodeBitmap(File file, ImageOptions options, Callback.Cancelable cancelable) throws IOException {        // check params        ...        Bitmap result = null;        try {            final BitmapFactory.Options bitmapOps = new BitmapFactory.Options();            bitmapOps.inJustDecodeBounds = true;            bitmapOps.inPurgeable = true;            bitmapOps.inInputShareable = true;            BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOps);            bitmapOps.inJustDecodeBounds = false;            bitmapOps.inPreferredConfig = options.getConfig();            int rotateAngle = 0;            int rawWidth = bitmapOps.outWidth;            int rawHeight = bitmapOps.outHeight;            int optionWith = options.getWidth();            int optionHeight = options.getHeight();            ...            bitmapOps.inSampleSize = calculateSampleSize(                    rawWidth, rawHeight,                    options.getMaxWidth(), options.getMaxHeight());            // decode file            Bitmap bitmap = null;            if (bitmap == null) {                bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), bitmapOps);            }            // 旋转、缩放、圆角等效果的处理            ...            result = bitmap;        } catch (IOException ex) {            throw ex;        } catch (Throwable ex) {            LogUtil.e(ex.getMessage(), ex);            result = null;        }        return result;    }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40

首先将inJustDecodeBounds属性设置为true,这样在解析图片文件的时候,只是获取了图片的宽高等参数,并不会真正的将图片加载进来。之后将inJustDecodeBounds属性设置为false,下次解析的时候,将会真正的将文件加载到内存中。之后获取图片的宽高和ImageView的宽高,calculateSampleSize()方法根据这四个参数获取inSampleSize属性的值。inSampleSize代表压缩比例。例如,inSampleSize = 1,那么图片宽高都不会被压缩,inSampleSize = 2,那么图片的宽高都会被压缩至原来的1/2,即图片大小变为原来的1/2 * 1/2 = 1/4。

public final class ImageDecoder {    public static int calculateSampleSize(final int rawWidth, final int rawHeight,                                          final int maxWidth, final int maxHeight) {        int sampleSize = 1;        if (rawWidth > maxWidth || rawHeight > maxHeight) {            if (rawWidth > rawHeight) {                sampleSize = Math.round((float) rawHeight / (float) maxHeight);            } else {                sampleSize = Math.round((float) rawWidth / (float) maxWidth);            }            if (sampleSize < 1) {                sampleSize = 1;            }            final float totalPixels = rawWidth * rawHeight;            final float maxTotalPixels = maxWidth * maxHeight * 2;            while (totalPixels / (sampleSize * sampleSize) > maxTotalPixels) {                sampleSize++;            }        }        return sampleSize;    } }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

通过while循环计算出恰当的压缩采样倍数。宽高都小于ImageView的图片不需要压缩,即sampleSize=1。大图片经过这个压缩采样倍数的压缩正好比ImageView宽高小。什么叫恰当?恰当就是当sampleSize减一,图片的宽/高会比ImageView的宽/高大。

图片的缓存

图片的缓存分为磁盘缓存和内存缓存。它们都采用LRU(Least recently used,最近最少使用)算法。

磁盘缓存

在HttpTask.doBackground()中,起初我以为this.request.save2Cache()是磁盘缓存的代码,跟进后发现,是个空实现。注释already saved by diskCacheFile#commit。中间作者可能处于某种原因更改了磁盘缓存的时机。diskCacheFile#commit()在FileLoader.load()中调用。

   if (diskCacheFile != null) {       targetFile = diskCacheFile.commit();   }
  • 1
  • 2
  • 3
  • 1
  • 2
  • 3

在首次下载之后,tempSaveFilePath、targetFile和diskCacheFile虽然对象类型不同,但是都指向同一个路径。

例如:/storage/emulated/0/Android/data/org.xutils.sample/cache/xUtils_img/3c62a6255c4910034613d999a508cf23.tmp。

diskCacheFile是个DiskCacheFile对象,在File类的基础上增加了DiskCacheEntity属性,DiskCacheEntity是个ORM的实体类,采用xUtils数据库注解实现,可以理解为DiskCacheEntity是数据库中的一张表,其中的每个属性对应数据表中的一个字段。root过的设备可以打开data/data/package name/database/xUtils_http_cache.db中的disk_cache表查看具体内容,这里就不再赘述。 跟进。

DiskCacheFile.commit()

public final class DiskCacheFile extends File implements Closeable {    public DiskCacheFile commit() throws IOException {        return getDiskCache().commitDiskCacheFile(this);    }    public LruDiskCache getDiskCache() {        // SD card adnroid/data/package/xutil_img        String dirName = this.getParentFile().getName();        return LruDiskCache.getDiskCache(dirName);    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

xUtils根据不同的dirName实例化不同的LruDiskCache,目前我们用到的只有/storage/emulated/0/android/data/org.xutils.sample/cache/xUtils_img/对应的LruDiskCache实例。“xUtils_img”文件夹在ImageLoader.createRequestParams()时设置。

LruDiskCache.commitDiskCacheFile()

public final class LruDiskCache {    private long diskCacheSize = LIMIT_SIZE;    private static final int LIMIT_COUNT = 5000; // 限制最多5000条数据    private static final long LIMIT_SIZE = 1024L * 1024L * 100L; // 限制最多100M文件    /*package*/ DiskCacheFile commitDiskCacheFile(DiskCacheFile cacheFile) throws IOException {        ...        DiskCacheFile result = null;        DiskCacheEntity cacheEntity = cacheFile.cacheEntity;        if (cacheFile.getName().endsWith(TEMP_FILE_SUFFIX)) { // is temp file            ProcessLock processLock = null;            DiskCacheFile destFile = null;            try {                String destPath = cacheEntity.getPath();                processLock = ProcessLock.tryLock(destPath, true, LOCK_WAIT);                if (processLock != null && processLock.isValid()) { // lock                    destFile = new DiskCacheFile(cacheEntity, destPath, processLock);                    if (cacheFile.renameTo(destFile)) {                        try {                            result = destFile;                            cacheDb.replace(cacheEntity);                        } catch (DbException ex) {                            LogUtil.e(ex.getMessage(), ex);                        }                        trimSize();                    } else {                        throw new IOException("rename:" + cacheFile.getAbsolutePath());                    }                } else {                    throw new FileLockedException(destPath);                }            } catch (InterruptedException ex) {                result = cacheFile;                LogUtil.e(ex.getMessage(), ex);            } finally {                ...            }        } else {            result = cacheFile;        }        return result;    }    private void trimSize() {        trimExecutor.execute(new Runnable() {            @Override            public void run() {                if (available) {                    long current = System.currentTimeMillis();                    if (current - lastTrimTime < TRIM_TIME_SPAN) {                        return;                    } else {                        lastTrimTime = current;                    }                    // trim expires                    deleteExpiry();                    // trim db                    try {                    // 超找DiskCacheEntity数据表中一共多少行                        int count = (int) cacheDb.selector(DiskCacheEntity.class).count();                        if (count > LIMIT_COUNT + 10) {                        // 依据lastAccess和hits排序,查找前count - LIMIT_COUNT条数据                            List<DiskCacheEntity> rmList = cacheDb.selector(DiskCacheEntity.class)                                    .orderBy("lastAccess").orderBy("hits")                                    .limit(count - LIMIT_COUNT).offset(0).findAll();                            if (rmList != null && rmList.size() > 0) {                                // delete cache files                                for (DiskCacheEntity entity : rmList) {                                    try {                                        // delete db entity                                        cacheDb.delete(entity);                                        // delete cache files                                        String path = entity.getPath();                                        if (!TextUtils.isEmpty(path)) {                                            deleteFileWithLock(path);                                            deleteFileWithLock(path + TEMP_FILE_SUFFIX);                                        }                                    } catch (DbException ex) {                                        LogUtil.e(ex.getMessage(), ex);                                    }                                }                            }                        }                    } catch (DbException ex) {                        LogUtil.e(ex.getMessage(), ex);                    }                    // trim disk                    try {                        while (FileUtil.getFileOrDirSize(cacheDir) > diskCacheSize) {                            List<DiskCacheEntity> rmList = cacheDb.selector(DiskCacheEntity.class)                                    .orderBy("lastAccess").orderBy("hits").limit(10).offset(0).findAll();                            if (rmList != null && rmList.size() > 0) {                                // delete cache files                                for (DiskCacheEntity entity : rmList) {                                    try {                                        // delete db entity                                        cacheDb.delete(entity);                                        // delete cache files                                        String path = entity.getPath();                                        if (!TextUtils.isEmpty(path)) {                                            deleteFileWithLock(path);                                            deleteFileWithLock(path + TEMP_FILE_SUFFIX);                                        }                                    } catch (DbException ex) {                                        LogUtil.e(ex.getMessage(), ex);                                    }                                }                            }                        }                    } catch (DbException ex) {                        LogUtil.e(ex.getMessage(), ex);                    }                }            }        });    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115
  • 116
  • 117
  • 118
  • 119
  • 120
  • 121
  • 122
  • 123
  • 124
  • 125
  • 126

港真,我没弄明白commitDiskCacheFile中将参数cacheFile转换成destFile存储的意义在哪里,它俩除了一个以.tmp结尾一个没有后缀之外,好像没什么区别。更新下数据表中的信息还是很有必要的,无论下次查找缓存文件还是删除的时候查找文件,都是通过数据表中的path列来查找的。每次添加新的文件之后,都会去调用trimSize()方法检查是否需要重新设置。在trimSize()方法中trimExecutor是个核心线程数为1的线程池,FIFO类型。available属性自从LruDiskCache实例化之后就一直为true。这里有两个try代码块,对应于上面两个上限:

  1. 缓存数据表不得超过5000(实际按5010判断)条数据
  2. 缓存文件容量不得超过100M

达到上述任意条件之一,都会执行相应的try代码块。其实这两个try代码块就是LRU算法的具体实现。这里涉及到一些xUtils3数据库API的一些用法,被我在注释说明了。第一个try代码块的作用:DiskCacheEntity数据表中超过5000行,删除按照LRU排序出来的数据及对应的缓存文件。第二个try代码块和第一个逻辑相同,只是查找条件不一样。这里提点小小的瑕疵,两个try代码块查找出来之后,执行的操作都是一样的,完全可以抽成一个方法。看来大神也喜欢CV,哈哈~

内存缓存

图片的压缩中提到:无论是从网络中加载图片还是从磁盘缓存中加载图片(内存缓存中的图片已经被压缩过,所以无需再次压缩),在正式加载图片之前。在HttpTask.doBackground()中会调用prepareCallback.prepare(rawResult),实际上调用的是ImageLoader.prepare()。内存的缓存就是在这个prepare()中。跟进。

/*package*/ final class ImageLoader ...{    private final static int MEM_CACHE_MIN_SIZE = 1024 * 1024 * 4; // 4M    static {        int memClass = ((ActivityManager) x.app()                .getSystemService(Context.ACTIVITY_SERVICE)).getMemoryClass();        // Use 1/8th of the available memory for this memory cache.        int cacheSize = 1024 * 1024 * memClass / 8;        if (cacheSize < MEM_CACHE_MIN_SIZE) {            cacheSize = MEM_CACHE_MIN_SIZE;        }        MEM_CACHE.resize(cacheSize);    }    private final static LruCache<MemCacheKey, Drawable> MEM_CACHE =            new LruCache<MemCacheKey, Drawable>(MEM_CACHE_MIN_SIZE) {                private boolean deepClear = false;                @Override                protected int sizeOf(MemCacheKey key, Drawable value) {                    if (value instanceof BitmapDrawable) {                        Bitmap bitmap = ((BitmapDrawable) value).getBitmap();                        return bitmap == null ? 0 : bitmap.getByteCount();                    } else if (value instanceof GifDrawable) {                        return ((GifDrawable) value).getByteCount();                    }                    return super.sizeOf(key, value);                }                @Override                public void trimToSize(int maxSize) {                    if (maxSize < 0) {                        deepClear = true;                    }                    super.trimToSize(maxSize);                    deepClear = false;                }                @Override                protected void entryRemoved(boolean evicted, MemCacheKey key, Drawable oldValue, Drawable newValue) {                    super.entryRemoved(evicted, key, oldValue, newValue);                    if (evicted && deepClear && oldValue instanceof ReusableDrawable) {                        ((ReusableDrawable) oldValue).setMemCacheKey(null);                    }                }            };    @Override    public Drawable prepare(File rawData) {        try {            ...            if (result == null) {                result = ImageDecoder.decodeFileWithLock(rawData, options, this);            }            if (result != null) {                if (result instanceof ReusableDrawable) {                    ((ReusableDrawable) result).setMemCacheKey(key);                    MEM_CACHE.put(key, result);                }            }            return result;        } catch (IOException ex) {            ...        }        return null;    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69

ImageLoader中定义了一个LruCache对象MEM_CACHE,默认使用1/8可用内存,如果1/8内存小于4M,内存大小则定义成4M。有趣的是这里的LruCache类是作者从android.util.LruCache类中拷贝出来的。LruCache的使用非常简单,上面的实例化是个固定的套路。MEM_CACHE.put(key, result)这里的key是个全局属性,在doLoad()方法中调用this.key = new MemCacheKey(url, options)实例化。需要注意一个小细节,内存缓存中的图片都是经过压缩过的,而磁盘缓存的图片是原图。内存缓存就此完结。

图片的各种加载途径顺序

这个问题其实有点弱鸡,先不看代码,按照加载速度,猜想一下也应该是:内存缓存–>硬盘缓存–>网络加载。不过本着严谨的精神,还是查看下相关代码。

从网络加载图片

参见首次加载图片流程分析

从内存缓存加载图片

/*package*/ final class ImageLoader implements ...{    static Cancelable doBind(...) {        ...        // load from Memory Cache        Drawable memDrawable = null;        if (localOptions.isUseMemCache()) {            memDrawable = MEM_CACHE.get(key);            if (memDrawable instanceof BitmapDrawable) {                Bitmap bitmap = ((BitmapDrawable) memDrawable).getBitmap();                if (bitmap == null || bitmap.isRecycled()) {                    memDrawable = null;                }            }        }        if (memDrawable != null) { // has mem cache            boolean trustMemCache = false;            try {                // hit mem cache                view.setScaleType(localOptions.getImageScaleType());                view.setImageDrawable(memDrawable);                ...            } catch (Throwable ex) {                ...            } finally {                ...            }        } else {            // load from Network or DiskCache            return new ImageLoader().doLoad(view, url, localOptions, callback);        }        ...    }                        }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

首先是查找内存缓存的,内存缓存没有命中才去从网络或者磁盘缓存中查找。这点在作者的注释上也能体现出来~

从磁盘缓存中加载图片

在HttpTask.doBackground()中会首先尝试从磁盘缓存中查找图片,代码如下:

public class HttpTask<ResultType> extends AbsTask<ResultType> implements ProgressHandler {    protected ResultType doBackground() throws Throwable {        ...        // 检查缓存        Object cacheResult = null;        if (cacheCallback != null && HttpMethod.permitsCache(params.getMethod())) {            // 尝试从缓存获取结果, 并为请求头加入缓存控制参数.            try {                clearRawResult();                LogUtil.d("load cache: " + this.request.getRequestUri());                // 从磁盘缓存中查找图片                rawResult = this.request.loadResultFromCache();            } catch (Throwable ex) {                LogUtil.w("load disk cache error", ex);            }            if (rawResult != null) {                if (prepareCallback != null) {                    try {                        // 压缩查找到的图片                        cacheResult = prepareCallback.prepare(rawResult);                    } catch (Throwable ex) {                        ...                    }                }                 if (cacheResult != null) {                    // 同步等待是否信任缓存                    this.update(FLAG_CACHE, cacheResult);                    synchronized (cacheLock) {                        while (trustCache == null) {                            try {                                cacheLock.wait();                            } catch (InterruptedException iex) {                                throw new Callback.CancelledException("cancelled before request");                            } catch (Throwable ignored) {                            }                        }                    }                    // 处理完成                    if (trustCache) {                        return null;                    }                }            }        }        ...    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49

从磁盘缓存中查找的过程等下再说,现在假设从磁盘缓存中命中了相应的图片(实际上也是这样)。之后压缩图片,并添加进内存缓存,压缩过程在分析内存缓存的过程中已经分析过了。最后在调用this.update(FLAG_CACHE, cacheResult)之后,锁住了HttpTask类的继续执行。this.update()会通过sHandler(实例化时传入MainLooper)调用HttpTask.onUpdate(),其中又在调用ImageLoader.onCache()之后,继续执行HttpTask类相关方法(其实是返回了null)。重点看下ImageLoader.onCache()。

ImageLoader.onCache()

/*package*/ final class ImageLoader implements ... {        @Override        public boolean onCache(Drawable result) {            if (!validView4Callback(true)) return false;            if (result != null) {                hasCache = true;                setSuccessDrawable4Callback(result);                if (cacheCallback != null) {                    return cacheCallback.onCache(result);                } else if (callback != null) {                    callback.onSuccess(result);                    return true;                }                return true;            }            return false;        }        private void setSuccessDrawable4Callback(final Drawable drawable) {        final ImageView view = viewRef.get();        if (view != null) {            view.setScaleType(options.getImageScaleType());            if (drawable instanceof GifDrawable) {                if (view.getScaleType() == ImageView.ScaleType.CENTER) {                    view.setScaleType(ImageView.ScaleType.CENTER_INSIDE);                }                view.setLayerType(View.LAYER_TYPE_SOFTWARE, null);            }            if (options.getAnimation() != null) {                ImageAnimationHelper.animationDisplay(view, drawable, options.getAnimation());            } else if (options.isFadeIn()) {                ImageAnimationHelper.fadeInDisplay(view, drawable);            } else {                view.setImageDrawable(drawable);            }        }    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

真相大白。现在还差从磁盘缓存查找图片的过程,即rawResult = this.request.loadResultFromCache()的过程。request对应的HttpRequest,跟进。

HttpRequest.loadResultFromCache()

public class HttpRequest extends UriRequest {    public Object loadResultFromCache() throws Throwable {        isLoading = true;        DiskCacheEntity cacheEntity = LruDiskCache.getDiskCache(params.getCacheDirName())                .setMaxSize(params.getCacheSize())                .get(this.getCacheKey());        if (cacheEntity != null) {            if (HttpMethod.permitsCache(params.getMethod())) {                Date lastModified = cacheEntity.getLastModify();                if (lastModified.getTime() > 0) {                    params.setHeader("If-Modified-Since", toGMTString(lastModified));                }                String eTag = cacheEntity.getEtag();                if (!TextUtils.isEmpty(eTag)) {                    params.setHeader("If-None-Match", eTag);                }            }            return loader.loadFromCache(cacheEntity);        } else {            return null;        }    }}public class FileLoader extends Loader<File> {    @Override    public File loadFromCache(final DiskCacheEntity cacheEntity) throws Throwable {        return LruDiskCache.getDiskCache(params.getCacheDirName()).getDiskCacheFile(cacheEntity.getKey());    }}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

根据cacheKey(其实就是url)获取到对应数据表中的实体类。之后通过实体类中的path查找对应的图片文件。一切真相大白。

总结

xUtils3图片模块采用二级缓存(内存LRU+磁盘LRU)+线程池(10核心+FILO)实现。内存占1/8可用内存,最少占用4M。磁盘缓存最多缓存5000条或者100M数据。内存中的图片已经被压缩过,磁盘中存储原图。图片加载优先级:内存–>磁盘–>网络。