Universal-Image-Loader(2)

10.MemoryCache

MemoryCache是实现内存缓存的类,不管是内存缓存还是磁盘缓存,对于ImageLoader来说都是核心功能,因为关系着图片的加载速度,因此要深入了解UIL中缓存的工作原理。

回忆一下,之前学过的ImageLoader的缓存实现,在之前的实现当中,利用的是LruCache来实现的,而LruCache又是通过accessOrder为true的LinkedHashMap来实现LRU算法的。

在UIL中,不光光有基于LRU算法的LRUMemoryCache,还有FIFOLimitedMemoryCache、LargestLimitedMemoryCache、LimitedAgeMemoryCache、LRULimitedMemoryCache、UsingFreqLimitedMemoryCache和WeakMemoryCache基于各种缓存规则的MemoryCache,它们实现的核心就是Map,一般LRU算法的都是基于accessOrder为true的LinkedHashMap,其他用的是HashMap,其中WeakMemoryCache的值用的是WeakReference。它们的保证同步方法是通过Collections.synchronizedList(Map...)获取到同步队列。UIL中默认配置的是LruMemoryCache.

MemoryCache还有两个基础抽象实现类BaseMemoryCache和LimitedMemoryCache,而LimitedMemoryCache又是继承于BaseMemoryCache,所有类型的MemoryCache类都是实现MemoryCache接口或者继承于LimitedMemoryCache和BaseMrmoryCache这三者的,大致可以分为是没有缓存大小限制和有缓存大小限制的,两者之间的区别就是,在添加新数据时如果缓存的大小超过大小限制阈值时是否删除Map中的数据;而如何删除数据的规则又将有缓存大小限制的MemoryCache分为几个类。下面是所有类型MemoryCache的分类表格。

MemoryCache子类 实现接口 Or 父类 有无大小限制 删除规则
LruMemoryCache MemoryCache LRU最近最少使用
LimitedAgeMemoryCache MemoryCache 存在超过限制时间的
FuzzyKeyMemoryCache MemoryCache put的时候有等价key的
LRULimitedMemoryCache LimitedMemoryCache LRU最近最少使用
FIFOLimitedMemoryCache LlimitedMemoryCache FIFO先入先出
LargestLimitedMemoryCache LimitedMemoryCache Largest最大的
UsingFreqLimitedMemoryCache LimitedMemoryCache 使用次数最少的
WeakMemoryCache BaseMemoryCache

下面是MemoryCache继承结构图,可以帮助我们理解整个MemoryCache的框架

MemoryCache继承结构图.PNG

下面就来详细介绍几个常用的MemoryCache以及它们的工作流程。

LruMemoryCache

首先是所有类型MemoryCache的接口MemoryCache.java。
它的作用主要是向外提供接口,外界主要通过该接口添加、获取数据,不关心内部的具体实现。
接口很简单,就几个基本的增删查方法。

public interface MemoryCache {

    boolean put(String key, Bitmap value);

    Bitmap get(String key);

    Bitmap remove(String key);

    Collection<String> keys();

    void clear();
}

然后介绍主要实现MemoryCache接口的无限制的MemoryCache类。

首先是UIL默认使用的LruMemoryCache。
可以看出来,实现的原理跟LruCache是十分相似的。都是利用了accessOrder为true的LinkedHashMap来实现LRU算法,在超过容量之后将删除Map中最近最少使用的数据。其他的操作大部分都是通过LinkedHashMap的同名操作实现的。

这里提前分析一下,既然都有容量限制,都是LRU算法,那么LruMemoryCache和LRULimitedMemoryCache有什么区别?
答:原理上是一样的,只不过删除的顺序不一样:LruMemoryCache在每次的put之后才调用了trimToSize()保证数据不超过限制大小;LRULimitedMemoryCache是确保数据不超过限制大小之后才添加进LinkedHashMap当中。
后面再详细分析LRULimitedMemoryCache的具体实现原理,来看看实现和LruMemoryCache是有什么区别。

public class LruMemoryCache implements MemoryCache {

    private final LinkedHashMap<String, Bitmap> map;

    private final int maxSize;
    /** Size of this cache in bytes */
    private int size;

    /** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */
    public LruMemoryCache(int maxSize) {
        if (maxSize <= 0) {
            throw new IllegalArgumentException("maxSize <= 0");
        }
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
    }

    /**
     * Returns the Bitmap for key if it exists in the cache. If a Bitmap was returned, it is moved to the head
     * of the queue. This returns null if a Bitmap is not cached.
     */
    @Override
    public final Bitmap get(String key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        synchronized (this) {
            return map.get(key);
        }
    }

