精华内容
下载资源
问答
  • Android xUtils3源码解析之图片模块

    千次阅读 2017-03-30 09:43:54
    本文已授权微信公众号《非著名程序员》原创首发,转载请务必注明出处。 xUtils3源码解析之图片模块,图片加载所需要的一切,都在这里~

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

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

    获取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();

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

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

    x.image().bind(imageView, url, imageOptions);
    
    // assets file
    x.image().bind(imageView, "assets://test.gif", imageOptions);
    
    // local file
    x.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>() {...});

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

    x.image().bind(holder.imgItem,
                        imgSrcList.get(position),
                        imageOptions,
                        new CustomBitmapLoadCallBack(holder));

    首次加载图片流程分析

    x.image()

    public final class x {
        public static ImageManager image() {
            if (Ext.imageManager == null) {
                ImageManagerImpl.registerInstance();
            }
            return Ext.imageManager;
        }
    }

    和初始化的套路一样,实例化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);
                }
            });
        }
    }

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

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

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

    为了方便在后面请求的时候获取各种参数的引用,所以首先是各种赋值。之后设置等待加载的占位图(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;
        }

    请求参数的构造过程注释比较清晰了。这里需要注意的是线程池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. 图片的缓存

    图片的压缩

    无论是从网络中加载图片还是从磁盘缓存中加载图片(内存缓存中的图片已经被压缩过,所以无需再次压缩),在正式加载图片之前。在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. 将压缩后的图片存入内存缓存中

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

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

    首先通过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;
        }
    

    首先将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;
        }
     }

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

    在首次下载之后,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);
        }
    }

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

    港真,我没弄明白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;
        }
    }

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

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

    从磁盘缓存中加载图片

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

    从磁盘缓存中查找的过程等下再说,现在假设从磁盘缓存中命中了相应的图片(实际上也是这样)。之后压缩图片,并添加进内存缓存,压缩过程在分析内存缓存的过程中已经分析过了。最后在调用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);
                }
            }
        }
    }

    真相大白。现在还差从磁盘缓存查找图片的过程,即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());
        }
    }

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

    总结

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

    展开全文
  • Android获取本地图片缩略图终极解决方案

    万次阅读 多人点赞 2016-05-24 20:43:21
    QAQ学Android真的还是要在项目中获得锻炼,脱离实际一切都是耍流氓哼唧~! 花了一下午时间搞定了项目中要实现的获取本地图片缩略图并显示在ListView上的,并且点击要能获得该图片文件路径功能,下面先上效果图: ...

    QAQ学Android真的还是要在项目中获得锻炼,脱离实际一切都是耍流氓哼唧~!

    花了一下午时间搞定了项目中要实现的:获取本地图片缩略图并显示在ListView上的,并且点击要能获得该图片文件路径功能,下面先上效果图:


    作为一个新手,大概碰到这种需求的思路就是:

    首先,递归遍历本地所有文件,然后按文件后缀名找出所有的图片文件,更好的方式是在媒体库里查找所有的图片(系统已经帮你过滤好了所有的图片文件直接去调用就阔以了),再通过得到的文件对象file显示图像。

    当然这种处理结果就是大概1~2张图片就直接OOM了。(反正我手机里图片都是至少上MB的...)。

    所以呢,必须对图片进行压缩,于是我又在网上找到了一个比较好的图片压缩方法(这里没有引用转载地址了,1是因为是昨天找到的现在已经找不到网址了,2是因为很多篇博客都是相同的方法相同的注释,我也不知道到底谁才是原作...):

    /*    *//**
         * 根据指定的图像路径和大小来获取缩略图
         * 此方法有两点好处:
         *     1. 使用较小的内存空间,第一次获取的bitmap实际上为null,只是为了读取宽度和高度,
         *        第二次读取的bitmap是根据比例压缩过的图像,第三次读取的bitmap是所要的缩略图。
         *     2. 缩略图对于原图像来讲没有拉伸,这里使用了2.2版本的新工具ThumbnailUtils,使
         *        用这个工具生成的图像不会被拉伸。
         * @param imagePath 图像的路径
         * @param width 指定输出图像的宽度
         * @param height 指定输出图像的高度
         * @return 生成的缩略图
         *//*
        private Bitmap getImageThumbnail(String imagePath, int width, int height) {
            Bitmap bitmap = null;
            BitmapFactory.Options options = new BitmapFactory.Options();
            options.inJustDecodeBounds = true;
            // 获取这个图片的宽和高,注意此处的bitmap为null
            bitmap = BitmapFactory.decodeFile(imagePath, options);
            options.inJustDecodeBounds = false; // 设为 false
            // 计算缩放比
            int h = options.outHeight;
            int w = options.outWidth;
            int beWidth = w / width;
            int beHeight = h / height;
            int be = 1;
            if (beWidth < beHeight) {
                be = beWidth;
            } else {
                be = beHeight;
            }
            if (be <= 0) {
                be = 1;
            }
            options.inSampleSize = be;
            // 重新读入图片,读取缩放后的bitmap,注意这次要把options.inJustDecodeBounds 设为 false
            bitmap = BitmapFactory.decodeFile(imagePath, options);
            // 利用ThumbnailUtils来创建缩略图,这里要指定要缩放哪个Bitmap对象
            bitmap = ThumbnailUtils.extractThumbnail(bitmap, width, height,
                    ThumbnailUtils.OPTIONS_RECYCLE_INPUT);
            return bitmap;
        }*/
    方法很简单,然而可以发现这个方法其实是做了这么几件事情:

    1、根据传入的图片路径构造bitmap,得到原图的宽高.

    2、计算图片合适的缩放比.

    3、利用ThumbnailUtils来创建缩略图,然后返回这个最终缩放以后的bitmap.

    尽管像方法中说的那样:使用较小的内存空间,第一次获取的bitmap实际上为null,只是为了读取宽度和高度, 第二次读取的bitmap是根据比例压缩过的图像,第三次读取的bitmap是所要的缩略图。

    然而整个方法还是开辟了3个bitmap对象的内存区域,这还不考虑ThumbnailUtils.extractThumbnail()方法所耗费的时空间。

    所以当我使用了这个方法实现了在ListView中遍历本地图片的时候,上下滑起来是灰常卡滴(这里就不贴gif图了有兴趣想知道的朋友可以自己尝试一下)


    Finaly,在踏破铁鞋无觅处之后,终于找到了最终解决方法,也是一开始忽略的方法:

    原来一直不知道的Thumbnails类,才是解决问题的关键。

    在Android系统中也有对应的thumbnails文件,下面是百度百科对它的描述:


    然后在MediaStore媒体库类中,也是有Thumbnails这么一个内部类的:

     /**
             * This class allows developers to query and get two kinds of thumbnails:
             * MINI_KIND: 512 x 384 thumbnail
             * MICRO_KIND: 96 x 96 thumbnail
             */
            public static class Thumbnails implements BaseColumns {
                public static final Cursor query(ContentResolver cr, Uri uri, String[] projection) {
                    return cr.query(uri, projection, null, null, DEFAULT_SORT_ORDER);
                }
    
                public static final Cursor queryMiniThumbnails(ContentResolver cr, Uri uri, int kind,
                        String[] projection) {
                    return cr.query(uri, projection, "kind = " + kind, null, DEFAULT_SORT_ORDER);
                }
    
                public static final Cursor queryMiniThumbnail(ContentResolver cr, long origId, int kind,
                        String[] projection) {
                    return cr.query(EXTERNAL_CONTENT_URI, projection,
                            IMAGE_ID + " = " + origId + " AND " + KIND + " = " +
                            kind, null, null);
                }
    
                /**
                 * This method cancels the thumbnail request so clients waiting for getThumbnail will be
                 * interrupted and return immediately. Only the original process which made the getThumbnail
                 * requests can cancel their own requests.
                 *
                 * @param cr ContentResolver
                 * @param origId original image id
                 */
                public static void cancelThumbnailRequest(ContentResolver cr, long origId) {
                    InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI,
                            InternalThumbnails.DEFAULT_GROUP_ID);
                }
    
                /**
                 * This method checks if the thumbnails of the specified image (origId) has been created.
                 * It will be blocked until the thumbnails are generated.
                 *
                 * @param cr ContentResolver used to dispatch queries to MediaProvider.
                 * @param origId Original image id associated with thumbnail of interest.
                 * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND.
                 * @param options this is only used for MINI_KIND when decoding the Bitmap
                 * @return A Bitmap instance. It could be null if the original image
                 *         associated with origId doesn't exist or memory is not enough.
                 */
                public static Bitmap getThumbnail(ContentResolver cr, long origId, int kind,
                        BitmapFactory.Options options) {
                    return InternalThumbnails.getThumbnail(cr, origId,
                            InternalThumbnails.DEFAULT_GROUP_ID, kind, options,
                            EXTERNAL_CONTENT_URI, false);
                }
    
                /**
                 * This method cancels the thumbnail request so clients waiting for getThumbnail will be
                 * interrupted and return immediately. Only the original process which made the getThumbnail
                 * requests can cancel their own requests.
                 *
                 * @param cr ContentResolver
                 * @param origId original image id
                 * @param groupId the same groupId used in getThumbnail.
                 */
                public static void cancelThumbnailRequest(ContentResolver cr, long origId, long groupId) {
                    InternalThumbnails.cancelThumbnailRequest(cr, origId, EXTERNAL_CONTENT_URI, groupId);
                }
    
                /**
                 * This method checks if the thumbnails of the specified image (origId) has been created.
                 * It will be blocked until the thumbnails are generated.
                 *
                 * @param cr ContentResolver used to dispatch queries to MediaProvider.
                 * @param origId Original image id associated with thumbnail of interest.
                 * @param groupId the id of group to which this request belongs
                 * @param kind The type of thumbnail to fetch. Should be either MINI_KIND or MICRO_KIND.
                 * @param options this is only used for MINI_KIND when decoding the Bitmap
                 * @return A Bitmap instance. It could be null if the original image
                 *         associated with origId doesn't exist or memory is not enough.
                 */
                public static Bitmap getThumbnail(ContentResolver cr, long origId, long groupId,
                        int kind, BitmapFactory.Options options) {
                    return InternalThumbnails.getThumbnail(cr, origId, groupId, kind, options,
                            EXTERNAL_CONTENT_URI, false);
                }
    
                /**
                 * Get the content:// style URI for the image media table on the
                 * given volume.
                 *
                 * @param volumeName the name of the volume to get the URI for
                 * @return the URI to the image media table on the given volume
                 */
                public static Uri getContentUri(String volumeName) {
                    return Uri.parse(CONTENT_AUTHORITY_SLASH + volumeName +
                            "/images/thumbnails");
                }
    
                /**
                 * The content:// style URI for the internal storage.
                 */
                public static final Uri INTERNAL_CONTENT_URI =
                        getContentUri("internal");
    
                /**
                 * The content:// style URI for the "primary" external storage
                 * volume.
                 */
                public static final Uri EXTERNAL_CONTENT_URI =
                        getContentUri("external");
    
                /**
                 * The default sort order for this table
                 */
                public static final String DEFAULT_SORT_ORDER = "image_id ASC";
    
                /**
                 * The data stream for the thumbnail
                 * <P>Type: DATA STREAM</P>
                 */
                public static final String DATA = "_data";
    
                /**
                 * The original image for the thumbnal
                 * <P>Type: INTEGER (ID from Images table)</P>
                 */
                public static final String IMAGE_ID = "image_id";
    
                /**
                 * The kind of the thumbnail
                 * <P>Type: INTEGER (One of the values below)</P>
                 */
                public static final String KIND = "kind";
    
                public static final int MINI_KIND = 1;
                public static final int FULL_SCREEN_KIND = 2;
                public static final int MICRO_KIND = 3;
                /**
                 * The blob raw data of thumbnail
                 * <P>Type: DATA STREAM</P>
                 */
                public static final String THUMB_DATA = "thumb_data";
    
                /**
                 * The width of the thumbnal
                 * <P>Type: INTEGER (long)</P>
                 */
                public static final String WIDTH = "width";
    
                /**
                 * The height of the thumbnail
                 * <P>Type: INTEGER (long)</P>
                 */
                public static final String HEIGHT = "height";
            }
        }
    我们可以从中找到几个可用于Cursor查找的重要参数:

    /**
     * The data stream for the thumbnail
     * <P>Type: DATA STREAM</P>
     */
    public static final String DATA = "_data";
    
    /**
     * The original image for the thumbnal
     * <P>Type: INTEGER (ID from Images table)</P>
     */
    public static final String IMAGE_ID = "image_id";
    /**
     * The content:// style URI for the "primary" external storage
     * volume.
     */
    public static final Uri EXTERNAL_CONTENT_URI =
            getContentUri("external");
    有了这三个参数,我们就可以很轻松从本地媒体库中获得图片缩略图的ID和路径。

            //先得到缩略图的URL和对应的图片id
            Cursor cursor = cr.query(
                    Thumbnails.EXTERNAL_CONTENT_URI,
                    new String[]{
                            Thumbnails.IMAGE_ID,
                            Thumbnails.DATA
                    },
                    null,
                    null,
                    null);

    这里的缩略图ID有什么用呢?我们从它的注释中可以很明显地得到:

          /**
                 * The original image for the thumbnal
                 * <P>Type: INTEGER (ID from Images table)</P>

    ID from Images table!!!这个ID是跟多媒体库中的images表的ID相对应的,由此,我们可以通过这个id来设置cursor的查找条件,从而找出images表中对应的真正的图片文件的路径!

    从而完美地实现了文章开头的功能需求。

    下面是完整的代码:

    获得一个HashMap参数的ArrayList,HashMap项的键"thumbnail_path"对应真实图片路径值,键"image_id_path"对应缩略图路径值,有了这两个路径,想干嘛干嘛了2333

    /**
         * 得到本地图片文件
         * @param context
         * @return
         */
        public static ArrayList<HashMap<String,String>> getAllPictures(Context context) {
            ArrayList<HashMap<String,String>> picturemaps = new ArrayList<>();
            HashMap<String,String> picturemap;
            ContentResolver cr = context.getContentResolver();
            //先得到缩略图的URL和对应的图片id
            Cursor cursor = cr.query(
                    Thumbnails.EXTERNAL_CONTENT_URI,
                    new String[]{
                            Thumbnails.IMAGE_ID,
                            Thumbnails.DATA
                    },
                    null,
                    null,
                    null);
            if (cursor.moveToFirst()) {
                do {
                    picturemap = new HashMap<>();
                    picturemap.put("image_id_path",cursor.getInt(0)+"");
                    picturemap.put("thumbnail_path",cursor.getString(1));
                    picturemaps.add(picturemap);
                } while (cursor.moveToNext());
                cursor.close();
            }
            //再得到正常图片的path
            for (int i = 0;i<picturemaps.size();i++) {
                picturemap = picturemaps.get(i);
                String media_id = picturemap.get("image_id_path");
                cursor = cr.query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                        new String[]{
                                MediaStore.Images.Media.DATA
                        },
                        MediaStore.Audio.Media._ID+"="+media_id,
                        null,
                        null
                );
                if (cursor.moveToFirst()) {
                    do {
                        picturemap.put("image_id",cursor.getString(0));
                        picturemaps.set(i,picturemap);
                    } while (cursor.moveToNext());
                    cursor.close();
                }
            }
            return picturemaps;
        }





    展开全文
  • 这两个缓存模块的作用各不相同,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。 内存缓存和硬盘缓存的相互结合才构成了...

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/54895665

    本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每天都有文章更新。

    在本系列的上一篇文章中,我带着大家一起阅读了一遍Glide的源码,初步了解了这个强大的图片加载框架的基本执行流程。

    不过,上一篇文章只能说是比较粗略地阅读了Glide整个执行流程方面的源码,搞明白了Glide的基本工作原理,但并没有去深入分析每一处的细节(事实上也不可能在一篇文章中深入分析每一处源码的细节)。那么从本篇文章开始,我们就一篇篇地来针对Glide某一块功能进行深入地分析,慢慢将Glide中的各项功能进行全面掌握。

    今天我们就先从缓存这一块内容开始入手吧。不过今天文章中的源码都建在上一篇源码分析的基础之上,还没有看过上一篇文章的朋友,建议先去阅读 Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程

    Glide缓存简介

    Glide的缓存设计可以说是非常先进的,考虑的场景也很周全。在缓存这一功能上,Glide又将它分成了两个模块,一个是内存缓存,一个是硬盘缓存。

    这两个缓存模块的作用各不相同,内存缓存的主要作用是防止应用重复将图片数据读取到内存当中,而硬盘缓存的主要作用是防止应用重复从网络或其他地方重复下载和读取数据。

    内存缓存和硬盘缓存的相互结合才构成了Glide极佳的图片缓存效果,那么接下来我们就分别来分析一下这两种缓存的使用方法以及它们的实现原理。

    缓存Key

    既然是缓存功能,就必然会有用于进行缓存的Key。那么Glide的缓存Key是怎么生成的呢?我不得不说,Glide的缓存Key生成规则非常繁琐,决定缓存Key的参数竟然有10个之多。不过繁琐归繁琐,至少逻辑还是比较简单的,我们先来看一下Glide缓存Key的生成逻辑。

    生成缓存Key的代码在Engine类的load()方法当中,这部分代码我们在上一篇文章当中已经分析过了,只不过当时忽略了缓存相关的内容,那么我们现在重新来看一下:

    public class Engine implements EngineJobListener,
            MemoryCache.ResourceRemovedListener,
            EngineResource.ResourceListener {
    
        public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
                DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
                Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
            Util.assertMainThread();
            long startTime = LogTime.getLogTime();
    
            final String id = fetcher.getId();
            EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                    loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                    transcoder, loadProvider.getSourceEncoder());
    
            ...
        }
    
        ...
    }

    可以看到,这里在第11行调用了fetcher.getId()方法获得了一个id字符串,这个字符串也就是我们要加载的图片的唯一标识,比如说如果是一张网络上的图片的话,那么这个id就是这张图片的url地址。

    接下来在第12行,将这个id连同着signature、width、height等等10个参数一起传入到EngineKeyFactory的buildKey()方法当中,从而构建出了一个EngineKey对象,这个EngineKey也就是Glide中的缓存Key了。

    可见,决定缓存Key的条件非常多,即使你用override()方法改变了一下图片的width或者height,也会生成一个完全不同的缓存Key。

    EngineKey类的源码大家有兴趣可以自己去看一下,其实主要就是重写了equals()和hashCode()方法,保证只有传入EngineKey的所有参数都相同的情况下才认为是同一个EngineKey对象,我就不在这里将源码贴出来了。

    内存缓存

    有了缓存Key,接下来就可以开始进行缓存了,那么我们先从内存缓存看起。

    首先你要知道,默认情况下,Glide自动就是开启内存缓存的。也就是说,当我们使用Glide加载了一张图片之后,这张图片就会被缓存到内存当中,只要在它还没从内存中被清除之前,下次使用Glide再加载这张图片都会直接从内存当中读取,而不用重新从网络或硬盘上读取了,这样无疑就可以大幅度提升图片的加载效率。比方说你在一个RecyclerView当中反复上下滑动,RecyclerView中只要是Glide加载过的图片都可以直接从内存当中迅速读取并展示出来,从而大大提升了用户体验。

    而Glide最为人性化的是,你甚至不需要编写任何额外的代码就能自动享受到这个极为便利的内存缓存功能,因为Glide默认就已经将它开启了。

    那么既然已经默认开启了这个功能,还有什么可讲的用法呢?只有一点,如果你有什么特殊的原因需要禁用内存缓存功能,Glide对此提供了接口:

    Glide.with(this)
         .load(url)
         .skipMemoryCache(true)
         .into(imageView);

    可以看到,只需要调用skipMemoryCache()方法并传入true,就表示禁用掉Glide的内存缓存功能。

    没错,关于Glide内存缓存的用法就只有这么多,可以说是相当简单。但是我们不可能只停留在这么简单的层面上,接下来就让我们就通过阅读源码来分析一下Glide的内存缓存功能是如何实现的。

    其实说到内存缓存的实现,非常容易就让人想到LruCache算法(Least Recently Used),也叫近期最少使用算法。它的主要算法原理就是把最近使用的对象用强引用存储在LinkedHashMap中,并且把最近最少使用的对象在缓存值达到预设定值之前从内存中移除。LruCache的用法也比较简单,我在 Android高效加载大图、多图解决方案,有效避免程序OOM 这篇文章当中有提到过它的用法,感兴趣的朋友可以去参考一下。

    那么不必多说,Glide内存缓存的实现自然也是使用的LruCache算法。不过除了LruCache算法之外,Glide还结合了一种弱引用的机制,共同完成了内存缓存功能,下面就让我们来通过源码分析一下。

    首先回忆一下,在上一篇文章的第二步load()方法中,我们当时分析到了在loadGeneric()方法中会调用Glide.buildStreamModelLoader()方法来获取一个ModelLoader对象。当时没有再跟进到这个方法的里面再去分析,那么我们现在来看下它的源码:

    public class Glide {
    
        public static <T, Y> ModelLoader<T, Y> buildModelLoader(Class<T> modelClass, Class<Y> resourceClass,
                Context context) {
             if (modelClass == null) {
                if (Log.isLoggable(TAG, Log.DEBUG)) {
                    Log.d(TAG, "Unable to load null model, setting placeholder only");
                }
                return null;
            }
            return Glide.get(context).getLoaderFactory().buildModelLoader(modelClass, resourceClass);
        }
    
        public static Glide get(Context context) {
            if (glide == null) {
                synchronized (Glide.class) {
                    if (glide == null) {
                        Context applicationContext = context.getApplicationContext();
                        List<GlideModule> modules = new ManifestParser(applicationContext).parse();
                        GlideBuilder builder = new GlideBuilder(applicationContext);
                        for (GlideModule module : modules) {
                            module.applyOptions(applicationContext, builder);
                        }
                        glide = builder.createGlide();
                        for (GlideModule module : modules) {
                            module.registerComponents(applicationContext, glide);
                        }
                    }
                }
            }
            return glide;
        }
    
        ...
    }

    这里我们还是只看关键,在第11行去构建ModelLoader对象的时候,先调用了一个Glide.get()方法,而这个方法就是关键。我们可以看到,get()方法中实现的是一个单例功能,而创建Glide对象则是在第24行调用GlideBuilder的createGlide()方法来创建的,那么我们跟到这个方法当中:

    public class GlideBuilder {
        ...
    
        Glide createGlide() {
            if (sourceService == null) {
                final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());
                sourceService = new FifoPriorityThreadPoolExecutor(cores);
            }
            if (diskCacheService == null) {
                diskCacheService = new FifoPriorityThreadPoolExecutor(1);
            }
            MemorySizeCalculator calculator = new MemorySizeCalculator(context);
            if (bitmapPool == null) {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) {
                    int size = calculator.getBitmapPoolSize();
                    bitmapPool = new LruBitmapPool(size);
                } else {
                    bitmapPool = new BitmapPoolAdapter();
                }
            }
            if (memoryCache == null) {
                memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
            }
            if (diskCacheFactory == null) {
                diskCacheFactory = new InternalCacheDiskCacheFactory(context);
            }
            if (engine == null) {
                engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
            }
            if (decodeFormat == null) {
                decodeFormat = DecodeFormat.DEFAULT;
            }
            return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
        }
    }

    这里也就是构建Glide对象的地方了。那么观察第22行,你会发现这里new出了一个LruResourceCache,并把它赋值到了memoryCache这个对象上面。你没有猜错,这个就是Glide实现内存缓存所使用的LruCache对象了。不过我这里并不打算展开来讲LruCache算法的具体实现,如果你感兴趣的话可以自己研究一下它的源码。

    现在创建好了LruResourceCache对象只能说是把准备工作做好了,接下来我们就一步步研究Glide中的内存缓存到底是如何实现的。

    刚才在Engine的load()方法中我们已经看到了生成缓存Key的代码,而内存缓存的代码其实也是在这里实现的,那么我们重新来看一下Engine类load()方法的完整源码:

    public class Engine implements EngineJobListener,
            MemoryCache.ResourceRemovedListener,
            EngineResource.ResourceListener {
        ...    
    
        public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
                DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
                Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
            Util.assertMainThread();
            long startTime = LogTime.getLogTime();
    
            final String id = fetcher.getId();
            EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                    loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                    transcoder, loadProvider.getSourceEncoder());
    
            EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
            if (cached != null) {
                cb.onResourceReady(cached);
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    logWithTimeAndKey("Loaded resource from cache", startTime, key);
                }
                return null;
            }
    
            EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
            if (active != null) {
                cb.onResourceReady(active);
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    logWithTimeAndKey("Loaded resource from active resources", startTime, key);
                }
                return null;
            }
    
            EngineJob current = jobs.get(key);
            if (current != null) {
                current.addCallback(cb);
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    logWithTimeAndKey("Added to existing load", startTime, key);
                }
                return new LoadStatus(cb, current);
            }
    
            EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
            DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
                    transcoder, diskCacheProvider, diskCacheStrategy, priority);
            EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
            jobs.put(key, engineJob);
            engineJob.addCallback(cb);
            engineJob.start(runnable);
    
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Started new load", startTime, key);
            }
            return new LoadStatus(cb, engineJob);
        }
    
        ...
    }

    可以看到,这里在第17行调用了loadFromCache()方法来获取缓存图片,如果获取到就直接调用cb.onResourceReady()方法进行回调。如果没有获取到,则会在第26行调用loadFromActiveResources()方法来获取缓存图片,获取到的话也直接进行回调。只有在两个方法都没有获取到缓存的情况下,才会继续向下执行,从而开启线程来加载图片。

    也就是说,Glide的图片加载过程中会调用两个方法来获取内存缓存,loadFromCache()和loadFromActiveResources()。这两个方法中一个使用的就是LruCache算法,另一个使用的就是弱引用。我们来看一下它们的源码:

    public class Engine implements EngineJobListener,
            MemoryCache.ResourceRemovedListener,
            EngineResource.ResourceListener {
    
        private final MemoryCache cache;
        private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
        ...
    
        private EngineResource<?> loadFromCache(Key key, boolean isMemoryCacheable) {
            if (!isMemoryCacheable) {
                return null;
            }
            EngineResource<?> cached = getEngineResourceFromCache(key);
            if (cached != null) {
                cached.acquire();
                activeResources.put(key, new ResourceWeakReference(key, cached, getReferenceQueue()));
            }
            return cached;
        }
    
        private EngineResource<?> getEngineResourceFromCache(Key key) {
            Resource<?> cached = cache.remove(key);
            final EngineResource result;
            if (cached == null) {
                result = null;
            } else if (cached instanceof EngineResource) {
                result = (EngineResource) cached;
            } else {
                result = new EngineResource(cached, true /*isCacheable*/);
            }
            return result;
        }
    
        private EngineResource<?> loadFromActiveResources(Key key, boolean isMemoryCacheable) {
            if (!isMemoryCacheable) {
                return null;
            }
            EngineResource<?> active = null;
            WeakReference<EngineResource<?>> activeRef = activeResources.get(key);
            if (activeRef != null) {
                active = activeRef.get();
                if (active != null) {
                    active.acquire();
                } else {
                    activeResources.remove(key);
                }
            }
            return active;
        }
    
        ...
    }

    在loadFromCache()方法的一开始,首先就判断了isMemoryCacheable是不是false,如果是false的话就直接返回null。这是什么意思呢?其实很简单,我们刚刚不是学了一个skipMemoryCache()方法吗?如果在这个方法中传入true,那么这里的isMemoryCacheable就会是false,表示内存缓存已被禁用。

    我们继续住下看,接着调用了getEngineResourceFromCache()方法来获取缓存。在这个方法中,会使用缓存Key来从cache当中取值,而这里的cache对象就是在构建Glide对象时创建的LruResourceCache,那么说明这里其实使用的就是LruCache算法了。

    但是呢,观察第22行,当我们从LruResourceCache中获取到缓存图片之后会将它从缓存中移除,然后在第16行将这个缓存图片存储到activeResources当中。activeResources就是一个弱引用的HashMap,用来缓存正在使用中的图片,我们可以看到,loadFromActiveResources()方法就是从activeResources这个HashMap当中取值的。使用activeResources来缓存正在使用中的图片,可以保护这些图片不会被LruCache算法回收掉。

    好的,从内存缓存中读取数据的逻辑大概就是这些了。概括一下来说,就是如果能从内存缓存当中读取到要加载的图片,那么就直接进行回调,如果读取不到的话,才会开启线程执行后面的图片加载逻辑。

    现在我们已经搞明白了内存缓存读取的原理,接下来的问题就是内存缓存是在哪里写入的呢?这里我们又要回顾一下上一篇文章中的内容了。还记不记得我们之前分析过,当图片加载完成之后,会在EngineJob当中通过Handler发送一条消息将执行逻辑切回到主线程当中,从而执行handleResultOnMainThread()方法。那么我们现在重新来看一下这个方法,代码如下所示:

    class EngineJob implements EngineRunnable.EngineRunnableManager {
    
        private final EngineResourceFactory engineResourceFactory;
        ...
    
        private void handleResultOnMainThread() {
            if (isCancelled) {
                resource.recycle();
                return;
            } else if (cbs.isEmpty()) {
                throw new IllegalStateException("Received a resource without any callbacks to notify");
            }
            engineResource = engineResourceFactory.build(resource, isCacheable);
            hasResource = true;
            engineResource.acquire();
            listener.onEngineJobComplete(key, engineResource);
            for (ResourceCallback cb : cbs) {
                if (!isInIgnoredCallbacks(cb)) {
                    engineResource.acquire();
                    cb.onResourceReady(engineResource);
                }
            }
            engineResource.release();
        }
    
        static class EngineResourceFactory {
            public <R> EngineResource<R> build(Resource<R> resource, boolean isMemoryCacheable) {
                return new EngineResource<R>(resource, isMemoryCacheable);
            }
        }
        ...
    }

    在第13行,这里通过EngineResourceFactory构建出了一个包含图片资源的EngineResource对象,然后会在第16行将这个对象回调到Engine的onEngineJobComplete()方法当中,如下所示:

    public class Engine implements EngineJobListener,
            MemoryCache.ResourceRemovedListener,
            EngineResource.ResourceListener {
        ...    
    
        @Override
        public void onEngineJobComplete(Key key, EngineResource<?> resource) {
            Util.assertMainThread();
            // A null resource indicates that the load failed, usually due to an exception.
            if (resource != null) {
                resource.setResourceListener(key, this);
                if (resource.isCacheable()) {
                    activeResources.put(key, new ResourceWeakReference(key, resource, getReferenceQueue()));
                }
            }
            jobs.remove(key);
        }
    
        ...
    }

    现在就非常明显了,可以看到,在第13行,回调过来的EngineResource被put到了activeResources当中,也就是在这里写入的缓存。

    那么这只是弱引用缓存,还有另外一种LruCache缓存是在哪里写入的呢?这就要介绍一下EngineResource中的一个引用机制了。观察刚才的handleResultOnMainThread()方法,在第15行和第19行有调用EngineResource的acquire()方法,在第23行有调用它的release()方法。其实,EngineResource是用一个acquired变量用来记录图片被引用的次数,调用acquire()方法会让变量加1,调用release()方法会让变量减1,代码如下所示:

    class EngineResource<Z> implements Resource<Z> {
    
        private int acquired;
        ...
    
        void acquire() {
            if (isRecycled) {
                throw new IllegalStateException("Cannot acquire a recycled resource");
            }
            if (!Looper.getMainLooper().equals(Looper.myLooper())) {
                throw new IllegalThreadStateException("Must call acquire on the main thread");
            }
            ++acquired;
        }
    
        void release() {
            if (acquired <= 0) {
                throw new IllegalStateException("Cannot release a recycled or not yet acquired resource");
            }
            if (!Looper.getMainLooper().equals(Looper.myLooper())) {
                throw new IllegalThreadStateException("Must call release on the main thread");
            }
            if (--acquired == 0) {
                listener.onResourceReleased(key, this);
            }
        }
    }

    也就是说,当acquired变量大于0的时候,说明图片正在使用中,也就应该放到activeResources弱引用缓存当中。而经过release()之后,如果acquired变量等于0了,说明图片已经不再被使用了,那么此时会在第24行调用listener的onResourceReleased()方法来释放资源,这个listener就是Engine对象,我们来看下它的onResourceReleased()方法:

    public class Engine implements EngineJobListener,
            MemoryCache.ResourceRemovedListener,
            EngineResource.ResourceListener {
    
        private final MemoryCache cache;
        private final Map<Key, WeakReference<EngineResource<?>>> activeResources;
        ...    
    
        @Override
        public void onResourceReleased(Key cacheKey, EngineResource resource) {
            Util.assertMainThread();
            activeResources.remove(cacheKey);
            if (resource.isCacheable()) {
                cache.put(cacheKey, resource);
            } else {
                resourceRecycler.recycle(resource);
            }
        }
    
        ...
    }

    可以看到,这里首先会将缓存图片从activeResources中移除,然后再将它put到LruResourceCache当中。这样也就实现了正在使用中的图片使用弱引用来进行缓存,不在使用中的图片使用LruCache来进行缓存的功能。

    这就是Glide内存缓存的实现原理。

    硬盘缓存

    接下来我们开始学习硬盘缓存方面的内容。

    不知道你还记不记得,在本系列的第一篇文章中我们就使用过硬盘缓存的功能了。当时为了禁止Glide对图片进行硬盘缓存而使用了如下代码:

    Glide.with(this)
         .load(url)
         .diskCacheStrategy(DiskCacheStrategy.NONE)
         .into(imageView);

    调用diskCacheStrategy()方法并传入DiskCacheStrategy.NONE,就可以禁用掉Glide的硬盘缓存功能了。

    这个diskCacheStrategy()方法基本上就是Glide硬盘缓存功能的一切,它可以接收四种参数:

    • DiskCacheStrategy.NONE: 表示不缓存任何内容。
    • DiskCacheStrategy.SOURCE: 表示只缓存原始图片。
    • DiskCacheStrategy.RESULT: 表示只缓存转换过后的图片(默认选项)。
    • DiskCacheStrategy.ALL : 表示既缓存原始图片,也缓存转换过后的图片。

    上面四种参数的解释本身并没有什么难理解的地方,但是有一个概念大家需要了解,就是当我们使用Glide去加载一张图片的时候,Glide默认并不会将原始图片展示出来,而是会对图片进行压缩和转换(我们会在后面学习这方面的内容)。总之就是经过种种一系列操作之后得到的图片,就叫转换过后的图片。而Glide默认情况下在硬盘缓存的就是转换过后的图片,我们通过调用diskCacheStrategy()方法则可以改变这一默认行为。

    好的,关于Glide硬盘缓存的用法也就只有这么多,那么接下来还是老套路,我们通过阅读源码来分析一下,Glide的硬盘缓存功能是如何实现的。

    首先,和内存缓存类似,硬盘缓存的实现也是使用的LruCache算法,而且Google还提供了一个现成的工具类DiskLruCache。我之前也专门写过一篇文章对这个DiskLruCache工具进行了比较全面的分析,感兴趣的朋友可以参考一下 Android DiskLruCache完全解析,硬盘缓存的最佳方案 。当然,Glide是使用的自己编写的DiskLruCache工具类,但是基本的实现原理都是差不多的。

    接下来我们看一下Glide是在哪里读取硬盘缓存的。这里又需要回忆一下上篇文章中的内容了,Glide开启线程来加载图片后会执行EngineRunnable的run()方法,run()方法中又会调用一个decode()方法,那么我们重新再来看一下这个decode()方法的源码:

    private Resource<?> decode() throws Exception {
        if (isDecodingFromCache()) {
            return decodeFromCache();
        } else {
            return decodeFromSource();
        }
    }

    可以看到,这里会分为两种情况,一种是调用decodeFromCache()方法从硬盘缓存当中读取图片,一种是调用decodeFromSource()来读取原始图片。默认情况下Glide会优先从缓存当中读取,只有缓存中不存在要读取的图片时,才会去读取原始图片。那么我们现在来看一下decodeFromCache()方法的源码,如下所示:

    private Resource<?> decodeFromCache() throws Exception {
        Resource<?> result = null;
        try {
            result = decodeJob.decodeResultFromCache();
        } catch (Exception e) {
            if (Log.isLoggable(TAG, Log.DEBUG)) {
                Log.d(TAG, "Exception decoding result from cache: " + e);
            }
        }
        if (result == null) {
            result = decodeJob.decodeSourceFromCache();
        }
        return result;
    }

    可以看到,这里会先去调用DecodeJob的decodeResultFromCache()方法来获取缓存,如果获取不到,会再调用decodeSourceFromCache()方法获取缓存,这两个方法的区别其实就是DiskCacheStrategy.RESULT和DiskCacheStrategy.SOURCE这两个参数的区别,相信不需要我再做什么解释吧。

    那么我们来看一下这两个方法的源码吧,如下所示:

    public Resource<Z> decodeResultFromCache() throws Exception {
        if (!diskCacheStrategy.cacheResult()) {
            return null;
        }
        long startTime = LogTime.getLogTime();
        Resource<T> transformed = loadFromCache(resultKey);
        startTime = LogTime.getLogTime();
        Resource<Z> result = transcode(transformed);
        return result;
    }
    
    public Resource<Z> decodeSourceFromCache() throws Exception {
        if (!diskCacheStrategy.cacheSource()) {
            return null;
        }
        long startTime = LogTime.getLogTime();
        Resource<T> decoded = loadFromCache(resultKey.getOriginalKey());
        return transformEncodeAndTranscode(decoded);
    }

    可以看到,它们都是调用了loadFromCache()方法从缓存当中读取数据,如果是decodeResultFromCache()方法就直接将数据解码并返回,如果是decodeSourceFromCache()方法,还要调用一下transformEncodeAndTranscode()方法先将数据转换一下再解码并返回。

    然而我们注意到,这两个方法中在调用loadFromCache()方法时传入的参数却不一样,一个传入的是resultKey,另外一个却又调用了resultKey的getOriginalKey()方法。这个其实非常好理解,刚才我们已经解释过了,Glide的缓存Key是由10个参数共同组成的,包括图片的width、height等等。但如果我们是缓存的原始图片,其实并不需要这么多的参数,因为不用对图片做任何的变化。那么我们来看一下getOriginalKey()方法的源码:

    public Key getOriginalKey() {
        if (originalKey == null) {
            originalKey = new OriginalKey(id, signature);
        }
        return originalKey;
    }

    可以看到,这里其实就是忽略了绝大部分的参数,只使用了id和signature这两个参数来构成缓存Key。而signature参数绝大多数情况下都是用不到的,因此基本上可以说就是由id(也就是图片url)来决定的Original缓存Key。

    搞明白了这两种缓存Key的区别,那么接下来我们看一下loadFromCache()方法的源码吧:

    private Resource<T> loadFromCache(Key key) throws IOException {
        File cacheFile = diskCacheProvider.getDiskCache().get(key);
        if (cacheFile == null) {
            return null;
        }
        Resource<T> result = null;
        try {
            result = loadProvider.getCacheDecoder().decode(cacheFile, width, height);
        } finally {
            if (result == null) {
                diskCacheProvider.getDiskCache().delete(key);
            }
        }
        return result;
    }

    这个方法的逻辑非常简单,调用getDiskCache()方法获取到的就是Glide自己编写的DiskLruCache工具类的实例,然后调用它的get()方法并把缓存Key传入,就能得到硬盘缓存的文件了。如果文件为空就返回null,如果文件不为空则将它解码成Resource对象后返回即可。

    这样我们就将硬盘缓存读取的源码分析完了,那么硬盘缓存又是在哪里写入的呢?趁热打铁我们赶快继续分析下去。

    刚才已经分析过了,在没有缓存的情况下,会调用decodeFromSource()方法来读取原始图片。那么我们来看下这个方法:

    public Resource<Z> decodeFromSource() throws Exception {
        Resource<T> decoded = decodeSource();
        return transformEncodeAndTranscode(decoded);
    }

    这个方法中只有两行代码,decodeSource()顾名思义是用来解析原图片的,而transformEncodeAndTranscode()则是用来对图片进行转换和转码的。我们先来看decodeSource()方法:

    private Resource<T> decodeSource() throws Exception {
        Resource<T> decoded = null;
        try {
            long startTime = LogTime.getLogTime();
            final A data = fetcher.loadData(priority);
            if (isCancelled) {
                return null;
            }
            decoded = decodeFromSourceData(data);
        } finally {
            fetcher.cleanup();
        }
        return decoded;
    }
    
    private Resource<T> decodeFromSourceData(A data) throws IOException {
        final Resource<T> decoded;
        if (diskCacheStrategy.cacheSource()) {
            decoded = cacheAndDecodeSourceData(data);
        } else {
            long startTime = LogTime.getLogTime();
            decoded = loadProvider.getSourceDecoder().decode(data, width, height);
        }
        return decoded;
    }
    
    private Resource<T> cacheAndDecodeSourceData(A data) throws IOException {
        long startTime = LogTime.getLogTime();
        SourceWriter<A> writer = new SourceWriter<A>(loadProvider.getSourceEncoder(), data);
        diskCacheProvider.getDiskCache().put(resultKey.getOriginalKey(), writer);
        startTime = LogTime.getLogTime();
        Resource<T> result = loadFromCache(resultKey.getOriginalKey());
        return result;
    }

    这里会在第5行先调用fetcher的loadData()方法读取图片数据,然后在第9行调用decodeFromSourceData()方法来对图片进行解码。接下来会在第18行先判断是否允许缓存原始图片,如果允许的话又会调用cacheAndDecodeSourceData()方法。而在这个方法中同样调用了getDiskCache()方法来获取DiskLruCache实例,接着调用它的put()方法就可以写入硬盘缓存了,注意原始图片的缓存Key是用的resultKey.getOriginalKey()。

    好的,原始图片的缓存写入就是这么简单,接下来我们分析一下transformEncodeAndTranscode()方法的源码,来看看转换过后的图片缓存是怎么写入的。代码如下所示:

    private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
        long startTime = LogTime.getLogTime();
        Resource<T> transformed = transform(decoded);
        writeTransformedToCache(transformed);
        startTime = LogTime.getLogTime();
        Resource<Z> result = transcode(transformed);
        return result;
    }
    
    private void writeTransformedToCache(Resource<T> transformed) {
        if (transformed == null || !diskCacheStrategy.cacheResult()) {
            return;
        }
        long startTime = LogTime.getLogTime();
        SourceWriter<Resource<T>> writer = new SourceWriter<Resource<T>>(loadProvider.getEncoder(), transformed);
        diskCacheProvider.getDiskCache().put(resultKey, writer);
    }

    这里的逻辑就更加简单明了了。先是在第3行调用transform()方法来对图片进行转换,然后在writeTransformedToCache()方法中将转换过后的图片写入到硬盘缓存中,调用的同样是DiskLruCache实例的put()方法,不过这里用的缓存Key是resultKey。

    这样我们就将Glide硬盘缓存的实现原理也分析完了。虽然这些源码看上去如此的复杂,但是经过Glide出色的封装,使得我们只需要通过skipMemoryCache()和diskCacheStrategy()这两个方法就可以轻松自如地控制Glide的缓存功能了。

    了解了Glide缓存的实现原理之后,接下来我们再来学习一些Glide缓存的高级技巧吧。

    高级技巧

    虽说Glide将缓存功能高度封装之后,使得用法变得非常简单,但同时也带来了一些问题。

    比如之前有一位群里的朋友就跟我说过,他们项目的图片资源都是存放在七牛云上面的,而七牛云为了对图片资源进行保护,会在图片url地址的基础之上再加上一个token参数。也就是说,一张图片的url地址可能会是如下格式:

    http://url.com/image.jpg?token=d9caa6e02c990b0a

    而使用Glide加载这张图片的话,也就会使用这个url地址来组成缓存Key。

    但是接下来问题就来了,token作为一个验证身份的参数并不是一成不变的,很有可能时时刻刻都在变化。而如果token变了,那么图片的url也就跟着变了,图片url变了,缓存Key也就跟着变了。结果就造成了,明明是同一张图片,就因为token不断在改变,导致Glide的缓存功能完全失效了。

    这其实是个挺棘手的问题,而且我相信绝对不仅仅是七牛云这一个个例,大家在使用Glide的时候很有可能都会遇到这个问题。

    那么该如何解决这个问题呢?我们还是从源码的层面进行分析,首先再来看一下Glide生成缓存Key这部分的代码:

    public class Engine implements EngineJobListener,
            MemoryCache.ResourceRemovedListener,
            EngineResource.ResourceListener {
    
        public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
                DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
                Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
            Util.assertMainThread();
            long startTime = LogTime.getLogTime();
    
            final String id = fetcher.getId();
            EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
                    loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
                    transcoder, loadProvider.getSourceEncoder());
    
            ...
        }
    
        ...
    }

    来看一下第11行,刚才已经说过了,这个id其实就是图片的url地址。那么,这里是通过调用fetcher.getId()方法来获取的图片url地址,而我们在上一篇文章中已经知道了,fetcher就是HttpUrlFetcher的实例,我们就来看一下它的getId()方法的源码吧,如下所示:

    public class HttpUrlFetcher implements DataFetcher<InputStream> {
    
        private final GlideUrl glideUrl;
        ...
    
        public HttpUrlFetcher(GlideUrl glideUrl) {
            this(glideUrl, DEFAULT_CONNECTION_FACTORY);
        }
    
        HttpUrlFetcher(GlideUrl glideUrl, HttpUrlConnectionFactory connectionFactory) {
            this.glideUrl = glideUrl;
            this.connectionFactory = connectionFactory;
        }
    
        @Override
        public String getId() {
            return glideUrl.getCacheKey();
        }
    
        ...
    }

    可以看到,getId()方法中又调用了GlideUrl的getCacheKey()方法。那么这个GlideUrl对象是从哪里来的呢?其实就是我们在load()方法中传入的图片url地址,然后Glide在内部把这个url地址包装成了一个GlideUrl对象。

    很明显,接下来我们就要看一下GlideUrl的getCacheKey()方法的源码了,如下所示:

    public class GlideUrl {
    
        private final URL url;
        private final String stringUrl;
        ...
    
        public GlideUrl(URL url) {
            this(url, Headers.DEFAULT);
        }
    
        public GlideUrl(String url) {
            this(url, Headers.DEFAULT);
        }
    
        public GlideUrl(URL url, Headers headers) {
            ...
            this.url = url;
            stringUrl = null;
        }
    
        public GlideUrl(String url, Headers headers) {
            ...
            this.stringUrl = url;
            this.url = null;
        }
    
        public String getCacheKey() {
            return stringUrl != null ? stringUrl : url.toString();
        }
    
        ...
    }
    

    这里我将代码稍微进行了一点简化,这样看上去更加简单明了。GlideUrl类的构造函数接收两种类型的参数,一种是url字符串,一种是URL对象。然后getCacheKey()方法中的判断逻辑非常简单,如果传入的是url字符串,那么就直接返回这个字符串本身,如果传入的是URL对象,那么就返回这个对象toString()后的结果。

    其实看到这里,我相信大家已经猜到解决方案了,因为getCacheKey()方法中的逻辑太直白了,直接就是将图片的url地址进行返回来作为缓存Key的。那么其实我们只需要重写这个getCacheKey()方法,加入一些自己的逻辑判断,就能轻松解决掉刚才的问题了。

    创建一个MyGlideUrl继承自GlideUrl,代码如下所示:

    public class MyGlideUrl extends GlideUrl {
    
        private String mUrl;
    
        public MyGlideUrl(String url) {
            super(url);
            mUrl = url;
        }
    
        @Override
        public String getCacheKey() {
            return mUrl.replace(findTokenParam(), "");
        }
    
        private String findTokenParam() {
            String tokenParam = "";
            int tokenKeyIndex = mUrl.indexOf("?token=") >= 0 ? mUrl.indexOf("?token=") : mUrl.indexOf("&token=");
            if (tokenKeyIndex != -1) {
                int nextAndIndex = mUrl.indexOf("&", tokenKeyIndex + 1);
                if (nextAndIndex != -1) {
                    tokenParam = mUrl.substring(tokenKeyIndex + 1, nextAndIndex + 1);
                } else {
                    tokenParam = mUrl.substring(tokenKeyIndex);
                }
            }
            return tokenParam;
        }
    
    }

    可以看到,这里我们重写了getCacheKey()方法,在里面加入了一段逻辑用于将图片url地址中token参数的这一部分移除掉。这样getCacheKey()方法得到的就是一个没有token参数的url地址,从而不管token怎么变化,最终Glide的缓存Key都是固定不变的了。

    当然,定义好了MyGlideUrl,我们还得使用它才行,将加载图片的代码改成如下方式即可:

    Glide.with(this)
         .load(new MyGlideUrl(url))
         .into(imageView);

    也就是说,我们需要在load()方法中传入这个自定义的MyGlideUrl对象,而不能再像之前那样直接传入url字符串了。不然的话Glide在内部还是会使用原始的GlideUrl类,而不是我们自定义的MyGlideUrl类。

    这样我们就将这个棘手的缓存问题给解决掉了。

    好了,关于Glide缓存方面的内容今天就分析到这里,现在我们不光掌握了Glide缓存的基本用法和高级技巧,还了解了它背后的实现原理,又是收获满满的一篇文章啊。下一篇文章当中,我会继续带着大家深入分析Glide的其他功能模块,讲一讲回调方面的知识,感兴趣的朋友请继续阅读 Android图片加载框架最全解析(四),玩转Glide的回调与监听

    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

    展开全文
  • 这里一定要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,而LinearLayout中的内容就是我们的两个Button,如果你直接调用button的scroll方法的话,那结果一定不是你想看到的。 另外还有...

    转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/48719871
    2016大家新年好!这是今年的第一篇文章,那么应CSDN工作人员的建议,为了能给大家带来更好的阅读体验,我也是将博客换成了宽屏版。另外,作为一个对新鲜事物从来后知后觉的人,我终于也在新的一年里改用MarkDown编辑器来写博客了,希望大家在我的博客里也能体验到新年新的气象。

    我写博客的题材很多时候取决于平时大家问的问题,最近一段时间有不少朋友都问到ViewPager是怎么实现的。那ViewPager相信每个人都再熟悉不过了,因此它实在是太常用了,我们可以借助ViewPager来轻松完成页面之间的滑动切换效果,但是如果问到它是如何实现的话,我感觉大部分人还是比较陌生的, 为此我也是做了一番功课。其实说到ViewPager最基本的实现原理主要就是两部分内容,一个是事件分发,一个是Scroller,那么对于事件分发,其实我在很早之前就已经写过了相关的内容,感兴趣的朋友可以去阅读 Android事件分发机制完全解析,带你从源码的角度彻底理解,但是对于Scroller我还从来没有讲过,因此本篇文章我们就先来学习一下Scroller的用法,并结合事件分发和Scroller来实现一个简易版的ViewPager。


    Scroller是一个专门用于处理滚动效果的工具类,可能在大多数情况下,我们直接使用Scroller的场景并不多,但是很多大家所熟知的控件在内部都是使用Scroller来实现的,如ViewPager、ListView等。而如果能够把Scroller的用法熟练掌握的话,我们自己也可以轻松实现出类似于ViewPager这样的功能。那么首先新建一个ScrollerTest项目,今天就让我们通过例子来学习一下吧。
    先撇开Scroller类不谈,其实任何一个控件都是可以滚动的,因为在View类当中有scrollTo()和scrollBy()这两个方法,如下图所示:

    这两个方法都是用于对View进行滚动的,那么它们之间有什么区别呢?简单点讲,scrollBy()方法是让View相对于当前的位置滚动某段距离,而scrollTo()方法则是让View相对于初始的位置滚动某段距离。这样讲大家理解起来可能有点费劲,我们来通过例子实验一下就知道了。

    修改activity_main.xml中的布局文件,代码如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        android:id="@+id/layout"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context="com.example.guolin.scrollertest.MainActivity">
    
        <Button
            android:id="@+id/scroll_to_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="scrollTo"/>
    
        <Button
            android:id="@+id/scroll_by_btn"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="scrollBy"/>
    
    </LinearLayout>
    

    外层我们使用了一个LinearLayout,然后在里面包含了两个按钮,一个用于触发scrollTo逻辑,一个用于触发scrollBy逻辑。

    接着修改MainActivity中的代码,如下所示:

    public class MainActivity extends AppCompatActivity {
    
        private LinearLayout layout;
    
        private Button scrollToBtn;
    
        private Button scrollByBtn;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            layout = (LinearLayout) findViewById(R.id.layout);
            scrollToBtn = (Button) findViewById(R.id.scroll_to_btn);
            scrollByBtn = (Button) findViewById(R.id.scroll_by_btn);
            scrollToBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    layout.scrollTo(-60, -100);
                }
            });
            scrollByBtn.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    layout.scrollBy(-60, -100);
                }
            });
        }
    }
    

    没错,代码就是这么简单。当点击了scrollTo按钮时,我们调用了LinearLayout的scrollTo()方法,当点击了scrollBy按钮时,调用了LinearLayout的scrollBy()方法。那有的朋友可能会问了,为什么都是调用的LinearLayout中的scroll方法?这里一定要注意,不管是scrollTo()还是scrollBy()方法,滚动的都是该View内部的内容,而LinearLayout中的内容就是我们的两个Button,如果你直接调用button的scroll方法的话,那结果一定不是你想看到的。

    另外还有一点需要注意,就是两个scroll方法中传入的参数,第一个参数x表示相对于当前位置横向移动的距离,正值向左移动,负值向右移动,单位是像素。第二个参数y表示相对于当前位置纵向移动的距离,正值向上移动,负值向下移动,单位是像素。

    那说了这么多,scrollTo()和scrollBy()这两个方法到底有什么区别呢?其实运行一下代码我们就能立刻知道了:

    可以看到,当我们点击scrollTo按钮时,两个按钮会一起向右下方滚动,因为我们传入的参数是-60和-100,因此向右下方移动是正确的。但是你会发现,之后再点击scrollTo按钮就没有任何作用了,界面不会再继续滚动,只有点击scrollBy按钮界面才会继续滚动,并且不停点击scrollBy按钮界面会一起滚动下去。

    现在我们再来回头看一下这两个方法的区别,scrollTo()方法是让View相对于初始的位置滚动某段距离,由于View的初始位置是不变的,因此不管我们点击多少次scrollTo按钮滚动到的都将是同一个位置。而scrollBy()方法则是让View相对于当前的位置滚动某段距离,那每当我们点击一次scrollBy按钮,View的当前位置都进行了变动,因此不停点击会一直向右下方移动。

    通过这个例子来理解,相信大家已经把scrollTo()和scrollBy()这两个方法的区别搞清楚了,但是现在还有一个问题,从上图中大家也能看得出来,目前使用这两个方法完成的滚动效果是跳跃式的,没有任何平滑滚动的效果。没错,只靠scrollTo()和scrollBy()这两个方法是很难完成ViewPager这样的效果的,因此我们还需要借助另外一个关键性的工具,也就我们今天的主角Scroller。

    Scroller的基本用法其实还是比较简单的,主要可以分为以下几个步骤:

    1. 创建Scroller的实例
    2. 调用startScroll()方法来初始化滚动数据并刷新界面
    3. 重写computeScroll()方法,并在其内部完成平滑滚动的逻辑

    那么下面我们就按照上述的步骤,通过一个模仿ViewPager的简易例子来学习和理解一下Scroller的用法。
    新建一个ScrollerLayout并让它继承自ViewGroup来作为我们的简易ViewPager布局,代码如下所示:

    /**
     * Created by guolin on 16/1/12.
     */
    public class ScrollerLayout extends ViewGroup {
    
        /**
         * 用于完成滚动操作的实例
         */
        private Scroller mScroller;
    
        /**
         * 判定为拖动的最小移动像素数
         */
        private int mTouchSlop;
    
        /**
         * 手机按下时的屏幕坐标
         */
        private float mXDown;
    
        /**
         * 手机当时所处的屏幕坐标
         */
        private float mXMove;
    
        /**
         * 上次触发ACTION_MOVE事件时的屏幕坐标
         */
        private float mXLastMove;
    
        /**
         * 界面可滚动的左边界
         */
        private int leftBorder;
    
        /**
         * 界面可滚动的右边界
         */
        private int rightBorder;
    
        public ScrollerLayout(Context context, AttributeSet attrs) {
            super(context, attrs);
            // 第一步,创建Scroller的实例
            mScroller = new Scroller(context);
            ViewConfiguration configuration = ViewConfiguration.get(context);
            // 获取TouchSlop值
            mTouchSlop = ViewConfigurationCompat.getScaledPagingTouchSlop(configuration);
        }
    
        @Override
        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
            int childCount = getChildCount();
            for (int i = 0; i < childCount; i++) {
                View childView = getChildAt(i);
                // 为ScrollerLayout中的每一个子控件测量大小
                measureChild(childView, widthMeasureSpec, heightMeasureSpec);
            }
        }
    
        @Override
        protected void onLayout(boolean changed, int l, int t, int r, int b) {
            if (changed) {
                int childCount = getChildCount();
                for (int i = 0; i < childCount; i++) {
                    View childView = getChildAt(i);
                    // 为ScrollerLayout中的每一个子控件在水平方向上进行布局
                    childView.layout(i * childView.getMeasuredWidth(), 0, (i + 1) * childView.getMeasuredWidth(), childView.getMeasuredHeight());
                }
                // 初始化左右边界值
                leftBorder = getChildAt(0).getLeft();
                rightBorder = getChildAt(getChildCount() - 1).getRight();
            }
        }
    
        @Override
        public boolean onInterceptTouchEvent(MotionEvent ev) {
            switch (ev.getAction()) {
                case MotionEvent.ACTION_DOWN:
                    mXDown = ev.getRawX();
                    mXLastMove = mXDown;
                    break;
                case MotionEvent.ACTION_MOVE:
                    mXMove = ev.getRawX();
                    float diff = Math.abs(mXMove - mXDown);
                    mXLastMove = mXMove;
                    // 当手指拖动值大于TouchSlop值时,认为应该进行滚动,拦截子控件的事件
                    if (diff > mTouchSlop) {
                        return true;
                    }
                    break;
            }
            return super.onInterceptTouchEvent(ev);
        }
    
        @Override
        public boolean onTouchEvent(MotionEvent event) {
            switch (event.getAction()) {
                case MotionEvent.ACTION_MOVE:
                    mXMove = event.getRawX();
                    int scrolledX = (int) (mXLastMove - mXMove);
                    if (getScrollX() + scrolledX < leftBorder) {
                        scrollTo(leftBorder, 0);
                        return true;
                    } else if (getScrollX() + getWidth() + scrolledX > rightBorder) {
                        scrollTo(rightBorder - getWidth(), 0);
                        return true;
                    }
                    scrollBy(scrolledX, 0);
                    mXLastMove = mXMove;
                    break;
                case MotionEvent.ACTION_UP:
                    // 当手指抬起时,根据当前的滚动值来判定应该滚动到哪个子控件的界面
                    int targetIndex = (getScrollX() + getWidth() / 2) / getWidth();
                    int dx = targetIndex * getWidth() - getScrollX();
                    // 第二步,调用startScroll()方法来初始化滚动数据并刷新界面
                    mScroller.startScroll(getScrollX(), 0, dx, 0);
                    invalidate();
                    break;
            }
            return super.onTouchEvent(event);
        }
    
        @Override
        public void computeScroll() {
            // 第三步,重写computeScroll()方法,并在其内部完成平滑滚动的逻辑
            if (mScroller.computeScrollOffset()) {
                scrollTo(mScroller.getCurrX(), mScroller.getCurrY());
                invalidate();
            }
        }
    }
    

    整个Scroller用法的代码都在这里了,代码并不长,一共才100多行,我们一点点来看。
    首先在ScrollerLayout的构造函数里面我们进行了上述步骤中的第一步操作,即创建Scroller的实例,由于Scroller的实例只需创建一次,因此我们把它放到构造函数里面执行。另外在构建函数中我们还初始化的TouchSlop的值,这个值在后面将用于判断当前用户的操作是否是拖动。

    接着重写onMeasure()方法和onLayout()方法,在onMeasure()方法中测量ScrollerLayout里的每一个子控件的大小,在onLayout()方法中为ScrollerLayout里的每一个子控件在水平方向上进行布局。如果有朋友对这两个方法的作用还不理解,可以参照我之前写的一篇文章 Android视图绘制流程完全解析,带你一步步深入了解View(二)

    接着重写onInterceptTouchEvent()方法, 在这个方法中我们记录了用户手指按下时的X坐标位置,以及用户手指在屏幕上拖动时的X坐标位置,当两者之间的距离大于TouchSlop值时,就认为用户正在拖动布局,然后我们就将事件在这里拦截掉,阻止事件传递到子控件当中。

    那么当我们把事件拦截掉之后,就会将事件交给ScrollerLayout的onTouchEvent()方法来处理。如果当前事件是ACTION_MOVE,说明用户正在拖动布局,那么我们就应该对布局内容进行滚动从而影响拖动事件,实现的方式就是使用我们刚刚所学的scrollBy()方法,用户拖动了多少这里就scrollBy多少。另外为了防止用户拖出边界这里还专门做了边界保护,当拖出边界时就调用scrollTo()方法来回到边界位置。

    如果当前事件是ACTION_UP时,说明用户手指抬起来了,但是目前很有可能用户只是将布局拖动到了中间,我们不可能让布局就这么停留在中间的位置,因此接下来就需要借助Scroller来完成后续的滚动操作。首先这里我们先根据当前的滚动位置来计算布局应该继续滚动到哪一个子控件的页面,然后计算出距离该页面还需滚动多少距离。接下来我们就该进行上述步骤中的第二步操作,调用startScroll()方法来初始化滚动数据并刷新界面。startScroll()方法接收四个参数,第一个参数是滚动开始时X的坐标,第二个参数是滚动开始时Y的坐标,第三个参数是横向滚动的距离,正值表示向左滚动,第四个参数是纵向滚动的距离,正值表示向上滚动。紧接着调用invalidate()方法来刷新界面。

    现在前两步都已经完成了,最后我们还需要进行第三步操作,即重写computeScroll()方法,并在其内部完成平滑滚动的逻辑 。在整个后续的平滑滚动过程中,computeScroll()方法是会一直被调用的,因此我们需要不断调用Scroller的computeScrollOffset()方法来进行判断滚动操作是否已经完成了,如果还没完成的话,那就继续调用scrollTo()方法,并把Scroller的curX和curY坐标传入,然后刷新界面从而完成平滑滚动的操作。

    现在ScrollerLayout已经准备好了,接下来我们修改activity_main.xml布局中的内容,如下所示:

    <?xml version="1.0" encoding="utf-8"?>
    <com.example.guolin.scrollertest.ScrollerLayout xmlns:android="http://schemas.android.com/apk/res/android"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        >
    
        <Button
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="This is first child view"/>
    
        <Button
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="This is second child view"/>
    
        <Button
            android:layout_width="match_parent"
            android:layout_height="100dp"
            android:text="This is third child view"/>
    
    </com.example.guolin.scrollertest.ScrollerLayout>
    

    可以看到,这里我们在ScrollerLayout中放置了三个按钮用来进行测试,其实这里不仅可以放置按钮,放置任何控件都是没问题的。

    最后MainActivity当中删除掉之前测试的代码:

    public class MainActivity extends AppCompatActivity {
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
        }
    }
    

    好的,所有代码都在这里了,现在我们可以运行一下程序来看一看效果了,如下图所示:

    怎么样,是不是感觉有点像一个简易的ViewPager了?其实借助Scroller,很多漂亮的滚动效果都可以轻松完成,比如实现图片轮播之类的特效。当然就目前这一个例子来讲,我们只是借助它来学习了一下Scroller的基本用法,例子本身有很多的功能点都没有去实现,比如说ViewPager会根据用户手指滑动速度的快慢来决定是否要翻页,这个功能在我们的例子中并没有体现出来,不过大家也可以当成自我训练来尝试实现一下。


    好的,那么本篇文章就到这里,相信通过这篇文章的学习,大家已经能够熟练掌握Scroller的使用方法了,当然ViewPager的内部实现要比这复杂得多,如果有朋友对ViewPager的源码感兴趣也可以尝试去读一下,不过一定需要非常扎实的基本功才行。

    关注我的技术公众号,每天都有优质技术文章推送。关注我的娱乐公众号,工作、学习累了的时候放松一下自己。

    微信扫一扫下方二维码即可关注:

            

    展开全文
  • Android N开发 你需要知道的一切

    万次阅读 2016-05-06 13:58:14
    Android N开发,你需要知道的一切......
  • 之所以大家会有这种错觉,是因为你将Glide 3的用法直接搬到Glide 4中去使用,结果IDE全面报错,然后大家可能就觉得Glide 4的用法完全变掉了。 其实Glide 4相对于Glide 3的变动并不大,只是你还没有了解它的变动...
  • nginx搭建图片服务器

    千次阅读 2015-07-14 14:50:47
    因此很多网站都会将图片存储从网站中分离出来,另外架构一个或多个服务器来存储图片,将图片放到一个虚拟目录中,而网页上的图片都用一个URL地址来指向这些服务器上的图片的地址,这样的话网站的性能就明显提高了,...
  •  完《水知道答案》一书后,我甚至想集资作一个中国各地水结晶的图片大全,却又担心由于污染,出不来结晶,只是各种污水丑态。中国水生态环境面临巨大压力,一方面是中国城镇化的高速发展,一方面是城市污水处理率...
  • 【Android开发经验】FaceBook推出的Android图片加载库-Fresco   欢迎关注ndroid-tech-frontier开源项目,定期翻译国外Android优质的技术、开源库、软件架构设计、测试等文章 原文链接:...
  • 安卓图片处理Picasso的解析使用

    千次阅读 2016-01-31 09:51:46
    Picasso其实是Android系统的图片下载和缓存类库,是Square开源的一个用于Android系统下载和缓存图片的项目。下面我们就来讲讲在Android开发中,Picasso有哪些特性及如何使用。简介: Picasso是Square公司开源的一...
  • Android平台利用ZXING生成二维码图片

    千次阅读 2015-01-13 16:20:32
    最近项目中用到了android手机需要根据提供的字符串生成二维码图片,之前用zxing做过二维码解码,编码还没做过,了一些demo都是用到了zxing的j2se包的内容,这个在android或者其他平台上显然无法实现,所以我们要利用...
  • 使用 AirPlay 发送图片

    千次阅读 2013-06-09 14:14:44
    原文:http://livevision.us/wordpress/2011/02/16/airplay-without-private-api/如果不是 Erica Sadun 的努力以及 AirPlayer、AirFlick、AirPDF...在这篇文章中,我将演示创建一个 iOS app( camThis)将图片发送到Appl
  • OpenCV_加载一张图片

    千次阅读 2016-12-16 13:24:41
    今天更新一个小Demo,使用opencv,从本地读取一张图片。【先代码->演示结果->分析代码】 OpenCV 版本: opencv-3.1.0 关于环境的配置略过,大家可以搜索一下其他的教程说明。代码:#include "highgui.h" int main...
  • FaceBook推出的Android图片加载库-Fresco

    千次阅读 2015-04-08 17:32:43
    FaceBook推出的Android图片加载库-Fresco 在Android设备上面,快速高效的显示图片是极为重要的。过去的几年里,我们在如何高效的存储图像这方面遇到了很多问题。图片太大,但是手机的内存却很小。每一个像素的R、G、...
  • python实现任意url转存为图片

    千次阅读 2019-01-26 15:47:32
    关键词:python、前后端分离、url返回图片 现在前后端分离的情况,比较常见,我一个Django框架后端,要为安卓、IOS、小程序、...第一步就是咱们的本职,不多描述了,主要看看第二步如何实现。 在网上找了好多的工...
  • Fresco-FaceBook推出的Android图片加载库

    千次阅读 2016-06-23 14:02:22
    在Android设备上面,快速高效的显示图片是极为重要的。过去的几年里,我们在如何高效的存储图像这方面遇到了很多问题。图片太大,但是手机的内存却很小。每一个像素的R、G、B和alpha通道总共要占用4byte的空间。如果...
  • 考虑到模板样式的多端一致性、多样性、可配置性等特点,所以生成分享图片的功能有java服务端进行实现。刚开始接到任务的时候,想着这么简单的东西,应该很快就能完成,于是保守的估计了一天用来开发此功能;2.二维码...
  • qt不支持jpg格式图片

    千次阅读 2015-04-27 10:24:22
    在编译 macosx客户端时,选择了一个jpg格式的图片作为背景图片,在cmake编译后一切都正常,但是在genapp.sh脚本进行xcode编译和qtdeploy打包后,其他都正常,但是背景图片却显示不出来。 一开始我怀疑是打包...
  • 彻底搞懂Python一切皆对象!!!

    千次阅读 2019-06-03 00:27:26
    一切皆对象前言简述Python的一等公民type、object和class的关系总结 前言 犹记得当初学习Python的时候,对于Python一切皆对象很是懵逼,因为Python是面向对象的动态型语言,而在函数及高阶函数的应用中,如若对于...
  • 在泰国举行的谷歌开发者论坛上,谷歌为我们介绍了一个名叫 Glide 的图片加载库,作者是bumptech。这个库被广泛的运用在google的开源项目中,包括2014年google I/O大会上发布的官方app。   二:使用  1...
  • 图片格式错误导致IE无法显示图片

    千次阅读 2008-06-29 00:04:00
    转载请注明出处:http://blog.csdn.net/soartju作者:高飞本文说的是...由于只差一个图片,所有的定制代码都已经写好,包括需要使用的这个图片的引用,已经在代码中定义引用了“title.png”。后来美工给的图片是“tit
  • 十一的时候,老司机再次发车,雅虎开源了其色情图片检测神经网络。老司机们得到消息第一时间就搭建了自己的检测平台。大家纷纷表示根本找不到数据测试的同时不忘言不由衷地抱怨以前写代码累脑袋,现在写代码累腰。 ...
  • 拥抱SVG:苦恼于图片适配 in Android?

    千次阅读 2016-09-19 20:54:31
    为什么我的图片在这台手机上显示地好好的完全符合设计图的要求结果换到另一台手机上就变形了?Oh my god ! 以前为了解决图片在不同的分辨率的屏幕上显示不一致的问题,通常我们会采取两种方式:一是根据不同的...
  • ps去除图片复杂的水印

    万次阅读 2018-02-17 14:38:41
    大多情况下,图片水印都是某网站的LOGO或网址之类的,我们是有可能得到的(如是ps教程论坛等论坛的水印,你只要上传一个大的透明背景的图片上论坛,就能简单获得他们的水印)。首先需要理解一下两个图层不透明度的混合...
  • 一次ionic3上传图片的经历

    千次阅读 2017-12-08 14:59:22
    一次ionic3上传图片的经历概述 本文只是回顾这样一次经历,所以不会贴大段大段代码 ...最先想到的是手机输入法的手写功能,去讯飞了一下,基本上都是说的语音识别(因为最近人工智能很火),没找到手写相关的API。正
  • 那么它的显示结果就是这个样子:  所以,这就是CircleImageView的使用方式了,也是github上hdodenhof大神的原文使用教程。因为使用比较简单,我就直接copy了一份过来。本文以简单的使用方式作为铺垫,为了...
  • android URI 和 UIL 图片加载问题

    千次阅读 2015-12-02 14:10:31
    缓存的依据: UIL根据图片的URI获取缓存在磁盘文件的MD5值,起来一切都顺利成章. 然而,问题出现了: 当网络上同一路径下的图片换了内容而没有换名字的话就会出现问题了: 不管内容怎么换,还是加载之前的图片 从缓存...
  • 最详细的Android图片压缩解释

    千次阅读 2016-12-09 13:52:00
    如题,多种压缩方式常用的有尺寸压缩、质量压缩以及通过JNI调用libjpeg库来进行压缩,三种方式结合使用实现指定图片内存大小,清晰度达到最优,下面就先分别介绍下这几种压缩方式。 原文出处:...
  • Python爬取淘宝图片

    万次阅读 2017-10-19 03:57:06
    爬取淘女郎模特图片与相关信息

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 75,312
精华内容 30,124
关键字:

一切看结果图片