Glide-源码详解

前言:

之前的文章中,笔者介绍了很多Glide的使用方法,但是由于Glide框架封装得太好了,很多人在使用的时候,只是知其然不知其所以然,为了不要仅仅成为"cv工程师",只会复制粘贴,所以这篇文章我们就一起来研究一下Glide的源码,看看Glide到底是怎么将一张图片加载出来的~

Glide 系列目录

前方高能预警,本文篇幅较长,阅读需要耐心

本文基于Glide 3.7.0版本

一.Glide的构造

//Glide.java

 Glide(Engine engine, MemoryCache memoryCache, BitmapPool bitmapPool, Context context, DecodeFormat decodeFormat) {
              ...
    }
Glide是通过GlideBuilder中的createGlide方法生成的(核心代码如下)
//GlideBuilder.java

Glide createGlide() {
        ...
        return new Glide(engine, memoryCache, bitmapPool, context, decodeFormat);
    }
Glide的构造参数主要有四个,都是通过createGlide生成的.
  • MemoryCache 内存缓存

  • BitmapPool 图片池

  • DecodeFormat 图片格式

  • Engine 引擎类

1.MemoryCache :内存缓存 LruResourceCache

//MemorySizeCalculator.java

final int maxSize = getMaxSize(activityManager);

private static int getMaxSize(ActivityManager activityManager) {
    //每个进程可用的最大内存
    final int memoryClassBytes = activityManager.getMemoryClass() * 1024 * 1024;

    //判断是否低配手机
    final boolean isLowMemoryDevice = isLowMemoryDevice(activityManager);
    
    return Math.round(memoryClassBytes
            * (isLowMemoryDevice ? LOW_MEMORY_MAX_SIZE_MULTIPLIER : MAX_SIZE_MULTIPLIER));
}
最大内存:如果是低配手机,就每个进程可用的最大内存乘以0.33,否则就每个进程可用的最大内存乘以0.4
//MemorySizeCalculator.java

int screenSize = screenDimensions.getWidthPixels() * screenDimensions.getHeightPixels()
                * BYTES_PER_ARGB_8888_PIXEL;(宽*高*4)
int targetPoolSize = screenSize * BITMAP_POOL_TARGET_SCREENS;(宽*高*4*4)
int targetMemoryCacheSize = screenSize * MEMORY_CACHE_TARGET_SCREENS;(宽*高*4*2)

//判断是否超过最大值,否则就等比缩小
if (targetMemoryCacheSize + targetPoolSize <= maxSize) {
    memoryCacheSize = targetMemoryCacheSize;
    bitmapPoolSize = targetPoolSize;
} else {
    int part = Math.round((float) maxSize / (BITMAP_POOL_TARGET_SCREENS + MEMORY_CACHE_TARGET_SCREENS));
    memoryCacheSize = part * MEMORY_CACHE_TARGET_SCREENS;
    bitmapPoolSize = part * BITMAP_POOL_TARGET_SCREENS;
}
targetPoolSize 和 targetMemoryCacheSize 之和不能超过maxSize 否则就等比缩小
//GlideBuilder.java

memoryCache = new LruResourceCache(calculator.getMemoryCacheSize());
内存缓存用的是targetMemoryCacheSize (即一般是缓存大小是屏幕的宽 * 高 * 4 * 2)

2.BitmapPool 图片池 LruBitmapPool

int size = calculator.getBitmapPoolSize();
bitmapPool = new LruBitmapPool(size);
图片池用的是targetPoolSize(即一般是缓存大小是屏幕的宽4*4)

3.DecodeFormat 图片格式

DecodeFormat DEFAULT = PREFER_RGB_565
默认是RGB_565

4.Engine 引擎类

//GlideBuilder.java

engine = new Engine(memoryCache, diskCacheFactory, diskCacheService, sourceService);
engine 里面主要参数
  • 内存缓存 memoryCache
  • 本地缓存 diskCacheFactory
  • 处理源资源的线程池 sourceService
  • 处理本地缓存的线程池 diskCacheService

