实现自己的ImageLoader(2)-----LruCache与DiskLruCache缓存详解

上篇博客说道,一个优秀的ImageLoader应该具有内存缓存能力和磁盘缓存能力,而缓存能力该怎么实现?

这里就要引出LruCache与DiskLruCache,先说LruCache

LruCache是Android的一个缓存类,通常用于实现内存缓存

public class LruCache<K, V>
LruCache有一个LinkedHashMap用于存储我们的缓存

private final LinkedHashMap<K, V> map;
当我们创建新的LruCache时

		mMemoryCache = new LruCache<String, Bitmap>(cacheSize) {
			protected int sizeOf(String key, Bitmap value) {
				return value.getRowBytes() * value.getHeight() / 1024;
			};
		};
需要传递一个参数以及重写里面的sizeof方法

    public LruCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<K, V>(0, 0.75f, true);
    }
这里我们可以看到LruCache初始化时创建了LinkedHashMap

而sizeof方法在LruCache的代码里是默认返回1

    protected int sizeOf(K key, V value) {
        return 1;
    }
sizeof必须要重写,因为系统是通过sizeof来确定储存的Value的大小,得到了Value的大小系统才可以给Value分配空间,不然无法储存,内部逻辑我们等会再看。

我们传入的Value是一个Bitmap对象,Bitmap的大小等于value.getRowBytes() * value.getHeight(),因为分配内存时的单位是KB,所以我们还需要除以1024统一单位

现在我们已经拿到了一个LruCache对象,接下来我们就要往里添加数据了,调用它的put方法

    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);
            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方法的逻辑

1.先判断key还有value是不是空,空就报错

2.增加size,其中safeSizeof调用我们重写的sizeof方法

3.向map里加入数据

4.判断previous是不是为空,这里我说一下,因为LinkedHashmap继承Hashmap,HashMap一个很重要的性质就是key不能重复,当key重复时HashMap会覆盖原来的key,并把先前的Value返回,如果key没有重复,那么则返回null,具体源码可以看这个博客http://www.cnblogs.com/children/archive/2012/10/02/2710624.html

5.当我们key重复时,size要把原来加上去的长度减回来,再调用entryRemoved(false, key, previous, value);方法,这个方法是一个空实现,主要是用于一些资源回收的工作,如果有必要的话我们要重写这个方法

6.调用trimToSize方法,这个方法是LRU(Least Recently Used)算法的具体实现。设想一下,当我们缓存空间满了的时候,我们还想往里加入缓存怎么办,我们是不是得删除一些不那么重要的缓存(要是删除了重要的缓存,用户体验会不好),而这些个缓存的重要度则通过使用次数来反应。下面是trimTosize的源码

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

                Map.Entry<K, V> toEvict = map.eldest();
                if (toEvict == null) {
                    break;
                }

                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= safeSizeOf(key, value);
                evictionCount++;
            }

            entryRemoved(true, key, value, null);
        }
    }
我们可以看出,trimTosize会先判断我们的size是不是超过maxsize了,如果超过了就删除最少使用的缓存,删除完再判断,如果空间不够还要再删,直到满足为止。

以上就是加入缓存的逻辑,而get方法的逻辑没有什么好说了,跟HashMap差不多。


接下来我们看看DiskLruCache(长文慎入)

DiskLruCache是用于磁盘缓存的一个类,如果我们需要把缓存缓存到sd卡里,那么就要用到DiskLruCache。有趣的是DiskLruCache并不属于Android源码的一部分,但是得到了Android官方文档的推荐,不知道是哪位大神造的轮子。鉴于Google已经被墙了,这个类可以从我刚上传的资源下载http://download.csdn.net/detail/yuwang_00/9480250


回到主题,DiskLruCache和LruCache不一样,它并不能通过构造方法来创建,它提供了open方法用于创建自身,有点类似于数据库。

当我们使用时,我们需要这样写

		File diskCacheDir = getDiskCacheDir(mContext, "bitmap");
		if (!diskCacheDir.exists()) {
			diskCacheDir.mkdirs();
		}
		if (getUsableSpace(diskCacheDir) > DISK_CACHE_SIZE) {
			try {
				mDiskLruCache = DiskLruCache.open(diskCacheDir, 1, 1,
						DISK_CACHE_SIZE);
				mIsDiskLruCacheCreated = true;
			} catch (IOException e) {
				// TODO: handle exception
				e.printStackTrace();
			}

		}
拿到外部储存的路径File对象(一般是sd卡下的路径),如果外部储存不存在则调用手机的缓存空间,跟LruCache一样
	private File getDiskCacheDir(Context context, String uniqueName) {
		boolean exter = Environment.getExternalStorageState().equals(
				Environment.MEDIA_MOUNTED);
		final String cachePath;
		if (exter) {
			cachePath = context.getExternalCacheDir().getPath();
		} else {
			cachePath = context.getCacheDir().getPath();
		}
		return new File(cachePath + File.separator + uniqueName);
	}
