Android 图片三级缓存LruCache实现

痛苦总是走在勇气前面。太阳底下,大概大家都一样吧。

前言

这是我面试时候遇到的一个问题,虽然之前了解过,但是并没有自己实现过。当时面试官叫我5分钟内设计并实现出来。当时比较嫩,直接说不会了,面试官有点尴尬。虽然已经成功拿到了offer了,但是还是要将不是很会的东西补补。

设计过程

在这里插入图片描述

对象关系图

在这里插入图片描述从上图可以看出,通过门面模式,将MemoryCacheObservableNetCacheObservableDiskCacheObservable的实例化和使用都放在了Source中,分别将调用封装为方法memorynetworkdisk

最后,通过在RxImageLoader中实例化Source对象,利用RxJava的操作符concatfirst将三种缓存策略按照优先级(内存,硬盘,网络)排序,将第一个命中的cache缓存返回。再将返回的数据转换为bitmap,加载到适当的ImageView中。

代码架构

RxImageLoader

完整源码请见后面附录。

public class RxImageLoader {
    /**
     * 缓存的数据,key:imageView的hashcode,value:图片资源
     */
    private static final Map<Integer, String> CACHE_KEYS_MAP =
            Collections.synchronizedMap(new HashMap<>());
    private static volatile RxImageLoader instance;
    private Sources sources;
	
	// 单例模式的构造函数,实例化必备参数
    private RxImageLoader(Context context) {}
    // 获取单例
    public static RxImageLoader getInstance(Context context) {}
    // 加载图片
    public Observable<Data> loadImage(ImageView imageView, String url) {}
}

Sources

完整源码请见后面附录。

public class Sources {
    private MemoryCacheObservable memoryCacheObservable;
    private DiskCacheObservable diskCacheObservable;
    private NetCacheObservable netCacheObservable;

    // 初始化数据
    public Sources(Context context) {}

    // 内存缓存
    public Observable<Data> memory(String url) {}
    // 磁盘缓存
    public Observable<Data> disk(String url) {}
    // 网络
    public Observable<Data> network(String url) {}
    // RxJava的Transformer
    private Observable.Transformer<Data, Data> logSource(final String source) {}
}

MemoryCacheObservable

该章节的实现是图片三级缓存的内存缓存部分。

由于内存的空间是有限的,因此为了充分利用内存,应该对内存保存采用一定的策略。常用的是采用LRU算法将长时间没有使用的Bitmap对象从内存给中移除。

该部分的实现采用的是Android开发库中的android.util.LruCache工具来实现。

public class MemoryCache<T> extends LruCache<T, Bitmap> {

    /**
     * 设置缓冲池的最大尺寸
     */
    public MemoryCache(int maxSize) {
        super(maxSize);
    }

    @Override
    protected int sizeOf(T key, Bitmap value) {
        int size = 0;
        if (value != null) {
            // 计算图片的大小
            size = value.getRowBytes() * value.getHeight();
        }
        return size;
    }
}

到这里,我们顺便分析下LruCache的移除机制。

public final V put(K key, V value) {
    // 键值对不能为空
    if (key == null || value == null) {
        throw new NullPointerException("key == null || value == null");
    }

    // 被替换的元素
    V previous;
    synchronized (this) {
        putCount++;
        // 缓冲池增大尺寸
        size += safeSizeOf(key, value);
        // 这里的map采用的LinkedHashMap,在put后会将被替换的元素返回
        previous = map.put(key, value);
        if (previous != null) {
            // 减去被替换的元素的尺寸
            size -= safeSizeOf(key, previous);
        }
    }

    // 销毁
    if (previous != null) {
        entryRemoved(false, key, previous, value);
    }

    // 开始判断当前尺寸是否超过缓存池定义的尺寸,如果超过了,就将最老的元素移除并销毁
    trimToSize(maxSize);
    return previous;
}

执行了put之后,需要对缓冲池的大小进行调整。其实就是判断当前的容量是否超过了缓冲池定义的最大尺寸,如果超过了就将最老的元素移除并销毁。