(1)memoryCache:内存缓存 LruBitmapPool

上面已经做了介绍

(2)diskCacheFactory:本地缓存 DiskLruCacheFactory

//DiskCache.java

/** 250 MB of cache. */
        int DEFAULT_DISK_CACHE_SIZE = 250 * 1024 * 1024;
        String DEFAULT_DISK_CACHE_DIR = "image_manager_disk_cache";
默认大小:250 MB
默认目录:image_manager_disk_cache

(3)sourceService 处理源资源的线程池 (ThreadPoolExecutor的子类)

  final int cores = Math.max(1, Runtime.getRuntime().availableProcessors());//获得可用的处理器个数
  sourceService = new FifoPriorityThreadPoolExecutor(cores);
线程池的核心线程数量等于获得可用的处理器个数

(4)diskCacheService 处理本地缓存的线程池 (ThreadPoolExecutor的子类)

diskCacheService = new FifoPriorityThreadPoolExecutor(1);
线程池的核心线程数量为1

二.with方法

with方法有很多重载,最后会返回一个RequestManager
//Glide.java

/**
 * @see #with(android.app.Activity)
 * @see #with(android.app.Fragment)
 * @see #with(android.support.v4.app.Fragment)
 * @see #with(android.support.v4.app.FragmentActivity)
 *
 * @param context Any context, will not be retained.
 * @return A RequestManager for the top level application that can be used to start a load.
 */
public static RequestManager with(Context context) {
    RequestManagerRetriever retriever = RequestManagerRetriever.get();
    return retriever.get(context);
}

就算你传入的是Context ,这里也会根据你Context 实际的类型,走不同的分支
//RequestManagerRetriever.java

public RequestManager get(Context context) {
    if (context == null) {
        throw new IllegalArgumentException("You cannot start a load on a null Context");
    } else if (Util.isOnMainThread() && !(context instanceof Application)) {
        if (context instanceof FragmentActivity) {
            return get((FragmentActivity) context);
        } else if (context instanceof Activity) {
            return get((Activity) context);
        } else if (context instanceof ContextWrapper) {
            return get(((ContextWrapper) context).getBaseContext());
        }
    }
    return getApplicationManager(context);
}
这里以FragmentActivity为例,最后会创建一个无界面的Fragment,即SupportRequestManagerFragment ,让请求和你的activity的生命周期同步
//RequestManagerRetriever.java

public RequestManager get(FragmentActivity activity) {
    if (Util.isOnBackgroundThread()) {
        return get(activity.getApplicationContext());
    } else {
        assertNotDestroyed(activity);
        FragmentManager fm = activity.getSupportFragmentManager();
        return supportFragmentGet(activity, fm);
    }
}
RequestManager supportFragmentGet(Context context, FragmentManager fm) {
    SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm);
    RequestManager requestManager = current.getRequestManager();
    if (requestManager == null) {
        requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
        current.setRequestManager(requestManager);
    }
    return requestManager;
}
这里需要注意一下,如果你是在子线程调用with方法,或者传入的Context是Application的话,请求是跟你的Application的生命周期同步
//RequestManagerRetriever.java

private RequestManager getApplicationManager(Context context) {
    // Either an application context or we're on a background thread.
    if (applicationManager == null) {
        synchronized (this) {
            if (applicationManager == null) {
                // Normally pause/resume is taken care of by the fragment we add to the fragment or activity.
                // However, in this case since the manager attached to the application will not receive lifecycle
                // events, we must force the manager to start resumed using ApplicationLifecycle.
                applicationManager = new RequestManager(context.getApplicationContext(),
                        new ApplicationLifecycle(), new EmptyRequestManagerTreeNode());
            }
        }
    }
    return applicationManager;
}

三.load方法