判断SD的可用空间还能不能够我所需的缓存空间,这里返回的单位是MB,所以我们定义DISK_CACHE_SIZE的时候要1024*1024*想要的大小

	private long getUsableSpace(File path) {
		if(Build.VERSION.SDK_INT>=VERSION_CODES.GINGERBREAD){
			return path.getUsableSpace();
		}
		final StatFs stats=new StatFs(path.getPath());
		return (long)stats.getBlockSize()*(long)stats.getAvailableBlocks();
	}
前期工作做完了,我们来看看open方法的四个参数,第一个参数我觉得猜都猜到了,是缓存的存储目录,如果像上面这么写的话,缓存会存储在/sdcard/Android/data/你的包名/data,当然你也可以自己指定一个目录,看你选择了。如果是前者,那么app卸载的时候缓存就会随app一起删除,如果自己指定的话,就不会删除(流氓软件就是这样来的,都卸载了还留一大堆缓存)

第二个参数是appversion,一般设置为1,如果版本号变了的话会清空之前的缓存文件,不过很多时候我们升级版本的话还是要保留之前版本缓存的数据的,所以一般不要改这个参数

第三个参数是valueCount,表示单个节点所对应的数据的个数,一般设为1

第四个参数是缓存的大小,注意单位应该是MB,LruCache则是KB

最后我们来看一下open方法的具体实现

    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize)
            throws IOException {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        if (valueCount <= 0) {
            throw new IllegalArgumentException("valueCount <= 0");
        }

        // prefer to pick up where we left off
        DiskLruCache cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        if (cache.journalFile.exists()) {
            try {
                cache.readJournal();
                cache.processJournal();
                cache.journalWriter = new BufferedWriter(new FileWriter(cache.journalFile, true),
                        IO_BUFFER_SIZE);
                return cache;
            } catch (IOException journalIsCorrupt) {
//                System.logW("DiskLruCache " + directory + " is corrupt: "
//                        + journalIsCorrupt.getMessage() + ", removing");
                cache.delete();
            }
        }

        // create a new empty cache
        directory.mkdirs();
        cache = new DiskLruCache(directory, appVersion, valueCount, maxSize);
        cache.rebuildJournal();
        return cache;
    }
先看构造方法

    private DiskLruCache(File directory, int appVersion, int valueCount, long maxSize) {
        this.directory = directory;
        this.appVersion = appVersion;
        this.journalFile = new File(directory, JOURNAL_FILE);
        this.journalFileTmp = new File(directory, JOURNAL_FILE_TMP);
        this.valueCount = valueCount;
        this.maxSize = maxSize;
    }
方法里真没啥可讲的,传值,创建日志文件,创建Tmp文件(

让我们回到open方法的逻辑,检查参数合不合法,然后检查cache的journalFile是否存在(以防万一),如果有了,就把它改成我们的。如果没有,就新建一个。rebuildJournal方法初始化了journalwriter,有了缓存文件writer对象,我们就能轻松的进行写入操作。


下面就到了DiskLruCache的写入操作,跟LruCache的LinkedHashmap不一样,DiskLruCache只是通过LinkedHashmap进行中转的,真正存储在journalFileTmp中。估计是大神为了安全性?稳定性?考虑,下面是写入操作

		DiskLruCache.Editor editor = mDiskLruCache.edit(key);
		if (editor != null) {
			OutputStream outputStream = editor
					.newOutputStream(DISK_CACHE_INDEX);
			if (downloadUrlToStream(url, outputStream)) {
				editor.commit();
			} else {
				editor.abort();
			}
			mDiskLruCache.flush();
		}
可以看出,DiskLruCache是采用Editor的方式读写数据,其中editor源码如下

    public Editor edit(String key) throws IOException {
        return edit(key, ANY_SEQUENCE_NUMBER);
    }

    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        checkNotClosed();
        validateKey(key);
        Entry entry = lruEntries.get(key);
        if (expectedSequenceNumber != ANY_SEQUENCE_NUMBER
                && (entry == null || entry.sequenceNumber != expectedSequenceNumber)) {
            return null; // snapshot is stale
        }
        if (entry == null) {
            entry = new Entry(key);
            lruEntries.put(key, entry);
        } else if (entry.currentEditor != null) {
            return null; // another edit is in progress
        }

        Editor editor = new Editor(entry);
        entry.currentEditor = editor;

        // flush the journal before creating files to prevent file leaks
        journalWriter.write(DIRTY + ' ' + key + '\n');
        journalWriter.flush();
        return editor;
    }
我们来解析一下写入流程
1.checkNotClosed();检查我们的journalWrite是否关闭,如果关闭了就报错,当我们关闭journalWrite的时候会把它置null

    public boolean isClosed() {
        return journalWriter == null;
    }

    private void checkNotClosed() {
        if (journalWriter == null) {
            throw new IllegalStateException("cache is closed");
        }
    }