    /** Caches Bitmap for key. The Bitmap is moved to the head of the queue. */
    @Override
    public final boolean put(String key, Bitmap value) {
        if (key == null || value == null) {
            throw new NullPointerException("key == null || value == null");
        }

        synchronized (this) {
            size += sizeOf(key, value);
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

    /**
     * Remove the eldest entries until the total of remaining entries is at or below the requested size.
     *
     * @param maxSize the maximum size of the cache before returning. May be -1 to evict even 0-sized elements.
     */
    private void trimToSize(int maxSize) {
        while (true) {
            String key;
            Bitmap value;
            synchronized (this) {
                if (size < 0 || (map.isEmpty() && size != 0)) {
                    throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
                }

                if (size <= maxSize || map.isEmpty()) {
                    break;
                }

                Map.Entry<String, Bitmap> toEvict = map.entrySet().iterator().next();
                if (toEvict == null) {
                    break;
                }
                key = toEvict.getKey();
                value = toEvict.getValue();
                map.remove(key);
                size -= sizeOf(key, value);
            }
        }
    }

    /** Removes the entry for key if it exists. */
    @Override
    public final Bitmap remove(String key) {
        if (key == null) {
            throw new NullPointerException("key == null");
        }

        synchronized (this) {
            Bitmap previous = map.remove(key);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
            return previous;
        }
    }

    @Override
    public Collection<String> keys() {
        synchronized (this) {
            return new HashSet<String>(map.keySet());
        }
    }

    @Override
    public void clear() {
        trimToSize(-1); // -1 will evict 0-sized elements
    }

    /**
     * Returns the size Bitmap in bytes.
     * 
     * An entry's size must not change while it is in the cache.
     */
    private int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override
    public synchronized final String toString() {
        return String.format("LruCache[maxSize=%d]", maxSize);
    }
}

LRULimitedMemoryCache

直接实现MemoryCache接口的只需要了解LruMemoryCache就足够了,本节主要介绍继承与LimitedMemoryCache的LRULimitedMemoryCache和FIFOLimitedMemoryCache。

这里LRULimitedMemoryCache的继承结构有3层(除去MemoryCache接口),每层结构都有自己的作用,因此没有清楚了解继承结构的话会导致思维混乱,无法理解LRULimitedMemoryCache的工作原理,所以先来看一下继承结构。

LimitedMemoryCache继承结构图.PNG

从图中可以看出,从BaseMemoryCache到LRULimitedMemoryCache一共有3层,所以问题就出来了,为什么要这么多层,像LruMemoryCache那样直接实现MemoryCache不可以吗?
当然可以,但是这样写就不符合类的单一职责要求,而且LRULimitedMemoryCache和FIFOLimitedMemoryCache这两个类只是单单删除规则不一样,如果LRULimitedMemoryCache直接实现MemoryCache的话,那么FIFOLimitedMemoryCache也要实现该类,而且会有大部分的代码和LRU是相同的,因此将共同的部分抽象出来,使得每个类的职责单一,降低耦合。

在这3层继承结构中,每一层的工作是:
BaseMemoryCache:有一个Map的内部类,该Map的作用是提供最底层的缓存功能,最终存储数据和获取数据实际都是BaseMemoryCache实现的,注意该Map的value并不是一个Bitmap类,而是一个Bitmap的Reference类,通常会传入Bitmap的WeakReference,这样的效果是Map中的value随时都能够GC回收;
LimitedMemoryCache:该类的作用是保证缓存大小不超过阈值。类内部有一个List容器类用于强引用存储Bitmap,通过该List类来保证正确提供容量限制的功能,即首先在put方法中判断当前存储的数据是否超过阈值,如果是则调用抽象方法removeNext()按照一定规则删除,再利用List来删除的Bitmap,如果List删除成功则说明缓存过该Bitmap,然后再改变缓存大小;
LRULimitedMemoryCache和FIFOLimitedMemoryCache等其他类的LimitedMemoryCache:用于提供删除规则即实现LimitedMemoryCache的removeNext(),内部有用于实现各个规则的数据结构,比如LRU利用accessOrder为true的LinkedHashMap,FIFO利用的是一个LinkedList。

下面分别介绍这三层结构。
1.BaseMemoryCache
首先先介绍BaseMemoryCache,因为LimitedMemoryCache继承于它,下面是它的实现源码。

关注的重点有:

  • BaseMemoryCache最主要的就是靠Map成员Map<String, Reference<Bitmap>> softMap实现的,如数据的缓存和获取就是通过该softMap的put和get实现的
  • Map中的value是Bitmap的Reference对象而不是Bitmap,而该Reference通常被传入WeakReference,因此造成的结果是,softMap中的value会随时被GC回收
  • put方法中利用抽象方法createReference()来创建Bitmap的引用对象
public abstract class BaseMemoryCache implements MemoryCache {

    /** Stores not strong references to objects */
    private final Map<String, Reference<Bitmap>> softMap = Collections.synchronizedMap(new HashMap<String, Reference<Bitmap>>());

    @Override
    public Bitmap get(String key) {
        Bitmap result = null;
        Reference<Bitmap> reference = softMap.get(key);
        if (reference != null) {
            result = reference.get();
        }
        return result;
    }

    @Override
    public boolean put(String key, Bitmap value) {
        softMap.put(key, createReference(value));
        return true;
    }

    @Override
    public Bitmap remove(String key) {
        Reference<Bitmap> bmpRef = softMap.remove(key);
        return bmpRef == null ? null : bmpRef.get();
    }

    @Override
    public Collection<String> keys() {
        synchronized (softMap) {
            return new HashSet<String>(softMap.keySet());
        }
    }

    @Override
    public void clear() {
        softMap.clear();
    }

    /** Creates {@linkplain Reference not strong} reference of value */
    protected abstract Reference<Bitmap> createReference(Bitmap value);
}

2.LimitedMemoryCache

之前已经说过了LimitedMemoryCache的作用就是实现防止缓存超过阈值的大小,所以关注该类的关注点在如何实现限制缓存大小。

由源码可以看出:

  • 允许最大缓存为16MB
  • 内部有一个List用于以强引用的方式存储Bitmap
  • 没有get方法,即get方法是直接调用的是BaseMemoryCache的get方法
  • 最主要的是put方法,这是实现限制缓存的关键,在put方法中,判断添加后的缓存大小是否超过阈值,如果超过则调用抽象方法removeNext()获取缓存中应该被删除的数据,比如LRU的removeNext()返回的是最近最少被使用的数据,FIFO的返回的是最早插入的数据。最后还要调用super.put(key, value)来让BaseMemoryCache中的softMap存储数据,因为前面说过数据的存储和获取是由BaseMemoryCache提供的
  • 内部类List的作用是确保缓存大小的正确变化。因为要确保removeNext()删除的数据是之前缓存的数据,如果List删除removeNext()返回的数据成功了证明缓存被删除了,此时缓存大小才会变化,才能使缓存反应真实的变化
  • 在put方法中缓存数据超过阈值时,removeNext()会删除子类比如LRU、FIFO中的缓存数据,List也会删除数据,但注意BaseMemoryCache中的softMap并不会删除数据,不必担心softMap中的缓存量过大,因为WeakReference的对象会随时被GC回收
public abstract class LimitedMemoryCache extends BaseMemoryCache {

    private static final int MAX_NORMAL_CACHE_SIZE_IN_MB = 16;
    private static final int MAX_NORMAL_CACHE_SIZE = MAX_NORMAL_CACHE_SIZE_IN_MB * 1024 * 1024;

    private final int sizeLimit;

    private final AtomicInteger cacheSize;

    /**
     * Contains strong references to stored objects. Each next object is added last. If hard cache size will exceed
     * limit then first object is deleted (but it continue exist at {@link #softMap} and can be collected by GC at any
     * time)
     */
    private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());

    /** @param sizeLimit Maximum size for cache (in bytes) */
    public LimitedMemoryCache(int sizeLimit) {
        this.sizeLimit = sizeLimit;
        cacheSize = new AtomicInteger();
        if (sizeLimit > MAX_NORMAL_CACHE_SIZE) {
            L.w("You set too large memory cache size (more than %1$d Mb)", MAX_NORMAL_CACHE_SIZE_IN_MB);
        }
    }

    /**
     * 实现缓存大小限制的关键
     */
    @Override
    public boolean put(String key, Bitmap value) {
        boolean putSuccessfully = false;
        // Try to add value to hard cache
        int valueSize = getSize(value);
        int sizeLimit = getSizeLimit();
        int curCacheSize = cacheSize.get();
        if (valueSize < sizeLimit) {
            // 在添加数据后大于限制大小时则删除removeNext()返回的Bitmap
            while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }
            hardCache.add(value);
            cacheSize.addAndGet(valueSize);

            putSuccessfully = true;
        }
        // 最后一定要将数据存到sofeMap中
        super.put(key, value);
        return putSuccessfully;
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            if (hardCache.remove(value)) {
                cacheSize.addAndGet(-getSize(value));
            }
        }
        return super.remove(key);
    }

    @Override
    public void clear() {
        hardCache.clear();
        cacheSize.set(0);
        super.clear();
    }

    protected int getSizeLimit() {
        return sizeLimit;
    }

    protected abstract int getSize(Bitmap value);

    protected abstract Bitmap removeNext();
}