这里方法也有很多重载
//RequestManager.java

public DrawableTypeRequest<String> load(String string) {
    return (DrawableTypeRequest<String>) fromString().load(string);
}
但是最后都会返回一个DrawableTypeRequest (继承了DrawableRequestBuilder)
DrawableRequestBuilder就是支持链式调用的一个类,我们平时有类似的需求的时候也可以模仿这样的处理方式,把一些非必须参数用链式调用的方式来设置

四.into方法

//GenericRequestBuilder.java    

public Target<TranscodeType> into(ImageView view) {
        Util.assertMainThread();
        if (view == null) {
            throw new IllegalArgumentException("You must pass in a non null View");
        }
        if (!isTransformationSet && view.getScaleType() != null) {
            switch (view.getScaleType()) {
                case CENTER_CROP:
                    applyCenterCrop();
                    break;
                case FIT_CENTER:
                case FIT_START:
                case FIT_END:
                    applyFitCenter();
                    break;
                //$CASES-OMITTED$
                default:
                    // Do nothing.
            }
        }
        return into(glide.buildImageViewTarget(view, transcodeClass));
    }
这里有三点需要注意的:
  • 1.Util.assertMainThread();这里会检查是否主线程,不是的话会抛出异常,所以into方法必须在主线程中调用.

  • 2.当你没有调用transform方法,并且你的ImageView设置了ScaleType,那么他会根据你的设置,对图片做处理(具体处理可以查看DrawableRequestBuilder的applyCenterCrop或者applyFitCenter方法,我们自己自定义BitmapTransformation也可以参考这里的处理).

  • 3.view在这里被封装成一个Target.

      //GenericRequestBuilder.java        
    
      public <Y extends Target<TranscodeType>> Y into(Y target) {
          Util.assertMainThread();
          if (target == null) {
              throw new IllegalArgumentException("You must pass in a non null Target");
          }
          if (!isModelSet) {
              throw new IllegalArgumentException("You must first set a model (try #load())");
          }
          Request previous = target.getRequest();
          if (previous != null) {
              previous.clear();
              requestTracker.removeRequest(previous);
              previous.recycle();
          }
          Request request = buildRequest(target);
          target.setRequest(request);
          lifecycle.addListener(target);
          requestTracker.runRequest(request);
          return target;
      }
    
这里可以看到控件封装成的Target能够获取自身绑定的请求,当发现之前的请求还在的时候,会把旧的请求清除掉,绑定新的请求,这也就是为什么控件复用时不会出现图片错位的问题(这点跟我在Picasso源码中看到的处理方式很相像).
接着在into里面会调用buildRequest方法来创建请求
//GenericRequestBuilder.java    

private Request buildRequest(Target<TranscodeType> target) {
        if (priority == null) {
            priority = Priority.NORMAL;
        }
        return buildRequestRecursive(target, null);
    }

//GenericRequestBuilder.java

private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {
        if (thumbnailRequestBuilder != null) {
           ...
            Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator);
           ...
            Request thumbRequest = thumbnailRequestBuilder.buildRequestRecursive(target, coordinator);
           ...
            coordinator.setRequests(fullRequest, thumbRequest);
            return coordinator;
        } else if (thumbSizeMultiplier != null) {           
            ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
            Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator);
            Request thumbnailRequest = obtainRequest(target, thumbSizeMultiplier, getThumbnailPriority(), coordinator);
            coordinator.setRequests(fullRequest, thumbnailRequest);
            return coordinator;
        } else {
            // Base case: no thumbnail.
            return obtainRequest(target, sizeMultiplier, priority, parentCoordinator);
        }
    }
1.这里就是请求的生成,buildRequestRecursive里面if有三个分支,这里是根据你设置thumbnail的情况来判断的,第一个是设置缩略图为新的请求的情况,第二个是设置缩略图为float的情况,第三个就是没有设置缩略图的情况.
前两个设置了缩略图的是有两个请求的,fullRequest和thumbnailRequest,没有设置缩略图则肯定只有一个请求了.
2.请求都是通过obtainRequest方法生成的(这个简单了解一下就行)
//GenericRequestBuilder.java