2.检查key是否合法,主要是看有没有特殊字符或者长度太长等

    private void validateKey(String key) {
        if (key.contains(" ") || key.contains("\n") || key.contains("\r")) {
            throw new IllegalArgumentException(
                    "keys must not contain spaces or newlines: \"" + key + "\"");
        }
    }
3.然后从一个LinkedHashmap对象中取出Entry对象,Entry entry = lruEntries.get(key);

4.第一个if是判断tmp文件是否正在被读取,毕竟文件不能被同时读取,同时读取会出错的

5.要是在LinkedHashmap对象中没找到Entry则新建一个然后加入,LinkedHashmap实际上起到一个目录的作用,这样做得好处显而易见,重复输入的时候就不必打开文件去一个一个比对数据,毕竟打开输入流对内存的占用还是挺高的,一不留神就容易OOM。

6.第三个else if判断tmp文件是不是正在被其他程序写入,并发会出问题

7.创建一个新的Editor,通过刚才的Entry对象

7.声明当前正在编辑文件entry.currentEditor = editor;

8.返回这个Editor


好了,现在我们拿到了Editor,我们接着往下看newOutputStream方法

        public OutputStream newOutputStream(int index) throws IOException {
            synchronized (DiskLruCache.this) {
                if (entry.currentEditor != this) {
                    throw new IllegalStateException();
                }
                return new FaultHidingOutputStream(new FileOutputStream(entry.getDirtyFile(index)));
            }
        }
而Entry类里的getDirtyFile

        public File getDirtyFile(int i) {
            return new File(directory, key + "." + i + ".tmp");
        }
一切都清晰了,这步就是完成dirty缓存文件.tmp的创建工作,并且拿到它的FilterOutputStream对象,FaultHidingOutputStream是继承于FilterOutputStream,构造方法并没有重写。
至于downloadUrlToStream方法是自己定义的一个方法,代码在下一篇博客实现三级缓存的时候再贴,它的作用是收到传入的FilterOutputStream后,将通过url下载好的图片buffer流写入.tmp文件

最后一步,editor.commit();提交

        public void commit() throws IOException {
            if (hasErrors) {
                completeEdit(this, false);
                remove(entry.key); // the previous entry is stale
            } else {
                completeEdit(this, true);
            }
        }
要是有错的话就会回滚,没错的话继续

   private synchronized void completeEdit(Editor editor, boolean success) throws IOException {
        Entry entry = editor.entry;
        if (entry.currentEditor != editor) {
            throw new IllegalStateException();
        }

        // if this edit is creating the entry for the first time, every index must have a value
        if (success && !entry.readable) {
            for (int i = 0; i < valueCount; i++) {
                if (!entry.getDirtyFile(i).exists()) {
                    editor.abort();
                    throw new IllegalStateException("edit didn't create file " + i);
                }
            }
        }

        for (int i = 0; i < valueCount; i++) {
            File dirty = entry.getDirtyFile(i);
            if (success) {
                if (dirty.exists()) {
                    File clean = entry.getCleanFile(i);
                    dirty.renameTo(clean);
                    long oldLength = entry.lengths[i];
                    long newLength = clean.length();
                    entry.lengths[i] = newLength;
                    size = size - oldLength + newLength;
                }
            } else {
                deleteIfExists(dirty);
            }
        }

        redundantOpCount++;
        entry.currentEditor = null;
        if (entry.readable | success) {
            entry.readable = true;
            journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');
            if (success) {
                entry.sequenceNumber = nextSequenceNumber++;
            }
        } else {
            lruEntries.remove(entry.key);
            journalWriter.write(REMOVE + ' ' + entry.key + '\n');
        }

        if (size > maxSize || journalRebuildRequired()) {
            executorService.submit(cleanupCallable);
        }
    }
这里主要是把一些变量置null,比如entry.currentEditor,然后把Dirty缓存文件.tmp改名为clean缓存文件,另外无论是dirty文件或者clean文件在journal中有记录,格式为

journalWriter.write(CLEAN + ' ' + entry.key + entry.getLengths() + '\n');



写入说完了,下面就是读取

		DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
		if (snapshot != null) {
			FileInputStream fileInputStream = (FileInputStream) snapshot
					.getInputStream(DISK_CACHE_INDEX);
			FileDescriptor fileDescriptor = fileInputStream.getFD();
			bitmap = mImageResizer.decodeSampledBitmapFromFileDescriptor(
					fileDescriptor, reqWidth, reqHeight);
get方法内部逻辑是从上文我们提到的LinkedHashmap取出Entry,拥有了这个Entry我们就相当于拥有了缓存文件,因为Entry中的getCleanFile方法可以拿到Clean文件

对象,有了Clean文件对象,我们就可以创建FileInputStream,有了FileInputStream就可以把图片显示出来,这里我是拿到了FileDescriptor,通过FileDescriptor实现图片压缩功能,这是上一篇博客的事情了。

到这里缓存分析就全部结束了,下面就是源码地址http://download.csdn.net/detail/yuwang_00/9480256









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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值