3.LRULimitedMemoryCache

由上面可知,数据的存取获取以及缓存大小的限制在前面两层结构已经实现了,此时LRULimitedMemoryCache以及FIFOLimitedMemoryCache等类的工作就简单很多了,只需要制定相应的删除规则removeNext()就行了,而删除指定的数据使得类本身也需要用一个数据结构存储数据,毕竟你没有数据怎么确定要删除的数据是哪个啊是吧,下面是源码。

关注的重点:

  • 用一个accessOrder为true的LinkedHashMap作为存储数据的数据结构
  • put方法是先调用super.put(key, value)再储存数据到本身。这很好理解嘛,前面说过的,数据的存储和获取是通过BaseMemoryCache实现的,因此只有高层成功存储了数据自身才存储数据
  • get方法与put方法相反,先是调用本身的get然后再返回父类的get,这里返回的是父类的get为什么还要多此一举调用本身的get干什么?这里调用get主要是为了触发accessOrder为true的LinkedHashMap的LRU算法,即把常用的数据移到队列的尾部,然后队头剩下的就是不常用的
  • removeNext()直接删除了LinkedHashMap队列头部的数据,这些数据是最近最少使用的数据,跟accessOrder为true的LinkedHashMap特性有关
  • 抽象方法getSize()用于计算传入的Bitmap的大小,这跟缓存的存储和删除改变的缓存大小密切相关,该方法由子类实现
public class LRULimitedMemoryCache extends LimitedMemoryCache {

    private static final int INITIAL_CAPACITY = 10;
    private static final float LOAD_FACTOR = 1.1f;

    /** Cache providing Least-Recently-Used logic */
    private final Map<String, Bitmap> lruCache = Collections.synchronizedMap(new LinkedHashMap<String, Bitmap>(INITIAL_CAPACITY, LOAD_FACTOR, true));

    /** @param maxSize Maximum sum of the sizes of the Bitmaps in this cache */
    public LRULimitedMemoryCache(int maxSize) {
        super(maxSize);
    }

    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            lruCache.put(key, value);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap get(String key) {
        lruCache.get(key); // call "get" for LRU logic
        return super.get(key);
    }

    @Override
    public Bitmap remove(String key) {
        lruCache.remove(key);
        return super.remove(key);
    }

    @Override
    public void clear() {
        lruCache.clear();
        super.clear();
    }

