Universal-Image-Loader解析(二)内部缓存原理

Universal-Image-Loader解析系列

Universal-Image-Loader解析(一)基本介绍与使用
Universal-Image-Loader解析(二)内部缓存原理
Universal-Image-Loader解析(三)源代码解析

前篇文章则跟大家介绍了UIL的一些常用用法。

对于我们所知道的缓存,常用的是内存缓存MemoryCache和硬盘缓存DiscCache。一个读取快容量小,一个读取慢容量大。

对于各自使用哪种缓存,则可以在前面配置ImageLoaderConfiguration进行缓存设置,当然也可以自己自定义适合的缓存。

ImageLoaderConfiguration configuration = new ImageLoaderConfiguration.Builder(this)  
        .memoryCache(new WeakMemoryCache())  
        .build();  

对于Universal-Image-Loader来说它的缓存结构也是分为内存缓存MemoryCache和硬盘缓存DiskCache

一.MemoryCache内存缓存

首先先看个结构图,理解UIL里面内存缓存的结构

MemoryCache

由于空间有限就没画成标准的UML类图形式。
对于基类MemoryCache它则是一个接口,里面定义了put,get图片的方法

public interface MemoryCache {
    ...
    boolean put(String key, Bitmap value);
    
    Bitmap get(String key);

    Bitmap remove(String key);

    Collection<String> keys();

    void clear();
}

都是大家比较所熟悉的方法,而对于其他的类

我们一个个看

LruMemoryCache

这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用。直接实现了MemoryCache方法

public class LruMemoryCache implements MemoryCache {

    private final LinkedHashMap<String, Bitmap> map;
    //最大容量
    private final int maxSize;
    /** 目前缓存的容量大小 */
    private int size;
    public LruMemoryCache(int maxSize) {
        ...
        this.maxSize = maxSize;
        this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);
    }
    @Override
    public final Bitmap get(String key) {
        ...
        synchronized (this) {
            return map.get(key);
        }
    }
    @Override
    public final boolean put(String key, Bitmap value) {
        ...
        synchronized (this) {
            size += sizeOf(key, value);
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

    /**
     * Lru算法,当容量超过最大缓存容量,则移除最久的条目
     */
    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);
            }
        }
    }

    @Override
    public final Bitmap remove(String key) {
        ...
        synchronized (this) {
            Bitmap previous = map.remove(key);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
            return previous;
        }
    }
    ...
    //返回图片的字节大小
    private int sizeOf(String key, Bitmap value) {
        return value.getRowBytes() * value.getHeight();
    }
    ...
}

LruMemoryCache的源码也比较简单,内部有个成员变量LinkedHashMap<String, Bitmap> map这里直接进行保存的话则是强引用的形式。
主要看get,put方法。
对于get方法来说,比较简单,直接根据指定的key返回对应的图片。
而对于put方法来说,则需要考虑容量的问题。

@Override
    public final boolean put(String key, Bitmap value) {
        ...
        synchronized (this) {
            size += sizeOf(key, value);
            Bitmap previous = map.put(key, value);
            if (previous != null) {
                size -= sizeOf(key, previous);
            }
        }

        trimToSize(maxSize);
        return true;
    }

put方法首先调用了sizeof方法,该方法则是返回指定Bitmap的字节大小,之后size +=,总缓存量增加,之后调用trimToSize该方法则是进行缓存容量判断的。

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

如果加入后的size 缓存容量 <= maxSize 最大缓存容量,则直接break,不用进行判定处理。
如果大于的话,则直接移除最久未使用的。

大家肯定有疑问,它到底怎么判断最久未使用的?没看到相关代码呀?

相信知道LinkedHashMap的话可能就知道。
LinkedHashMap自身已经实现了顺序存储,默认情况下是按照元素的添加顺序存储,也可以启用按照访问顺序存储,即最近读取的数据放在最前面,最早读取的数据放在最后面,然后它还有一个判断是否删除最老数据的方法,默认是返回false,即不删除数据。大家常见也就是按顺序存储,很少忘了它还可以根据最近未使用的方法。

//LinkedHashMap的一个构造函数,当参数accessOrder为true时,即会按照访问顺序排序,最近访问的放在最前,最早访问的放在后面
public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) {
        super(initialCapacity, loadFactor);
        this.accessOrder = accessOrder;
}

