教你写Android ImageLoader框架之图片缓存 (完结篇)

分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow

也欢迎大家转载本篇文章。分享知识,造福人民,实现我们中华民族伟大复兴!

                       

教你写Android ImageLoader框架系列博文中,我们从基本架构到具体实现已经更新了大部分的内容。今天,我们来讲最后一个关键点,即图片的缓存。为了用户体验,通常情况下我们都会将已经下载的图片缓存起来,一般来说内存和本地都会有图片缓存。那既然是框架,必然需要有很好的定制性,这让我们又自然而然的想到了抽象。下面我们就一起来看看缓存的实现吧。     

缓存接口

教你写Android ImageLoader框架之图片加载与加载策略我们聊到了Loader,然后阐述了AbsLoader的基本逻辑,其中就有图片缓存。因此AbsLoader中必然含有缓存对象的引用。我们看看相关代码:

/** * @author mrsimple */public abstract class AbsLoader implements Loader {    /**     * 图片缓存     */    private static BitmapCache mCache = SimpleImageLoader.getInstance().getConfig().bitmapCache;    // 代码省略}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

AbsLoader中定义了一个static的BitmapCache对象,这个就是图片缓存对象。那为什么是static呢?因为不管Loader有多少个,缓存对象都应该是共享的,也就是缓存只有一份。说了那么多,那我们先来了解一下BitmapCache吧。      

public interface BitmapCache {    public Bitmap get(BitmapRequest key);    public void put(BitmapRequest key, Bitmap value);    public void remove(BitmapRequest key);}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

BitmapCache很简单,只声明了获取、添加、移除三个方法来操作图片缓存。这里有依赖了一个BitmapRequest类,这个类代表了一个图片加载请求,该类中有该请求对应的ImageView、图片uri、显示Config等属性。在缓存这块我们主要要使用图片的uri来检索缓存中是否含有该图片,缓存以图片的uri为key,Bitmap为value来关联存储。另外需要BitmapRequest的ImageView宽度和高度,以此来按尺寸加载图片。          

定义BitmapCache接口还是为了可扩展性,面向接口的编程的理念又再一次的浮现在你面前。如果是你,你会作何设计呢?自己写代码来练习一下吧,看看自己作何考虑,如果实现,这样你才会从中有更深的领悟。

内存缓存

既然是框架,那就需要接受用户各种各样的需求。但通常来说框架会有一些默认的实现,对于图片缓存来说内存缓存就其中的一个默认实现,它会将已经加载的图片缓存到内存中,大大地提升图片重复加载的速度。内存缓存我们的策略是使用LRU算法,直接使用了support.v4中的LruCache类,相关代码如下。

/** * 图片的内存缓存,key为图片的uri,值为图片本身 *  * @author mrsimple */public class MemoryCache implements BitmapCache {    private LruCache<String, Bitmap> mMemeryCache;    public MemoryCache() {        // 计算可使用的最大内存        final int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);        // 取4分之一的可用内存作为缓存        final int cacheSize = maxMemory / 4;        mMemeryCache = new LruCache<String, Bitmap>(cacheSize) {            @Override            protected int sizeOf(String key, Bitmap bitmap) {                return bitmap.getRowBytes() * bitmap.getHeight() / 1024;            }        };    }    @Override    public Bitmap get(BitmapRequest key) {        return mMemeryCache.get(key.imageUri);    }    @Override    public void put(BitmapRequest key, Bitmap value) {        mMemeryCache.put(key.imageUri, value);    }    @Override    public void remove(BitmapRequest key) {        mMemeryCache.remove(key.imageUri);    }}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

就是简单的实现了BitmapCache接口,然后内部使用LruCache类实现内存缓存。比较简单,就不做说明了。

sd卡缓存

对于图片缓存,内存缓存是不够的,更多的需要是将图片缓存到sd卡中,这样用户在下次进入app时可以直接从本地加载图片,避免重复地从网络上读取图片数据,即耗流量,用户体验又不好。sd卡缓存我们使用了Jake Wharton的DiskLruCache类,我们的sd卡缓存类为DiskCache,代码如下 :

public class DiskCache implements BitmapCache {    /**     * 1MB     */    private static final int MB = 1024 * 1024;    /**     * cache dir     */    private static final String IMAGE_DISK_CACHE = "bitmap";    /**     * Disk LRU Cache     */    private DiskLruCache mDiskLruCache;    /**     * Disk Cache Instance     */    private static DiskCache mDiskCache;    /**     * @param context     */    private DiskCache(Context context) {        initDiskCache(context);    }    public static DiskCache getDiskCache(Context context) {        if (mDiskCache == null) {            synchronized (DiskCache.class) {                if (mDiskCache == null) {                    mDiskCache = new DiskCache(context);                }            }        }        return mDiskCache;    }    /**     * 初始化sdcard缓存     */    private void initDiskCache(Context context) {        try {            File cacheDir = getDiskCacheDir(context, IMAGE_DISK_CACHE);            if (!cacheDir.exists()) {                cacheDir.mkdirs();            }            mDiskLruCache = DiskLruCache                    .open(cacheDir, getAppVersion(context), 1, 50 * MB);        } catch (IOException e) {            e.printStackTrace();        }    }    /**     * 获取sd缓存的目录,如果挂载了sd卡则使用sd卡缓存,否则使用应用的缓存目录。     * @param context Context     * @param uniqueName 缓存目录名,比如bitmap     * @return     */    public File getDiskCacheDir(Context context, String uniqueName) {        String cachePath;        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) {            Log.d("", "### context : " + context + ", dir = " + context.getExternalCacheDir());            cachePath = context.getExternalCacheDir().getPath();        } else {            cachePath = context.getCacheDir().getPath();        }        return new File(cachePath + File.separator + uniqueName);    }        @Override    public synchronized Bitmap get(final BitmapRequest bean) {        // 图片解析器        BitmapDecoder decoder = new BitmapDecoder() {            @Override            public Bitmap decodeBitmapWithOption(Options options) {                final InputStream inputStream = getInputStream(bean.imageUriMd5);                Bitmap bitmap = BitmapFactory.decodeStream(inputStream, null,                        options);                IOUtil.closeQuietly(inputStream);                return bitmap;            }        };        return decoder.decodeBitmap(bean.getImageViewWidth(),                bean.getImageViewHeight());    }    private InputStream getInputStream(String md5) {        Snapshot snapshot;        try {            snapshot = mDiskLruCache.get(md5);            if (snapshot != null) {                return snapshot.getInputStream(0);            }        } catch (IOException e) {            e.printStackTrace();        }        return null;    }    public void put(BitmapRequest key, Bitmap value) {        // 代码省略     }    public void remove(BitmapRequest key) {        // 代码省略    }}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83
  • 84
  • 85
  • 86
  • 87
  • 88
  • 89
  • 90
  • 91
  • 92
  • 93
  • 94
  • 95
  • 96
  • 97
  • 98
  • 99
  • 100
  • 101
  • 102
  • 103
  • 104
  • 105
  • 106
  • 107
  • 108
  • 109
  • 110
  • 111
  • 112
  • 113
  • 114
  • 115

代码比较简单,也就是实现BitmapCache,然后包装一下DiskLruCache类的方法实现图片文件的增加、删除、获取方法。这里给大家介绍一个类,是我为了简化图片按ImageView尺寸加载的辅助类,即BitmapDecoder。

BitmapDecoder

BitmapDecoder是一个按ImageView尺寸加载图片的辅助类,一般我加载图片的过程是这样的: 
1. 创建BitmapFactory.Options options,设置options.inJustDecodeBounds = true,使得只解析图片尺寸等信息;
2. 根据ImageView的尺寸来检查是否需要缩小要加载的图片以及计算缩放比例;
3. 设置options.inJustDecodeBounds = false,然后按照options设置的缩小比例来加载图片.

BitmapDecoder类使用decodeBitmap方法封装了这个过程 ( 模板方法噢 ),用户只需要实现一个子类,并且覆写BitmapDecoder的decodeBitmapWithOption实现图片加载即可完成这个过程(参考DiskCache中的get方法)。代码如下 :

/** * 封装先加载图片bound,计算出inSmallSize之后再加载图片的逻辑操作 *  * @author mrsimple */public abstract class BitmapDecoder {    /**     * @param options     * @return     */    public abstract Bitmap decodeBitmapWithOption(Options options);    /**     * @param width 图片的目标宽度     * @param height 图片的目标高度     * @return     */    public Bitmap decodeBitmap(int width, int height) {        // 如果请求原图,则直接加载原图        if (width <= 0 || height <= 0) {            return decodeBitmapWithOption(null);        }        // 1、获取只加载Bitmap宽高等数据的Option, 即设置options.inJustDecodeBounds = true;        BitmapFactory.Options options = getJustDecodeBoundsOptions();        // 2、通过options加载bitmap,此时返回的bitmap为空,数据将存储在options中        decodeBitmapWithOption(options);        // 3、计算缩放比例, 并且将options.inJustDecodeBounds设置为false;        calculateInSmall(options, width, height);        // 4、通过options设置的缩放比例加载图片        return decodeBitmapWithOption(options);    }    /**     * 获取BitmapFactory.Options,设置为只解析图片边界信息     */    private Options getJustDecodeBoundsOptions() {        //        BitmapFactory.Options options = new BitmapFactory.Options();        // 设置为true,表示解析Bitmap对象,该对象不占内存        options.inJustDecodeBounds = true;        return options;    }    protected void calculateInSmall(Options options, int width, int height) {        // 设置缩放比例        options.inSampleSize = computeInSmallSize(options, width, height);        // 图片质量        options.inPreferredConfig = Config.RGB_565;        // 设置为false,解析Bitmap对象加入到内存中        options.inJustDecodeBounds = false;        options.inPurgeable = true;        options.inInputShareable = true;    }    private int computeInSmallSize(BitmapFactory.Options options, int reqWidth, int reqHeight) {        // Raw height and width of image        final int height = options.outHeight;        final int width = options.outWidth;        int inSampleSize = 1;        if (height > reqHeight || width > reqWidth) {            // Calculate ratios of height and width to requested height and            // width            final int heightRatio = Math.round((float) height / (float) reqHeight);            final int widthRatio = Math.round((float) width / (float) reqWidth);            inSampleSize = heightRatio < widthRatio ? heightRatio : widthRatio;            final float totalPixels = width * height;            // Anything more than 2x the requested pixels we'll sample down            // further            final float totalReqPixelsCap = reqWidth * reqHeight * 2;            while (totalPixels / (inSampleSize * inSampleSize) > totalReqPixelsCap) {                inSampleSize++;            }        }        return inSampleSize;    }}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70
  • 71
  • 72
  • 73
  • 74
  • 75
  • 76
  • 77
  • 78
  • 79
  • 80
  • 81
  • 82
  • 83

在decodeBitmap中,我们首先创建BitmapFactory.Options对象,并且设置options.inJustDecodeBounds = true,然后第一次调用decodeBitmapWithOption(options),使得只解析图片尺寸等信息;然后调用calculateInSmall方法,该方法会调用computeInSmallSize来根据ImageView的尺寸来检查是否需要缩小要加载的图片以及计算缩放比例,在calculateInSmall方法的最后将 options.inJustDecodeBounds = false,使得下次再次decodeBitmapWithOption(options)时会加载图片;那最后一步必然就是调用decodeBitmapWithOption(options)啦,这样图片就会按照按照options设置的缩小比例来加载图片了。      

我们使用这个辅助类封装了这个麻烦、重复的过程,在一定程度上简化了代码,也使得代码的可复用性更高,也是模板方法模式的一个较好的示例。      

二级缓存

有了内存和sd卡缓存,其实这还不够。我们的需求很可能就是这个缓存会同时有内存和sd卡缓存,这样上述两种缓存的优点我们就会具备,这里我们把它称为二级缓存。看看代码吧,也很简单。     

/** * 综合缓存,内存和sd卡双缓存 *  * @author mrsimple */public class DoubleCache implements BitmapCache {    DiskCache mDiskCache;    MemoryCache mMemoryCache = new MemoryCache();    public DoubleCache(Context context) {        mDiskCache = DiskCache.getDiskCache(context);    }    @Override    public Bitmap get(BitmapRequest key) {        Bitmap value = mMemoryCache.get(key);        if (value == null) {            value = mDiskCache.get(key);            saveBitmapIntoMemory(key, value);        }        return value;    }    private void saveBitmapIntoMemory(BitmapRequest key, Bitmap bitmap) {        // 如果Value从disk中读取,那么存入内存缓存        if (bitmap != null) {            mMemoryCache.put(key, bitmap);        }    }    @Override    public void put(BitmapRequest key, Bitmap value) {        mDiskCache.put(key, value);        mMemoryCache.put(key, value);    }    @Override    public void remove(BitmapRequest key) {        mDiskCache.remove(key);        mMemoryCache.remove(key);    }}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43

其实就是封装了内存缓存和sd卡缓存的相关操作嘛~ 那我就不要再费口舌了

自定义缓存

缓存是有很多实现策略的,既然我们要可扩展性,那就要允许用户注入自己的缓存实现。只要你实现BitmapCache,就可以将它通过ImageLoaderConfig注入到ImageLoader内部。

    private void initImageLoader() {        ImageLoaderConfig config = new ImageLoaderConfig()                .setLoadingPlaceholder(R.drawable.loading)                .setNotFoundPlaceholder(R.drawable.not_found)                .setCache(new MyCache())        // 初始化        SimpleImageLoader.getInstance().init(config);    }
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

MyCache.java    

// 自定义缓存实现类public class MyCache implements BitmapCache {    // 代码    @Override    public Bitmap get(BitmapRequest key) {        // 你的代码    }    @Override    public void put(BitmapRequest key, Bitmap value) {        // 你的代码      }    @Override    public void remove(BitmapRequest key) {        // 你的代码    }}
   
   
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

Github地址

SimpleImageLoader

总结

ImageLoader系列到这里就算结束了,我们从基本架构、具体实现、设计上面详细的阐述了一个简单、可扩展性较好的ImageLoader实现过程,希望大家看完这个系列之后能够自己去实现一遍,这样你会发现一些具体的问题,领悟能够更加的深刻。如果你在看这系列博客的过程中,真的能够从中体会到面向对象的基本原则、设计思考等东西,而不是说”我擦,我又找到了一个可以copy来用的ImageLoader”,那我就觉得我做的这些分享到达目的了。

           

给我老师的人工智能教程打call!http://blog.csdn.net/jiangjunshow

这里写图片描述

猜你喜欢

转载自blog.csdn.net/qq_43661309/article/details/84102119