    @Override
    protected int getSize(Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override
    protected Bitmap removeNext() {
        Bitmap mostLongUsedValue = null;
        synchronized (lruCache) {
            Iterator<Entry<String, Bitmap>> it = lruCache.entrySet().iterator();
            if (it.hasNext()) {
                Entry<String, Bitmap> entry = it.next();
                mostLongUsedValue = entry.getValue();
                it.remove();
            }
        }
        return mostLongUsedValue;
    }

    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

4.FIFOLimitedMemoryCache

看完上面之后,FIFOLimiedMemoryCache更容易理解了,只是removeNext()返回的数据跟LRU不一样而已,具体看下面的源码

  • 内部使用的是LinkedList作为存储数据的数据结构
  • put,get等方法除了调用super的put和get方法,本身直接调用LinkedList的put,get方法用于添加和获取数据
  • removeNext()直接就是删除队列的头部数据,也就是FIFO原则
public class FIFOLimitedMemoryCache extends LimitedMemoryCache {

    private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());

    public FIFOLimitedMemoryCache(int sizeLimit) {
        super(sizeLimit);
    }

    @Override
    public boolean put(String key, Bitmap value) {
        if (super.put(key, value)) {
            queue.add(value);
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Bitmap remove(String key) {
        Bitmap value = super.get(key);
        if (value != null) {
            queue.remove(value);
        }
        return super.remove(key);
    }

    @Override
    public void clear() {
        queue.clear();
        super.clear();
    }

    @Override
    protected int getSize(Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }

    @Override
    protected Bitmap removeNext() {
        return queue.remove(0);
    }

    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

内存缓存小结

缓存是ImageLoader中的一个核心功能,因此需要深刻的理解,重新看一下MemoryCache的框架图,此时便会有了更深的体会。

MemoryCache继承机构图.PNG
  • MemoryCache:整个缓存内存框架的最底层的接口,它是外界主要使用的接口
  • LruMemoryCache:直接实现了MemoryCache接口,里面用了accessOrder为true的LinkedHashMap进行数据的存取的容器,在每次put方法时会检查缓存大小是否超出阈值,是的话则根据LRU算法删除数据
  • BaseMemoryCache:实现了MemoryCache接口,内部使用了HashMap<String, Reference<Bitmap>>作为数据存储读取的容器。需要注意的点是,传入Map的Reference一般都是WeakReference,即该Bitmap可能随时被GC回收
  • LimitedMemoryCache:继承了BaseMemoryCache,该类主要是记录已经缓存的大小cacheSize,还利用List缓存添加过的Bitmap对象,实现了限制缓存大小的功能,即在put方法中发现添加的数据总数超过阈值16MB时,会调用抽象方法removeNext()取出要删除的对象,然后利用内部类List检测该对象是否被缓存过,是则删除List中的数据并改变cacheSize的大小
  • LRULimitedMemoryCache:继承了LimitedMemoryCache,有前面可知,数据的存取以及容量的限制父类已经实现,该类只是为了提供在缓存超过缓存大小时应该删除哪个数据的规则即removeNext()方法。内部使用了accessOrder为true的LinkedHashMap作为数据的存储(注意这些存储的数据并不是拿来供外界使用的,而是为了确定下一个被删除的数据),然后利用LinkedHashMap的LRU特性在removeNext中返回最近最少使用的数据
  • FIFOLimitedMemoryCache:跟LRU一样,只是内部用的是LinkedList实现数据的存储,当然这些数据不是供外界使用的,然后再removeNext中返回LinkedList队头的数据,也就是最早插入的数据,这就是FIFO算法
  • 剩下的MemoryCache跟FIFO,LRU一样,只是在removeNext中根据不同的规则提供了不一样的被删除的数据

11.DiskCache

ImageLoader中另一个缓存是磁盘缓存,首先还是先回忆一下之前学过的ImageLoader的DiskLruCache的工作原理是什么。

DiskLruCache工作原理:内部使用了accessOrder为true的LinkedHashMap作为数据的索引,因为DiskLruCache是以文件的形式存储数据的,因此LinkedHaspMap里面并不持有Bitmap对象,实际上持有的是一个Entry内部类对象,该对象指向的是一个缓存文件,DiskLruCache对缓存数据的添加和获取其实就是对该缓存文件的写入和读取。DiskLruCache对缓存文件的写入和读取分别是通过内部类对象Editor和Snotshot的OutputStream和InputStream来实现数据的写入和读取。

接下来我们从源码中了解DiskCache整个的框架,跟MemoryCache一样,先从框架图入手掌握整个继承结构。
注意图中还有一个DiskLruCache不属于继承结构,其实DiskLruCache就是之前学过的ImageLoader里面的DiskLruCache,但是UIL中也添加了该类,原因是LruDiskCache在内部使用了DiskLruCache,简单来说就是在DiskLruCache外封装了一层。在ImageLoaderConfiguration中默认使用的LruDiskCache

DiskCache继承结构图.PNG

LruDiskCache

LruDiskCache是直接实现DiskCache的类,首先来看一下DiskCache接口的方法。
可以看到需要实现的方法并不多,主要关注get和save方法。get方法返回的是文件类,即缓存文件,要清楚地意识到凡是磁盘缓存都是用文件来缓存数据的;save方法用于将数据存储进与imageUri相对应的文件,其中参数有一个InputStream,该InputStream是图片的输入流,通过该输入流将数据写入文件当中。

public interface DiskCache {
    /**
     * Returns root directory of disk cache
     */
    File getDirectory();

    /**
     * Returns file of cached image
     */
    File get(String imageUri);

    /**
     * Saves image stream in disk cache.
     * Incoming image stream shouldn't be closed in this method.
     *
     */
    boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

    boolean save(String imageUri, Bitmap bitmap) throws IOException;

    boolean remove(String imageUri);

    /** Closes disk cache, releases resources. */
    void close();

    void clear();
}


下面介绍LruDiskCache,相对于DiskLruCache简单很多,原因是内部使用DiskLruCache类作为成员,文件数据的写入和读取其实都是通过DiskLruCache实现的。下面是使用LruDiskCache需要关注的地方。

  • 初始化:直接通过构造方法便可创建,无需DiskLruCache那样需要通过open,因为在构造方法内部调用了open方法
  • get:因为使用DiskLruCache读取数据是先获取Snapshot然后,通过Snapshot的InputStream读取文件数据的,在LruDiskCache中封装了整个过程,直接在save方法内部获取Snapshot并返回文件
  • save:同get方法一样,LurDiskCache封装了通过DiskLruCache的Editor的OutputStream写入数据的过程,直接在参数里面传入文件的输入流便可实现写入数据的功能,并且在commit方法当中可以确保缓存大小不超过阈值
  • remove:通过DiskLruCache的remove即可

总的来说,其实LruDiskCache就是对DiskLruCache做了一层封装,实际的数据的操作方法还是通过DiskLruCache来实现的。

public class LruDiskCache implements DiskCache {
    /** {@value */
    public static final int DEFAULT_BUFFER_SIZE = 32 * 1024; // 32 Kb
    /** {@value */
    public static final Bitmap.CompressFormat DEFAULT_COMPRESS_FORMAT = Bitmap.CompressFormat.PNG;
    /** {@value */
    public static final int DEFAULT_COMPRESS_QUALITY = 100;

    private static final String ERROR_ARG_NULL = " argument must be not null";
    private static final String ERROR_ARG_NEGATIVE = " argument must be positive number";

    protected DiskLruCache cache;
    private File reserveCacheDir;

    //用于文件的命名,有Hash和MD5两种文件名
    protected final FileNameGenerator fileNameGenerator;

    protected int bufferSize = DEFAULT_BUFFER_SIZE;

    protected Bitmap.CompressFormat compressFormat = DEFAULT_COMPRESS_FORMAT;
    protected int compressQuality = DEFAULT_COMPRESS_QUALITY;

    public LruDiskCache(File cacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize) throws IOException {
        this(cacheDir, null, fileNameGenerator, cacheMaxSize, 0);
    }

    /**
     * @param cacheDir          Directory for file caching
     * @param reserveCacheDir   null-ok; Reserve directory for file caching. It's used when the primary directory isn't available.
     * @param fileNameGenerator {@linkplain com.nostra13.universalimageloader.cache.disc.naming.FileNameGenerator
     *                          Name generator} for cached files. Generated names must match the regex
     *                          <strong>[a-z0-9_-]{1,64}</strong>
     * @param cacheMaxSize      Max cache size in bytes. <b>0</b> means cache size is unlimited.
     * @param cacheMaxFileCount Max file count in cache. <b>0</b> means file count is unlimited.
     * @throws IOException if cache can't be initialized (e.g. "No space left on device")
     */
    public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
            int cacheMaxFileCount) throws IOException {
        if (cacheDir == null) {
            throw new IllegalArgumentException("cacheDir" + ERROR_ARG_NULL);
        }
        if (cacheMaxSize < 0) {
            throw new IllegalArgumentException("cacheMaxSize" + ERROR_ARG_NEGATIVE);
        }
        if (cacheMaxFileCount < 0) {
            throw new IllegalArgumentException("cacheMaxFileCount" + ERROR_ARG_NEGATIVE);
        }
        if (fileNameGenerator == null) {
            throw new IllegalArgumentException("fileNameGenerator" + ERROR_ARG_NULL);
        }

        if (cacheMaxSize == 0) {
            cacheMaxSize = Long.MAX_VALUE;
        }
        if (cacheMaxFileCount == 0) {
            cacheMaxFileCount = Integer.MAX_VALUE;
        }

        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
        initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
    }

    private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
            throws IOException {
        try {
            cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
        } catch (IOException e) {
            L.e(e);
            if (reserveCacheDir != null) {
                initCache(reserveCacheDir, null, cacheMaxSize, cacheMaxFileCount);
            }
            if (cache == null) {
                throw e; //new RuntimeException("Can't initialize disk cache", e);
            }
        }
    }

    @Override
    public File getDirectory() {
        return cache.getDirectory();
    }

    @Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } catch (IOException e) {
            L.e(e);
            return null;
        } finally {
            if (snapshot != null) {
                snapshot.close();
            }
        }
    }

    @Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        if (editor == null) {
            return false;
        }

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean copied = false;
        try {
            copied = IoUtils.copyStream(imageStream, os, listener, bufferSize);
        } finally {
            IoUtils.closeSilently(os);
            if (copied) {
                editor.commit();
            } else {
                editor.abort();
            }
        }
        return copied;
    }

    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        if (editor == null) {
            return false;
        }

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        } finally {
            IoUtils.closeSilently(os);
        }
        if (savedSuccessfully) {
            editor.commit();
        } else {
            editor.abort();
        }
        return savedSuccessfully;
    }

    @Override
    public boolean remove(String imageUri) {
        try {
            return cache.remove(getKey(imageUri));
        } catch (IOException e) {
            L.e(e);
            return false;
        }
    }

    @Override
    public void close() {
        try {
            cache.close();
        } catch (IOException e) {
            L.e(e);
        }
        cache = null;
    }

    @Override
    public void clear() {
        try {
            cache.delete();
        } catch (IOException e) {
            L.e(e);
        }
        try {
            initCache(cache.getDirectory(), reserveCacheDir, cache.getMaxSize(), cache.getMaxFileCount());
        } catch (IOException e) {
            L.e(e);
        }
    }

    private String getKey(String imageUri) {
        return fileNameGenerator.generate(imageUri);
    }

    public void setBufferSize(int bufferSize) {
        this.bufferSize = bufferSize;
    }

    public void setCompressFormat(Bitmap.CompressFormat compressFormat) {
        this.compressFormat = compressFormat;
    }

    public void setCompressQuality(int compressQuality) {
        this.compressQuality = compressQuality;
    }
}