private Request obtainRequest(Target<TranscodeType> target, float sizeMultiplier, Priority priority,
            RequestCoordinator requestCoordinator) {
        return GenericRequest.obtain(...);
    }
REQUEST_POOL是一个队列,当队列中有,那么就从队列中取,没有的话就新建一个GenericRequest
//GenericRequest.java

public static <A, T, Z, R> GenericRequest<A, T, Z, R> obtain(...) {

        GenericRequest<A, T, Z, R> request = (GenericRequest<A, T, Z, R>) REQUEST_POOL.poll();
        if (request == null) {
            request = new GenericRequest<A, T, Z, R>();
        }
        request.init(...);
        return request;
    }
回到into方法:当创建了请求后runRequest会调用Request的begin方法,即调用GenericRequest的begin方法
//GenericRequestBuilder.java

 public <Y extends Target<TranscodeType>> Y into(Y target) {
      
        Request request = buildRequest(target);
      ...
        requestTracker.runRequest(request);
      ...
    }

//GenericRequest.java

 public void begin() {
    ...
      if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
            onSizeReady(overrideWidth, overrideHeight);
        } else {
            target.getSize(this);
        }
    ...
    }
最终会调用Engine的load方法
//GenericRequest.java

public void onSizeReady(int width, int height) {
    ...
    loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
                priority, isMemoryCacheable, diskCacheStrategy, this);
    ...
    }
我们先看load方法的前面一段:
1.首先会尝试从cache里面取,这里cache就是Glide的构造函数里面的MemoryCache(是一个LruResourceCache),如果取到了,就从cache里面删掉,然后加入activeResources中
2.如果cache里面没取到,就会从activeResources中取,activeResources是一个以弱引用为值的map,他是用于存储使用中的资源.之所以在内存缓存的基础上又多了这层缓存,是为了当内存不足而清除cache中的资源中,不会影响使用中的资源.
//Engine.java   

public <T, Z, R> LoadStatus load(...) {
        ...
        EngineResource<?> cached = loadFromCache(key, isMemoryCacheable);
        if (cached != null) {
            cb.onResourceReady(cached);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from cache", startTime, key);
            }
            return null;
        }
        EngineResource<?> active = loadFromActiveResources(key, isMemoryCacheable);
        if (active != null) {
            cb.onResourceReady(active);
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                logWithTimeAndKey("Loaded resource from active resources", startTime, key);
            }
            return null;
        }
        ...
    }
load方法接着会通过EngineJobFactory创建一个EngineJob,里面主要管理里两个线程池,diskCacheService和sourceService,他们就是Glide构造函数中Engine里面创建的那两个线程池.
//Engine.java

public <T, Z, R> LoadStatus load(...) {
            ...
            EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
           ...
        }

//Engine.java
static class EngineJobFactory {
    private final ExecutorService diskCacheService;
    private final ExecutorService sourceService;
    private final EngineJobListener listener;

    public EngineJobFactory(ExecutorService diskCacheService, ExecutorService sourceService,
            EngineJobListener listener) {
        this.diskCacheService = diskCacheService;
        this.sourceService = sourceService;
        this.listener = listener;
    }

    public EngineJob build(Key key, boolean isMemoryCacheable) {
        return new EngineJob(key, diskCacheService, sourceService, isMemoryCacheable, listener);
    }
}
接着说load方法,前面创建了EngineJob,接着调用EngineJob的start方法,并将EngineRunnable放到diskCacheService(处理磁盘缓存的线程池里面运行),接着线程池就会调用EngineRunnable的run方法.
//Engine.java

public <T, Z, R> LoadStatus load(...) {
        ...
        EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
        jobs.put(key, engineJob);
        engineJob.addCallback(cb);
        engineJob.start(runnable);
       ...
    }