public void trimToSize(int maxSize) {
    while (true) {
        K key;
        V value;
        synchronized (this) {
            if (size < 0 || (map.isEmpty() && size != 0)) {
                throw new IllegalStateException(getClass().getName()
                        + ".sizeOf() is reporting inconsistent results!");
            }

            // 小于缓冲池最大尺寸的时候退出
            if (size <= maxSize) {
                break;
            }

            // 通过LinkedHashMap的eldest方法,返回最老的元素
            Map.Entry<K, V> toEvict = map.eldest();
            if (toEvict == null) {
                break;
            }

            key = toEvict.getKey();
            value = toEvict.getValue();
            // 移除并调整size
            map.remove(key);
            size -= safeSizeOf(key, value);
            evictionCount++;
        }

        // 销毁
        entryRemoved(true, key, value, null);
    }
}

最后,我们的MemoryCacheObservable实现效果如下:

public class MemoryCacheObservable extends CacheObservable {

    /**
     * 缓存大小设置为24M
     */
    private static final int CACHE_SIZE = (24 * 1024 * 1024);

    private MemoryCache<String> mCache = new MemoryCache<>(CACHE_SIZE);

    @Override
    public Observable<Data> getObservable(String url) {
        return Observable.just(url)
                .map(s -> new Data(mCache.get(url), url));
    }

    public void putData(Data data) {
        mCache.put(data.url, data.bitmap);
    }
}

DiskCacheObservable

这是图片三级缓存的第二部曲,需要读取本地保存的图片,并将图片载入到内存缓存中。

public class DiskCacheObservable extends CacheObservable {

    private static final String PNG = "PNG";

    private File mCacheFile;

    public DiskCacheObservable(Context context) {
        mCacheFile = context.getCacheDir();
    }

    @Override
    public Observable<Data> getObservable(String url) {
        return Observable.just(url)
                .map(s -> {
                    Log.i("LRU", "read file from disk");
                    File f = getFile(url);
                    return new Data(f, url);
                });
    }

    private File getFile(String url) {
        url = url.replaceAll(File.separator, "-");
        return new File(mCacheFile, url);
    }

    /**
     * 将下载到的图片保存到外存中
     *
     * @param data 图片数据
     */
    public void putData(Data data) {
        // todo:保存图片
        Observable.just(data).map(data1 -> {
            File f = getFile(data1.url);
            OutputStream out = null;
            try {
                out = new FileOutputStream(f);
                Bitmap.CompressFormat format;
                if (data1.url.endsWith(PNG) || data1.url.endsWith(PNG.toLowerCase())) {
                    format = Bitmap.CompressFormat.PNG;
                }
                // 这里并没有考虑到其他格式的处理
                else {
                    format = Bitmap.CompressFormat.JPEG;
                }
                data1.bitmap.compress(format, 100, out);
                out.flush();
                out.close();
            } catch (IOException e) {
                // 使用以下方法将异常封装为RuntimeException并抛出来,
                // 这样异常发生是RxJava就会捕捉到该异常并通过onError方法将异常发送给订阅者。
                throw Exceptions.propagate(e);
            } finally {
                if (out != null) {
                    try {
                        out.close();
                    } catch (IOException e) {
                        throw Exceptions.propagate(e);
                    }
                }
            }
            return data1;
        }).subscribeOn(Schedulers.io()).subscribe();
    }
}

NetCacheObservable

当我们的内存缓存和硬盘缓存都不能找到的时候,我们只能通过网络拉取来完成了,拉取完毕之后,需要将图片保存到磁盘并载入到内存中。

public class NetCacheObservable extends CacheObservable {

    @Override
    public Observable<Data> getObservable(String url) {
        return Observable.just(url)
                .map(s -> {
                    Bitmap bitmap;
                    InputStream inputStream = null;
                    Log.i("LRU", "get img on net:" + url);
                    try {
                        URLConnection con = new URL(url).openConnection();
                        inputStream = con.getInputStream();
                        bitmap = BitmapFactory.decodeStream(inputStream);
                    } catch (IOException e) {
                        throw Exceptions.propagate(e);
                    } finally {
                        if (inputStream != null) {
                            try {
                                inputStream.close();
                            } catch (IOException e) {
                                throw Exceptions.propagate(e);
                            }
                        }
                    }
                    return new Data(bitmap, url);
                }).subscribeOn(Schedulers.io());
    }
}