BaseDiskCache

UIL中还有另外两个具体磁盘缓存类LimitedAgeDiskCache和UnlimitedDiskCache,它们的不同点只是一个还删除缓存文件的过时文件,一个不限制缓存大小。这里只介绍他们的共同父类:BaseDiskCache。

在BaseDiskCache中并没有使用特殊的数据结构来存储数据,直接就是通过对文件类的操作来达成使用文件缓存的目的。注意该类并没有限制缓存大小,下面就简单看一下BaseDiskCache的save和get方法。

public abstract class BaseDiskCache implements DiskCache {

    ...

        @Override
    public File get(String imageUri) {
        return getFile(imageUri);
    }

    protected File getFile(String imageUri) {
        String fileName = fileNameGenerator.generate(imageUri);
        File dir = cacheDir;
        if (!cacheDir.exists() && !cacheDir.mkdirs()) {
            if (reserveCacheDir != null && (reserveCacheDir.exists() || reserveCacheDir.mkdirs())) {
                dir = reserveCacheDir;
            }
        }
        return new File(dir, fileName);
    }

    @Override
    public boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException {
        File imageFile = getFile(imageUri);
        File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
        boolean loaded = false;
        try {
            OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
            try {
                loaded = IoUtils.copyStream(imageStream, os, listener, bufferSize);
            } finally {
                IoUtils.closeSilently(os);
            }
        } finally {
            if (loaded && !tmpFile.renameTo(imageFile)) {
                loaded = false;
            }
            if (!loaded) {
                tmpFile.delete();
            }
        }
        return loaded;
    }


}