//LinkedHashMap自带的判断是否删除最老的元素方法,默认返回false,即不删除老数据
//我们要做的就是重写这个方法,当满足一定条件时删除老数据
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
}

回看我们前面LinkedHashMap的创建

  this.map = new LinkedHashMap<String, Bitmap>(0, 0.75f, true);

再举个使用例子

例子

就比较明了了。

BaseMemoryCache

BaseMemoryCache同样也是实现了MemoryCache方法,不过它还是一个抽象类。
它是一个内存缓存的基类,实现了内存缓存中常用的方法,只不过它里面提供了一个非强引用的Reference作为扩展,方便GC的回收,避免OOM.

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

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

代码也比较简单,内存持有一个Map<String, Reference<Bitmap>> softMap来保存非强引用对象,具体的引用类型则看它实现的抽象方法createReference

WeakMemoryCache

我们看它的一个子类WeakMemoryCache则是继承与BaseMemory,实现createReference

public class WeakMemoryCache extends BaseMemoryCache {
    @Override
    protected Reference<Bitmap> createReference(Bitmap value) {
        return new WeakReference<Bitmap>(value);
    }
}

很明显是来保存弱引用对象的。

LimitedMemoryCache

我们看它的另外一个子类LimitedMemoryCache,但它并没有实现BaseMemoryCache里的createReference方法,它也是一个抽象类,在BaseMemoryCache基础上封装了个抽象方法
protected abstract Bitmap removeNext();用来处理当缓存容量不足时的情况。

public abstract class LimitedMemoryCache extends BaseMemoryCache {
    ...
    //当前保存的Bitmap,用来统计缓存数
    private final List<Bitmap> hardCache = Collections.synchronizedList(new LinkedList<Bitmap>());
    ...
    @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) {
            while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }
            hardCache.add(value);
            cacheSize.addAndGet(valueSize);

            putSuccessfully = true;
        }
        // Add value to soft cache
        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);
    }
   ...
    protected abstract int getSize(Bitmap value);

    protected abstract Bitmap removeNext();
}

可以看到在 LimitedMemoryCache里面又有一个List<Bitmap>保存的是强引用,而在BaseMemoryCache里面也有个Map<String, Reference<Bitmap>> softMap来保存Bitmap,为什么要这样。

这主要是因为在BaseMemoryCache里面并没有做缓存限制处理,它只是封装实现了基本的Bitmap的put,get。而当面对缓存容量有限的情况下,则需要交给子类去处理。

我们看下这里的put方法,关键在

while (curCacheSize + valueSize > sizeLimit) {
                Bitmap removedValue = removeNext();
                if (hardCache.remove(removedValue)) {
                    curCacheSize = cacheSize.addAndGet(-getSize(removedValue));
                }
            }

当超过容量时,调用抽象方法removeNext由子类自行实现,之后hardCache移除,但此时并没有调用softMap的移除。

也就是对于List<Bitmap>来说,当它的缓存容量超过的时候,它会移除第一个对象来缓解容量,但是保存在Map<String, Reference<Bitmap>> softMap里面的Bitmap并没有被移除。
如果这样下去softMap岂不是会无限大?

这是因为在Map<String, Reference<Bitmap>> softMap里面保存的Bitmap是弱引用的存在,而在List<Bitmap>里面保存的是强引用,当内存不足的时候,GC则会先清除softMap里面的对象。

FIFOLimitedMemoryCache

我们看下LimitedMemoryCache的一个子类FIFOLimitedMemoryCache,看到FIFO也就是先进先出了。

public class FIFOLimitedMemoryCache extends LimitedMemoryCache {

    private final List<Bitmap> queue = Collections.synchronizedList(new LinkedList<Bitmap>());
    ...
    @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
    protected Bitmap removeNext() {
        return queue.remove(0);
    }

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

可以看到同样的这里也有个List<Bitmap> queue来保存记录,而在removeNext那里,返回的正是队列的第一个元素,符合FIFO。

LRULimitedMemoryCache

再来看一个另外一个子类LRULimitedMemoryCache也就是最近未使用删除。

public class LRULimitedMemoryCache extends LimitedMemoryCache {

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

可以看到,这里的LRU处理则是使用LinkedHashMap,在它的构造方法中第三个参数为true表示使用LRU,之后再removeNext返回那个Bitmap。

同理其他子类也如下,就不一一列举。

MemoryCache小结

1. 只使用的是强引用缓存