//EngineJob.java

public void start(EngineRunnable engineRunnable) {
    this.engineRunnable = engineRunnable;
    future = diskCacheService.submit(engineRunnable);
}

//EngineRunnable.java

public void run() {
       ...
        try {
            resource = decode();
        } catch (Exception e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Exception decoding", e);
            }
            exception = e;
        }
       ...
        if (resource == null) {
            onLoadFailed(exception);
        } else {
            onLoadComplete(resource);
        }
    }
run里面调用的是decode()方法,里面会尝试先从磁盘缓存中读取,如果不行就从源资源中读取
//EngineRunnable.java   

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        //第一次会走这
        return decodeFromCache();//从磁盘缓存中读取

    } else {

        return decodeFromSource();//从源资源中读取

    }
}
我们先来看从磁盘中读取的策略
//EngineRunnable.java

private Resource<?> decodeFromCache() throws Exception {
    Resource<?> result = null;
    try {
        result = decodeJob.decodeResultFromCache();
    } catch (Exception e) {
        if (Log.isLoggable(TAG, Log.DEBUG)) {
            Log.d(TAG, "Exception decoding result from cache: " + e);
        }
    }

    if (result == null) {
        result = decodeJob.decodeSourceFromCache();
    }
    return result;
}
我们可以看到这里先尝试读取处理后的图片(Result),然后再尝试读取原图,但是这里面具体逻辑会根据你设置的磁盘缓存策略来决定是否真的会读取处理图和原图
那么我们再回到EngineRunnable的run()方法中
public void run() {
           ...
            try {
                resource = decode();
            } catch (Exception e) {
                if (Log.isLoggable(TAG, Log.VERBOSE)) {
                    Log.v(TAG, "Exception decoding", e);
                }
                exception = e;
            }
           ...
            if (resource == null) {
                onLoadFailed(exception);
            } else {
                onLoadComplete(resource);
            }
        }
第一次走decode的时候会先尝试从磁盘中获取,如果获取的为null,那么在onLoadFailed方法里面又会把这个run再次放入线程池中,但是这次是放入sourceService(处理源资源的线程池)
//EngineRunnable.java

private void onLoadFailed(Exception e) {
    if (isDecodingFromCache()) {
        stage = Stage.SOURCE;
        manager.submitForSource(this);
    } else {
        manager.onException(e);
    }
}

//EngineJob.java

@Override
public void submitForSource(EngineRunnable runnable) {
    future = sourceService.submit(runnable);
}
接着sourceService里面又会调用调用EngineRunnable的run方法,这次decode里面会走从源资源读取的那条分支
//EngineRunnable.java   

private Resource<?> decode() throws Exception {
    if (isDecodingFromCache()) {
        //第一次会走这
        return decodeFromCache();//从磁盘缓存中读取

    } else {
        //第二次会走这
        return decodeFromSource();//从源资源读取

    }
}

//DecodeJob.java

public Resource<Z> decodeFromSource() throws Exception {
    Resource<T> decoded = decodeSource();//获取数据,并解码
    return transformEncodeAndTranscode(decoded);//处理图片
}
里面主要做了两件事,一个是获取图片,一个是处理图片
1.我们先来看获取图片的decodeSource方法
//DecodeJob.java

private Resource<T> decodeSource() throws Exception {
       ...
        //拉取数据
        final A data = fetcher.loadData(priority);
       ...
        //解码,并保存源资源到磁盘
        decoded = decodeFromSourceData(data);
       ...
    return decoded;
}