磁盘缓存小结

相对于内存缓存,UIL中的磁盘缓存的框架简单的多,大致分为两个磁盘缓存策略,一个是使用DiskLruCache作为底层实现的LruDiskCache,另一个是直接对文件类File进行操作。

  • LruDiskCache:底层采用DiskLruCache实现,只不过是封装了DiskLruCache较为复杂的数据操作方法
  • BaseDiskCache:不采取任何数据,直接在实现方法里面使用方法类实现

12.ImageLoader

之前的章节主要介绍了ImageLoader在加载图片时主要用到的一些类,比如配置类ImageLoaderConfiguration、线程池管理者以及主要任务执行者ImageLoaderEngine、加载和显示图片任务LoadAndDisplayImageDisk、图片加载显示配置类DisplayImageOptions、图片下载器ImageDownloader、图片解码器ImageDocoder、缓存内存类MemoryCache和DiskCache等等。接下来分析ImageLoader的使用和具体工作流程,由于主要需要的类的分析过了,因此分析起来简单了许多。

ImageLoader使用

ImageLoader的很简单,主要分为3步

  1. 配置加载图片的参数类ImageLoaderConfiguration并创建Imageloader对象
  2. 配置显示图片用的参数类DisplayImageOptions
  3. 使用displayImage()显示图片

第一步,配置并创建ImageLoader对象
后面注释的是Configuration的可选项和一些默认项,可以看出一些加载图片的必要默认项已经可以让ImageLoader正常工作了,比如taskExecutor、diskCache、memoryCache、downloader、decoder和defaultDisplayImageOptions等等。

//Set the ImageLoaderConfigutation
ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(getApplicationContext())
        .threadPriority(Thread.NORM_PRIORITY - 2)
        .denyCacheImageMultipleSizesInMemory()
        .diskCacheFileNameGenerator(new Md5FileNameGenerator())
        .diskCacheSize(50 * 1024 * 1024)
        .tasksProcessingOrder(QueueProcessingType.LIFO)
        .writeDebugLogs()
        .build();

//Initial ImageLoader with ImageLoaderConfiguration
ImageLoader.getInstance().init(configuration);

/*
ImageLoaderConfiguration config = new ImageLoaderConfiguration.Builder(context)
    .memoryCacheExtraOptions(480, 800) // default = device screen dimensions
    .diskCacheExtraOptions(480, 800, null)
    .taskExecutor(...)// default
    .taskExecutorForCachedImages(...)
    .threadPoolSize(3) // default
    .threadPriority(Thread.NORM_PRIORITY - 2) // default
    .tasksProcessingOrder(QueueProcessingType.FIFO) // default
    .denyCacheImageMultipleSizesInMemory()
    .memoryCache(new LruMemoryCache(2 * 1024 * 1024))
    .memoryCacheSize(2 * 1024 * 1024)
    .memoryCacheSizePercentage(13) // default
    .diskCache(new UnlimitedDiskCache(cacheDir)) // default
    .diskCacheSize(50 * 1024 * 1024)
    .diskCacheFileCount(100)
    .diskCacheFileNameGenerator(new HashCodeFileNameGenerator()) // default
    .imageDownloader(new BaseImageDownloader(context)) // default
    .imageDecoder(new BaseImageDecoder()) // default
    .defaultDisplayImageOptions(DisplayImageOptions.createSimple()) // default
    .writeDebugLogs()
    .build();
/**/


第二步,配置显示图片用的参数类DislayImageOptions
从类名就能判断出该类的作用是让ImageLoader按需求显示图片,跟Configuration一样,Options也有很多可选的配置参数,并且一些显示图片的必要参数已经被初始化了,比如displayer用于控件显示图片和handler传回主线程操作。
但值得注意的是,在Options中,cacheInMemory和cacheOnDisk默认是false,因此很有必要在程序中手动将它们将设置成true,如下面代码所示。

DisplayImageOptions options = new DisplayImageOptions.Builder()
        .cacheInMemory(true)    //缓存的这两步很有必要
        .cacheOnDisk(true)
        .considerExifParams(true)
        .displayer(new CircleBitmapDisplayer(Color.WHITE, 5))
        .build();
/*
DisplayImageOptions options = new DisplayImageOptions.Builder()
    .showImageOnLoading(R.drawable.ic_stub) // resource or drawable
    .showImageForEmptyUri(R.drawable.ic_empty) // resource or drawable
    .showImageOnFail(R.drawable.ic_error) // resource or drawable
    .resetViewBeforeLoading(false)  // default
    .delayBeforeLoading(1000)
    .cacheInMemory(false) // default
    .cacheOnDisk(false) // default
    .preProcessor(...)
    .postProcessor(...)
    .extraForDownloader(...)
    .considerExifParams(false) // default
    .imageScaleType(ImageScaleType.IN_SAMPLE_POWER_OF_2) // default
    .bitmapConfig(Bitmap.Config.ARGB_8888) // default
    .decodingOptions(...)
    .displayer(new SimpleBitmapDisplayer()) // default
    .handler(new Handler()) // default
    .build();      
/**/    