代码附录

Data

public class Data {
    public Bitmap bitmap;
    public String url;

    public Data(Bitmap bitmap, String url) {
        this.bitmap = bitmap;
        this.url = url;
    }

    public Data(File f, String url) {
        if (f != null && f.exists()) {
            this.url = url;
            FileInputStream stream = null;
            try {
                stream = new FileInputStream(f);
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }
            bitmap = BitmapFactory.decodeStream(stream);
        }
    }

    /**
     * 判断缓存数据是否存在
     *
     * @return true:缓存存在
     */
    public boolean isAvailable() {
        return url != null && bitmap != null;
    }
}

RxImageLoader

public class RxImageLoader {
    /**
     * 缓存的数据,key:imageView的hashcode,value:图片资源名称
     */
    private static final Map<Integer, String> CACHE_KEYS_MAP =
            Collections.synchronizedMap(new HashMap<>());
    private static volatile RxImageLoader instance;
    private Sources sources;

    private RxImageLoader(Context context) {
        if (sources == null) {
            sources = new Sources(context.getApplicationContext());
        }
    }

    public static RxImageLoader getInstance(Context context) {
        if (instance == null) {
            synchronized (RxImageLoader.class) {
                if (instance == null) {
                    instance = new RxImageLoader(context);
                }
            }
        }
        return instance;
    }

    public Observable<Data> loadImage(ImageView imageView, String url) {
        if (imageView != null) {
            CACHE_KEYS_MAP.put(imageView.hashCode(), url);
        }
        return Observable
                // 按照优先级获取图片:内存缓存->磁盘缓存->网络
                .concat(sources.memory(url), sources.disk(url), sources.network(url))
                // 选取第一个命中的缓存
                .first(data -> data != null && data.isAvailable() && url.equals(data.url))
                .observeOn(AndroidSchedulers.mainThread())
                // 填充数据
                .doOnNext(data -> {
                    if (imageView == null) {
                        return;
                    }
                    int hashCode = imageView.hashCode();
                    String cacheUrl = CACHE_KEYS_MAP.get(hashCode);
                    if (url.equals(cacheUrl)) {
                        imageView.setImageBitmap(data.bitmap);
                    }
                });
    }
}

Sources

public class Sources {
    private MemoryCacheObservable memoryCacheObservable;
    private DiskCacheObservable diskCacheObservable;
    private NetCacheObservable netCacheObservable;

    public Sources(Context context) {
        // 初始化缓存数据
        memoryCacheObservable = new MemoryCacheObservable();
        diskCacheObservable = new DiskCacheObservable(context);
        netCacheObservable = new NetCacheObservable();
    }

    public Observable<Data> memory(String url) {
        return memoryCacheObservable.getObservable(url)
                .compose(logSource("MEMORY"));
    }

    public Observable<Data> disk(String url) {
        return diskCacheObservable.getObservable(url)
                .compose(logSource("DISK"))
                .filter(Data::isAvailable)
                // 将图片保存在外存中
                .doOnNext(data -> memoryCacheObservable.putData(data));
    }

    public Observable<Data> network(String url) {
        return netCacheObservable.getObservable(url)
                .filter(Data::isAvailable)
                // 将图片保存到内存和外存中
                .doOnNext(data -> {
                    memoryCacheObservable.putData(data);
                    diskCacheObservable.putData(data);
                });
    }

    private Observable.Transformer<Data, Data> logSource(final String source) {
        return dataObservable -> dataObservable.doOnNext(data -> {
            if (data != null && data.isAvailable()) {
                Log.i("LRU", source + " has the data!");
            } else {
                Log.i("LRU", source + " not has the data!");
            }
        });
    }
}

测试

由于我们已经在代码中打了Log信息了,仅仅需要查看Log信息就可以知道是否实现成功了。

  • 代码在章后。

测试效果

第一次点进去——这个时候并没有磁盘缓存和内存缓存。