//DecodeJob.java
private Resource<T> decodeFromSourceData(A data) throws IOException {
    final Resource<T> decoded;
    if (diskCacheStrategy.cacheSource()) {
        //解码并保存源资源(图片)到磁盘缓存中
        decoded = cacheAndDecodeSourceData(data);
    } else {
        long startTime = LogTime.getLogTime();
        decoded = loadProvider.getSourceDecoder().decode(data, width, height);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Decoded from source", startTime);
        }
    }
    return decoded;
}
这里调用了DataFetcher的loadData方法来获取数据,DataFetcher有很多实现类,一般来说我们都是从网络中读取数据,我们这边就以HttpUrlFetcher为例
//HttpUrlFetcher.java

@Override
public InputStream loadData(Priority priority) throws Exception {
    return loadDataWithRedirects(glideUrl.toURL(), 0 /*redirects*/, null /*lastUrl*/, glideUrl.getHeaders());
}

private InputStream loadDataWithRedirects(URL url, int redirects, URL lastUrl, Map<String, String> headers)
        throws IOException {
    if (redirects >= MAXIMUM_REDIRECTS) {
        throw new IOException("Too many (> " + MAXIMUM_REDIRECTS + ") redirects!");
    } else {
        // Comparing the URLs using .equals performs additional network I/O and is generally broken.
        // See http://michaelscharf.blogspot.com/2006/11/javaneturlequals-and-hashcode-make.html.
        try {
            if (lastUrl != null && url.toURI().equals(lastUrl.toURI())) {
                throw new IOException("In re-direct loop");
            }
        } catch (URISyntaxException e) {
            // Do nothing, this is best effort.
        }
    }
    urlConnection = connectionFactory.build(url);
    for (Map.Entry<String, String> headerEntry : headers.entrySet()) {
      urlConnection.addRequestProperty(headerEntry.getKey(), headerEntry.getValue());
    }
    urlConnection.setConnectTimeout(2500);
    urlConnection.setReadTimeout(2500);
    urlConnection.setUseCaches(false);
    urlConnection.setDoInput(true);

    // Connect explicitly to avoid errors in decoders if connection fails.
    urlConnection.connect();
    if (isCancelled) {
        return null;
    }
    final int statusCode = urlConnection.getResponseCode();
    if (statusCode / 100 == 2) {
        //请求成功
        return getStreamForSuccessfulRequest(urlConnection);
    } else if (statusCode / 100 == 3) {
        String redirectUrlString = urlConnection.getHeaderField("Location");
        if (TextUtils.isEmpty(redirectUrlString)) {
            throw new IOException("Received empty or null redirect url");
        }
        URL redirectUrl = new URL(url, redirectUrlString);
        return loadDataWithRedirects(redirectUrl, redirects + 1, url, headers);
    } else {
        if (statusCode == -1) {
            throw new IOException("Unable to retrieve response code from HttpUrlConnection.");
        }
        throw new IOException("Request failed " + statusCode + ": " + urlConnection.getResponseMessage());
    }
}
2.看完了获取图片的方法,我们再来看看处理图片的方法transformEncodeAndTranscode
//DecodeJob.java

private Resource<Z> transformEncodeAndTranscode(Resource<T> decoded) {
   ...
    //对图片做剪裁等处理
    Resource<T> transformed = transform(decoded);
  ...
    //将处理后的图片写入磁盘缓存(会根据配置来决定是否写入)
    writeTransformedToCache(transformed);
 ...
    //转码,转为需要的类型
    Resource<Z> result = transcode(transformed);
 ...
    return result;
}

Glide的整个加载流程就基本上走完了,整个篇幅还是比较长的,第一次看得时候可能会有点懵逼,需要结合着源码多走几遍才能够更加熟悉.

阅读源码并不是我们最终的目的,我们阅读源码主要有两个目的

一个是能够更加熟悉这个框架,那么使用的时候就更加得心应手了,比如我从源码中发现了很多文章都说错了默认的缓存策略
一个是学习里面的设计模式和思想,应用到我们自己的项目中,比如说面对接口编程,对于不同的数据,用DataFetcher的不同实现类来拉取数据

热门文章

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

推荐阅读更多精彩内容