第三步,使用displayImage()显示图片
调用displayImage()之前要获取到ImageLoader的实例,因为ImageLoader采用单例模式,因此ImageLoader的实例是通过IamgeLoader的静态方法getInstance()获取的。
displayImage()有很多种重载方法,这里只展示一个,后面的注释是所有displayImage()的版本。
由displayImage的参数中可以看出最主要的两个参数就是imageUri和imageView,也就是要显示的图片的uri地址和显示图片的控件,这里也体现出了ImageLoader的最本质的工作,那就是将图片从uri中加载到控件中。

注:不像前面Configuration和Optins配置一次就够了,displayImage()方法在每次加载显示图片时都应该调用一次,因此通常该方法使用在ListView的Adapter的getView当中,因为getView中可以获取到当前要显示图片的控件,并且列表滑动就会触发getView方法,因此只需要在getView中将对应的ImageView传送给displayImage就可以了


ImageLoader.getInstance().displayImage(imageUri, imageView, options, imageLoadingListener);

/*
//所有displayImage的方法,前面一部分是针对ImageAware的,后面一部分是针对ImageView的,也就是我们在开发中所使用到的,其实实现是利用了前面的方法

displayImage(String uri, ImageAware imageAware)
displayImage(String uri, ImageAware imageAware, ImageLoadingListener listener)
displayImage(String uri, ImageAware imageAware, DisplayImageOptions options)
displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
        ImageLoadingListener listener) 
displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
        ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
        ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)

displayImage(String uri, ImageView imageView)
displayImage(String uri, ImageView imageView, ImageSize targetImageSize)
displayImage(String uri, ImageView imageView, DisplayImageOptions options)
displayImage(String uri, ImageView imageView, ImageLoadingListener listener)
displayImage(String uri, ImageView imageView, DisplayImageOptions options,
        ImageLoadingListener listener)
displayImage(String uri, ImageView imageView, DisplayImageOptions options,
        ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
/**/

到此ImageLoader的使用方法就结束了,是不是很简单,只需要3步,甚至如果使用的都是默认的配置,那只需要初始化ImageLoader并调用displayImage就可以了。下面具体分析ImageLoader的工作流程。

ImageLoader工作流程

从上面可以看出外界使用ImageLoader只需要调用displayImage()就可以实现图片的加载和显示,所以研究ImageLoader的工作流程其实就是分析ImageLoader的displayImage方法。由于displayImage()中调用的方法会贯穿整个UIL包,加上前面仔细分析了主要类的工作原理,因此下面只需分析主要的部分,便于理解整个UIL的工作流程。

public class ImageLoader
{
    ...

    //这些是工作时所需要的类成员
    private ImageLoaderConfiguration configuration;
    private ImageLoaderEngine engine;

    private ImageLoadingListener defaultListener = new SimpleImageLoadingListener();

    private volatile static ImageLoader instance;

    /**
     *  uri:图片的uri
     *  imageAware:显示图片的控件
     *  options:显示图片的参数配置
     *  targetSize:要显示的图片的大小
     *  listener:用于加载图片中的回调
     *  progressListener:也是用于图片加载时的回调
     */
    public void displayImage(String uri, ImageAware imageAware, DisplayImageOptions options,
            ImageSize targetSize, ImageLoadingListener listener, ImageLoadingProgressListener progressListener)
            {
                ...
                //将uri转换成key
                String memoryCacheKey = MemoryCacheUtils.generateKey(uri, targetSize);
                //从内存中获取相应key的图片
                Bitmap bmp = configuration.memoryCache.get(memoryCacheKey);
                //如果不为空说明从内存中获取到了图片
                if (bmp != null && !bmp.isRecycled()) {
                    //判断是否需要加工处理,如果是则封装图片的信息和需求成Info类,然后通过一个任务类ProcessAndDisplayImageTask将图片按照需求加载到控件中
                    if (options.shouldPostProcess()) {
                        ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                                options, listener, progressListener, engine.getLockForUri(uri));
                        ProcessAndDisplayImageTask displayTask = new ProcessAndDisplayImageTask(engine, bmp, imageLoadingInfo,
                                defineHandler(options));

                        //如果此时运行在子线程中就直接运行,否则使用线程池执行
                        if (options.isSyncLoading()) {
                            displayTask.run();
                        } else {
                            engine.submit(displayTask);
                        }
                    } 
                    //如果不需要经过处理,则直接通过displayer的display将图片显示在控件中
                    else {
                        options.getDisplayer().display(bmp, imageAware, LoadedFrom.MEMORY_CACHE);
                        listener.onLoadingComplete(uri, imageAware.getWrappedView(), bmp);
                    }
                } 
                //为空说明需要内存缓存中不存在该图片,需要从磁盘和网络中加载该图片
                else {
                    //设置加载前的默认图片
                    if (options.shouldShowImageOnLoading()) {
                        imageAware.setImageDrawable(options.getImageOnLoading(configuration.resources));
                    } else if (options.isResetViewBeforeLoading()) {
                        imageAware.setImageDrawable(null);
                    }

                    //封装图片加载信息类,然后通过LoadAndDisplayImageTask执行加载显示图片任务
                    ImageLoadingInfo imageLoadingInfo = new ImageLoadingInfo(uri, imageAware, targetSize, memoryCacheKey,
                            options, listener, progressListener, engine.getLockForUri(uri));
                    LoadAndDisplayImageTask displayTask = new LoadAndDisplayImageTask(engine, imageLoadingInfo,
                            defineHandler(options));
                    if (options.isSyncLoading()) {
                        displayTask.run();
                    } else {
                        engine.submit(displayTask);
                    }
                }

            }

    ...
}