MEMORY not has the data!
read file from disk
DISK not has the data!
MEMORY not has the data!
read file from disk
DISK not has the data!
get img on net:https://img5.duitang.com/uploads/item/201411/07/20141107164412_v284V.jpeg
MEMORY not has the data!
read file from disk
DISK not has the data!
MEMORY not has the data!
read file from disk
DISK not has the data!
MEMORY not has the data!
read file from disk
DISK not has the data!
get img on net:http://pic28.photophoto.cn/20130818/0020033143720852_b.jpg
get img on net:http://pic17.nipic.com/20111023/8104044_230939695000_2.jpg
get img on net:http://pic3.nipic.com/20090527/1242397_102231006_2.jpg
get img on net:http://pic26.nipic.com/20121227/10193203_131357536000_2.jpg
bitmap size:561152
加载完成了!
bitmap size:271122
加载完成了!
bitmap size:655360
加载完成了!
bitmap size:699392
加载完成了!
bitmap size:701440
加载完成了!

按返回键退出页面,然后重新进去。(需要自行写多一个页面,跳转到LruActivity)

MEMORY has the data!
MEMORY has the data!
MEMORY has the data!
MEMORY has the data!
MEMORY has the data!
bitmap size:561152
加载完成了!
bitmap size:271122
加载完成了!
bitmap size:655360
加载完成了!
bitmap size:699392
加载完成了!
bitmap size:701440
加载完成了!

退出整个应用,重新进去该页面。

MEMORY not has the data!
read file from disk
DISK has the data!
MEMORY not has the data!
read file from disk
DISK has the data!
MEMORY not has the data!
read file from disk
DISK has the data!
MEMORY not has the data!
read file from disk
DISK has the data!
MEMORY not has the data!
read file from disk
DISK has the data!
bitmap size:561152
加载完成了!
bitmap size:271122
加载完成了!
bitmap size:655360
加载完成了!
bitmap size:699392
加载完成了!
bitmap size:701440
加载完成了!

测试完毕,成功!O(∩_∩)O

测试代码

public class LruActivity extends ListActivity {
    ArrayList<String> contents = new ArrayList<>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        contents.add("http://pic28.photophoto.cn/20130818/0020033143720852_b.jpg");
        contents.add("https://img5.duitang.com/uploads/item/201411/07/20141107164412_v284V.jpeg");
        contents.add("http://pic3.nipic.com/20090527/1242397_102231006_2.jpg");
        contents.add("http://pic17.nipic.com/20111023/8104044_230939695000_2.jpg");
        contents.add("http://pic26.nipic.com/20121227/10193203_131357536000_2.jpg");
        getListView().setAdapter(new RxAdapter());
    }

    private void startSubscribe(ImageView img, String url) {
        RxImageLoader.getInstance(this)
                .loadImage(img, url)
                .subscribe(new Subscriber<Data>() {
                    @Override
                    public void onCompleted() {
                        Log.i("LRU", "加载完成了!");
                    }

                    @Override
                    public void onError(Throwable e) {
                        e.printStackTrace();
                    }

                    @Override
                    public void onNext(Data data) {
                        Log.i("LRU", "bitmap size:" + data.bitmap.getHeight() * data.bitmap.getWidth());
                    }
                });
    }

    class RxAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return contents.size();
        }

        @Override
        public String getItem(int i) {
            return contents.get(i);
        }

        @Override
        public long getItemId(int i) {
            return i;
        }

        @Override
        public View getView(int i, View view, ViewGroup viewGroup) {
            ViewHolder holder;
            if (view == null) {
                holder = new ViewHolder();
                view = View.inflate(LruActivity.this,
                        R.layout.activity_lru, null);
                holder.img = (ImageView) view.findViewById(R.id.img);
                view.setTag(holder);
            } else {
                holder = (ViewHolder) view.getTag();
            }
            holder.img.setImageResource(R.mipmap.ic_launcher);
            startSubscribe(holder.img, getItem(i));
            return view;
        }
    }

    class ViewHolder {
        ImageView img;
    }
}

附录

发布了181 篇原创文章 · 获赞 217 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/Notzuonotdied/article/details/86311822