  • LruMemoryCache(这个类就是这个开源框架默认的内存缓存类,缓存的是bitmap的强引用)

2.使用强引用和弱引用相结合的缓存有

  • UsingFreqLimitedMemoryCache(如果缓存的图片总量超过限定值,先删除使用频率最小的bitmap)

  • LRULimitedMemoryCache(这个也是使用的lru算法,和LruMemoryCache不同的是,他缓存的是bitmap的弱引用)

  • FIFOLimitedMemoryCache(先进先出的缓存策略,当超过设定值,先删除最先加入缓存的bitmap)

  • LargestLimitedMemoryCache(当超过缓存限定值,先删除最大的bitmap对象)

  • LimitedAgeMemoryCache(当 bitmap加入缓存中的时间超过我们设定的值,将其删除)

3.只使用弱引用缓存

  • WeakMemoryCache(这个类缓存bitmap的总大小没有限制,唯一不足的地方就是不稳定,缓存的图片容易被回收掉)

二.DiskCache硬盘缓存

同样先来看个结构

DiskCache

DiskCache的设计其实和MemoryCache一样,对于基类DiskCache,它同样是一个接口

public interface DiskCache {
    //返回硬盘缓存的根目录
    File getDirectory();

    File get(String imageUri);

    boolean save(String imageUri, InputStream imageStream, IoUtils.CopyListener listener) throws IOException;

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

    boolean remove(String imageUri);

    void close();

    void clear();
}

同样一个个看

LruDiskCache

LruDiskCache则是直接实现了DiskCache接口,采用LRU算法来进行缓存处理。
再理解LruDiskCache前,先理解另一个类DiskLruCache

final class DiskLruCache implements Closeable {
    static final String JOURNAL_FILE = "journal";
    static final String JOURNAL_FILE_TEMP = "journal.tmp";
    static final String JOURNAL_FILE_BACKUP = "journal.bkp";
    static final String MAGIC = "libcore.io.DiskLruCache";
    ...
    private final LinkedHashMap<String, Entry> lruEntries =
            new LinkedHashMap<String, Entry>(0, 0.75f, true);
    ...
    final ThreadPoolExecutor executorService =
            new ThreadPoolExecutor(0, 1, 60L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
    ...
    public static DiskLruCache open(File directory, int appVersion, int valueCount, long maxSize, int maxFileCount)
            throws IOException {
        ...
    }
    ...
    public synchronized Snapshot get(String key) throws IOException {
        ...
    }
    ...
    private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        ...
    }
    /** A snapshot of the values for an entry. */
    public final class Snapshot implements Closeable {
        private final String key;
        private final long sequenceNumber;
        private File[] files;
        private final InputStream[] ins;
        private final long[] lengths;
        ...
    }
    ...
    public final class Editor {
        private final Entry entry;
        private final boolean[] written;
        private boolean hasErrors;
        private boolean committed;
        ...
    }
    ...
    private final class Entry {
        private final String key;

        private final long[] lengths;

        private boolean readable;

        private Editor currentEditor;

        private long sequenceNumber;
        ...
    }

这个DiskLruCache比较长也比较复杂,它是LruDiskCache的一个文件工具类。这里的缓存数据存储在文件系统上的一个目录。
同时也注意到这里的一个成员变量
private final LinkedHashMap<String, Entry> lruEntries =new LinkedHashMap<String, Entry>(0, 0.75f, true);
可以知道这是用来处理LRU的。

同时这里的value则是EntryEntry则是封装了当前文件的编辑情况Ediotr以及key
而这里Editor封装了文件的写入情况OutputStreamSnapshot封装了文件的读取情况InputStream

回头看回LruDiskCache

public class LruDiskCache implements DiskCache {
    protected DiskLruCache cache;
    private File reserveCacheDir;

    protected final FileNameGenerator fileNameGenerator;
    ...
    public LruDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator, long cacheMaxSize,
            int cacheMaxFileCount) throws IOException {
        ...
        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
        initCache(cacheDir, reserveCacheDir, cacheMaxSize, cacheMaxFileCount);
    }