//同步加载图片并显示的任务
final class LoadAndDisplayImageTask implements Runnable, IoUtils.CopyListener
{
    ...

    @Override
    public void run()
    {
        ...
        //从内存中提取图片
        bmp = configuration.memoryCache.get(memoryCacheKey);
        if (bmp == null || bmp.isRecycled()) {
            //如果内存中为null则从磁盘网络中获取图片
            bmp = tryLoadBitmap();
            ...
            if (bmp != null && options.isCacheInMemory()) {
                //将图片存进内存缓存中
                configuration.memoryCache.put(memoryCacheKey, bmp);
            }
            ...
        }
        ...
        //前面加载完图片接着通过执行DisplayBitmapTask显示图片到控件中
        DisplayBitmapTask displayBitmapTask = new DisplayBitmapTask(bmp, imageLoadingInfo, engine, loadedFrom);
        runTask(displayBitmapTask, syncLoading, handler, engine);

    }

    //同步从磁盘、网络中加载图片
    private Bitmap tryLoadBitmap() throws TaskCancelledException
    {
        Bitmap bitmap = null;
        ...
        //从磁盘缓存中获取文件并解码成图片
        File imageFile = configuration.diskCache.get(uri);
        bitmap = decodeImage(Scheme.FILE.wrap(imageFile.getAbsolutePath()));
        
        if (bitmap == null || bitmap.getWidth() <= 0 || bitmap.getHeight() <= 0) 
        {
            //如果磁盘中获取不到图片则通过tryCacheImageOnDisk()从网络中获取并存至磁盘中
            if (options.isCacheOnDisk() && tryCacheImageOnDisk()) 
            {
                ...
                //再次从磁盘中获取文件
                imageFile = configuration.diskCache.get(uri);
                if (imageFile != null) {
                    imageUriForDecoding = Scheme.FILE.wrap(imageFile.getAbsolutePath());
                }
            }   
            //再次将文件解码成图片
            bitmap = decodeImage(imageUriForDecoding);
        }
        ...
        return bitmap;
    }

    //从网络中加载图片(通过ImageDownloader),并按照需求压缩(通过ImageDecoder),最后放入磁盘缓存中
    private boolean tryCacheImageOnDisk() throws TaskCancelledException 
    {
        boolean loaded;

        loaded = downloadImage();
        ...
        resizeAndSaveImage(width, height);
        ...

        return loaded;
    }

}

//异步显示图片的任务,里面主要是调用了图片在加载过程中的各种回调,最后通过displayer.display将图片显示到控件中,注意该任务要运行在主线程当中
final class DisplayBitmapTask implements Runnable {

    @Override
    public void run() {
        if (imageAware.isCollected()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_COLLECTED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else if (isViewWasReused()) {
            L.d(LOG_TASK_CANCELLED_IMAGEAWARE_REUSED, memoryCacheKey);
            listener.onLoadingCancelled(imageUri, imageAware.getWrappedView());
        } else {
            L.d(LOG_DISPLAY_IMAGE_IN_IMAGEAWARE, loadedFrom, memoryCacheKey);

            //最主要的是这步,显示图片
            displayer.display(bitmap, imageAware, loadedFrom);
            engine.cancelDisplayTaskFor(imageAware);
            listener.onLoadingComplete(imageUri, imageAware.getWrappedView(), bitmap);
        }
    }

}

上面就是整个UIL的工作流程,大致上可以分为两步,同步加载图片和异步显示图片,主要关注点在同步加载图片这个过程。

  • 同步加载图片:内存缓存中获取->磁盘缓存中获取->网络中获取->存进磁盘缓存->磁盘缓存中获取->存进内存缓存->返回图片
  • 异步显示图片:利用Handler将任务执行在主线程当中,通过displayer将图片显示在控件当中

以下是加载显示图片的流程,帮助理解和记忆。

UIL_Load&Display Task Flow.PNG

总结UIL的关注点

UIL主要关注以下几点:图片从网络加载的加载所使用的方式,内存缓存的方式,磁盘缓存的方式以及整个UIL的工作流程。

网络加载:采用HttpURLConnection进行网络连接来获取数据
内存缓存:UIL中内存缓存有好几种用于内存缓存的类,其中默认使用的是LruMemoryCache,它的实现原理跟Android自带的LruCache差不多,都是利用accessOrder为true的LinkedHashMap实现的,其他的还有LRULimitedMemoryCache,FIFOLimitedMemoryCache等等,它们的公共特点就是缓存大小有限制,不同点是在缓存超过限制的时候删除的规则不一样
磁盘缓存:UIL中的磁盘缓存比内存缓存简单了好多,主要分为两种实现,一种是UIL中默认使用的LruDiskCache,它的底层实现是直接使用的DiskLruCache,只不过是做了一层封装;另一种实现是直接对文件类File进行操作,其中有两个具体实现,一个是有缓存大小限制的,另一个是没有限制的
UIL工作流程:就是内存缓存->磁盘缓存->网络3个流程,详细在前一节有介绍

优缺点
优点:比较老的框架,稳定,加载速度适中
缺点:不支持GIF图片加载,缓存机制没有和http的缓存很好的结合,完全是自己的一套缓存机制

0.ImageLoader中使用的设计模式

单例模式

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

建造者模式

在ImageLoaderConfiguration和DisplayImageOptions都是使用了建造者模式,原因是它们有很多可选的属性可以被设置,使用建造者模式可以使得代码更清晰和健壮。

学到的知识

看源码的方式:先了解大致的流程,或者自己猜想工作流程,然后带着大局观去学习源码,这样有利于保持思路的清晰,而不至于在函数各种跳转的过程中迷失了,导致兴趣全无

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容