    private void initCache(File cacheDir, File reserveCacheDir, long cacheMaxSize, int cacheMaxFileCount)
        ...
            cache = DiskLruCache.open(cacheDir, 1, 1, cacheMaxSize, cacheMaxFileCount);
        ...
    }
    @Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } 
        ...
    }
    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        DiskLruCache.Editor editor = cache.edit(getKey(imageUri));
        ...
        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        }
        ...
        return savedSuccessfully;
    }

首先LruDiskCache内部成员变量带有DiskLruCache还有文件的保存目录等,在它的构造方法中调用DiskLruCache.open方法创建了DiskLruCache对象,而在它的open方法里,则根据文件的目录情况创建了对应的文件系统。

再看它的save方法,先调用getKey方法将uri转换为对应的key,而在cache,edit中

private synchronized Editor edit(String key, long expectedSequenceNumber) throws IOException {
        ...
        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;
        ...
        return editor;
    }

则是根据指定的key先判断缓存文件中有没有相应的key,如果没有则创建一个Entry对象持有它,之后保存在lruEntries之后,创建一个当前Entry的编辑对象Editor,以便之后写入到文件中。

s之后调用了

        OutputStream os = new BufferedOutputStream(editor.newOutputStream(0), bufferSize);

editor.newOutputStream则是根据当前目录和key创建出一个文件,之后打开这个文件的一个输出流情况,获取到之后就进行Bitmap的写入。

同理,看下LruDiskCache的get方法

@Override
    public File get(String imageUri) {
        DiskLruCache.Snapshot snapshot = null;
        try {
            snapshot = cache.get(getKey(imageUri));
            return snapshot == null ? null : snapshot.getFile(0);
        } 
        ...
    }

调用了cache,get

public synchronized Snapshot get(String key) throws IOException {
        。。。
        Entry entry = lruEntries.get(key);
        ...
        File[] files = new File[valueCount];
        InputStream[] ins = new InputStream[valueCount];
        try {
            File file;
            for (int i = 0; i < valueCount; i++) {
                file = entry.getCleanFile(i);
                files[i] = file;
                ins[i] = new FileInputStream(file);
            }
        } 
        ...
        return new Snapshot(key, entry.sequenceNumber, files, ins, entry.lengths);
    }

在get方法中,先根据key拿到对应的Entry,再拿到对应的文件打开输入流,之后传入到Snapshot
而在snapshot.getFile

/** Returns file with the value for {@code index}. */
        public File getFile(int index) {
            return files[index];
        }

返回的则是对应的文件。

BaseDiskCache

BaseDiskCache同样也是直接实现了DiskCache方法,实现的方法也比较简单

public abstract class BaseDiskCache implements DiskCache {
    ...
    protected final File cacheDir;
    protected final File reserveCacheDir;

    protected final FileNameGenerator fileNameGenerator;
    
    public BaseDiskCache(File cacheDir, File reserveCacheDir, FileNameGenerator fileNameGenerator) {
        ...
        this.cacheDir = cacheDir;
        this.reserveCacheDir = reserveCacheDir;
        this.fileNameGenerator = fileNameGenerator;
    }
    
    @Override
    public boolean save(String imageUri, Bitmap bitmap) throws IOException {
        File imageFile = getFile(imageUri);
        File tmpFile = new File(imageFile.getAbsolutePath() + TEMP_IMAGE_POSTFIX);
        OutputStream os = new BufferedOutputStream(new FileOutputStream(tmpFile), bufferSize);
        boolean savedSuccessfully = false;
        try {
            savedSuccessfully = bitmap.compress(compressFormat, compressQuality, os);
        } finally {
            IoUtils.closeSilently(os);
            if (savedSuccessfully && !tmpFile.renameTo(imageFile)) {
                savedSuccessfully = false;
            }
            if (!savedSuccessfully) {
                tmpFile.delete();
            }
        }
        bitmap.recycle();
        return savedSuccessfully;
    }
    
    @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);
    }

比较简单,根据对应的文件去打开获取。它的两个子类LimitedAgeDiskCacheUnlimitedDiskCache也都不一一扩展开了。

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

推荐阅读更多精彩内容