源码分析Android的图片加载库 Glide的一次加载过程


基于com.github.bumptech.glide:glide:3.7.0
这是一篇快速过源码,而非品味细枝末节的分析,否则简书的2W byte的限制,可能要分好几期才能彻彻底底的讲完。

 Glide
    .with(myFragment)
    .load(url)
    .centerCrop()
    .placeholder(R.drawable.loading_spinner)
    .crossFade()
    .into(myImageView);

我们就以此来作为出发点。
可以看到整个请求使用的当前流行的流式代码,我们来逐个击破。

  • 初步调查

    • Glide
      注释写的很明白,是一个提供请求接口的单例,和我们熟悉的门面模式很相似,它是一个请求的入口,你可以看到,类中的很多方法都是静态的,直接通过Glide来调起的。
      那么我们直奔主题,看一看with方法,你会发现有很多重载,但是最后都是统一进入了fragmentGet或者是supportFragmentGet来获得一个RequestManager对象
     RequestManager supportFragmentGet(Context context, FragmentManager fm) {
        //根据传入的Fragment来获取RequestManager
        SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm);
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }
    
  会根据你具体传入的类型的不同,最终选择3.0+的Fragment还是AppCompat的Fragment。由于两种不同的Fragment的FragmentManager是不同的,此处有两种方法`getSupportRequestManagerFragment` 和`getRequestManagerFragment`,实际上原理是一样的。

SupportRequestManagerFragment getSupportRequestManagerFragment(final FragmentManager fm) {
//当前fragment栈中是否有我们需要的fragment在
SupportRequestManagerFragment current = (SupportRequestManagerFragment) fm.findFragmentByTag(
FRAGMENT_TAG);
if (current == null) {
//如果不存在,去我们的缓存Map中取
current = pendingSupportRequestManagerFragments.get(fm);
if (current == null) {
//如果依然没有去生成这个Fragment
current = new SupportRequestManagerFragment();
pendingSupportRequestManagerFragments.put(fm, current);
fm.beginTransaction().add(current, FRAGMENT_TAG).commitAllowingStateLoss();
//抹去缓存map中该key值
handler.obtainMessage(ID_REMOVE_SUPPORT_FRAGMENT_MANAGER, fm).sendToTarget();
}
}
return current;
}

     Glide利用生成额外的无界面Fragment到Framgent栈中,用来同步context的生命周期。

    * RequestManager
            A class for managing and starting requests for Glide
    用来管理和发起请求的类。
    我们顺着load方法去看,又是个重载方法,根据传入的type的类型不同,返回不同类型的`GenericRequestBuilder`

public DrawableTypeRequest<String> load(String string) {
return (DrawableTypeRequest<String>) fromString().load(string);
}
public DrawableTypeRequest<String> fromString() {
return loadGeneric(String.class);
}
private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
...
}

DrawableRequestBuilder.java

@Override
public DrawableRequestBuilder<ModelType> load(ModelType model) {
super.load(model);
return this;
}

GenericRequestBuilder.java

public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> load(ModelType model) {
this.model = model;
isModelSet = true;
return this;
}

     最后得到的是一个`DrawableRequestBuilder<ModelType>`对象,强转成`DrawableTypeRequest<ModelType>`。

    * GenericRequestBuilder的不断构造
     我们通过centerCrop方法去进中央裁剪,DrawableTypeRequest的centerCrop方法在父类DrawableRequestBuilder中

@Override
public DrawableRequestBuilder<ModelType> transform(Transformation<GifBitmapWrapper>... transformation) {
super.transform(transformation);
return this;
}

GenericRequestBuilder.java

public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> transform(
Transformation<ResourceType>... transformations) {
isTransformationSet = true;
if (transformations.length == 1) {
transformation = transformations[0];
} else {
transformation = new MultiTransformation<ResourceType>(transformations);
}

    return this;
}
    修改内部参数,并且以构造者模式返回自己本身以供继续修改使用。
   之后的`placeholder`方法也是继续修改GenericRequestBuilder的参数,为请求增加占屏图片。
   `crossFade`方法则是增加了animationFactory参数

GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> animate(
GlideAnimationFactory<TranscodeType> animationFactory) {
if (animationFactory == null) {
throw new NullPointerException("Animation factory must not be null!");
}
this.animationFactory = animationFactory;

    return this;
}
    期间方法不单单有transform,包括priority修改优先级,encoder修改编码方式,diskCacheStrategy修改硬盘缓存策略等等。
    
  * ###最后的into方法
      之前都是在买材料囤货,就是这个方法开始做请求。
     DrawableRequestBuilder.java

@Override
public Target<GlideDrawable> into(ImageView view) {
return super.into(view);
}

GenericRequestBuilder.java

public Target<TranscodeType> into(ImageView view) {
//判断是否在主线程
Util.assertMainThread();
if (view == null) {
throw new IllegalArgumentException("You must pass in a non null View");
}
//如果imageView本身有填充方式,请求那么做相应的处理
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));

}

   然后通过传入view和转码类型(transcodeClass)来创建相应的Target。
ImageViewTargetFactory.java
    ```
public <Z> Target<Z> buildTarget(ImageView view, Class<Z> clazz) {
        if (GlideDrawable.class.isAssignableFrom(clazz)) {
            return (Target<Z>) new GlideDrawableImageViewTarget(view);
        } else if (Bitmap.class.equals(clazz)) {
            return (Target<Z>) new BitmapImageViewTarget(view);
        } else if (Drawable.class.isAssignableFrom(clazz)) {
            return (Target<Z>) new DrawableImageViewTarget(view);
        } else {
            throw new IllegalArgumentException("Unhandled class: " + clazz
                    + ", try .as*(Class).transcode(ResourceTranscoder)");
        }
    }
 OK,我们继续往下看,看下重载的方法into(Target)
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;
    }

Glide会先去看这个target之前有没有过请求,如果这个target之前有过请求要把这个请求clear掉,并且recycle。也就是说,如果一个imageView上我们要多次做加载请求,那么最后以最后一次请求为准。这个在listView或者是RecyclerView中使用就相当频繁了。
看到target.getRequest()方法,根据之前我们说的可能会生成的三种不同的target,我们这里去看BitmapImageViewTarget,最终的getRequest方法是在父类ViewTarget

public Request getRequest() {
        Object tag = getTag();
        Request request = null;
        if (tag != null) {
            if (tag instanceof Request) {
                request = (Request) tag;
            } else {
                throw new IllegalArgumentException("You must not call setTag() on a view Glide is targeting");
            }
        }
        return request;
    }
private Object getTag() {
        if (tagId == null) {
            return view.getTag();
        } else {
            return view.getTag(tagId);
        }
    }

所以很明显了,Glide玩的套路是把request对象通过setTag的方式和View绑定的

 继续看request是如何build出来的

  ```

private Request buildRequest(Target<TranscodeType> target) {
//如果实现没有优先级的规定,设置为优先级普通
if (priority == null) {
priority = Priority.NORMAL;
}
return buildRequestRecursive(target, null);
}
private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {
if (thumbnailRequestBuilder != null) {
//缩略图的相应的request创建
...
} else if (thumbSizeMultiplier != null) {
// Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse.
...
} else {
// Base case: no thumbnail.
return obtainRequest(target, sizeMultiplier, priority, parentCoordinator);
}
}
private Request obtainRequest(Target<TranscodeType> target, float sizeMultiplier, Priority priority,
RequestCoordinator requestCoordinator) {
return GenericRequest.obtain(
loadProvider,
model,
signature,
context,
priority,
target,
sizeMultiplier,
placeholderDrawable,
placeholderId,
errorPlaceholder,
errorId,
fallbackDrawable,
fallbackResource,
requestListener,
requestCoordinator,
glide.getEngine(),
transformation,
transcodeClass,
isCacheable,
animationFactory,
overrideWidth,
overrideHeight,
diskCacheStrategy);
}

   我们看到真正的生成请求方法`obtainRequest`,传入了大量的参数,我们不一一深究是什么东西,我们先接着看生成了请求之后的下一个方法,`  requestTracker.runRequest(request);`

public void runRequest(Request request) {
requests.add(request);
if (!isPaused) {
request.begin();
} else {
pendingRequests.add(request);
}
}

requestTracker内部维护了一个请求列表,那我们直接进入到request实现类,去看看begin方法到底做了什么。

@Override
public void begin() {
startTime = LogTime.getLogTime();
//就是之前提到的load传入类型,如果都没有加载类型就抛异常
if (model == null) {
onException(null);
return;
}

    status = Status.WAITING_FOR_SIZE;
    //如果已经拿到了尺寸就进入加载流程,否则继续View的尺寸
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        onSizeReady(overrideWidth, overrideHeight);
    } else {
        target.getSize(this);
    }
    //如果正在请求,那么就为view填充占位图
    if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
        target.onLoadStarted(getPlaceholderDrawable());
    }
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logV("finished run method in " + LogTime.getElapsedMillis(startTime));
    }

}

    终于我们找到最终的加载方法在这个onSizeReady回调中
@Override
public void onSizeReady(int width, int height) {
    //参数准备
    ...
    loadStatus = engine.load(signature, width, height, dataFetcher, loadProvider, transformation, transcoder,
            priority, isMemoryCacheable, diskCacheStrategy, this);
    loadedFromMemoryCache = resource != null;
    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logV("finished onSizeReady in " + LogTime.getElapsedMillis(startTime));
    }

}

     最终GenericRequest把加载任务都扔到了engin中去了

public <T, Z, R> LoadStatus load(Key signature, int width, int height, DataFetcher<T> fetcher,
DataLoadProvider<T, Z> loadProvider, Transformation<Z> transformation, ResourceTranscoder<Z, R> transcoder,
Priority priority, boolean isMemoryCacheable, DiskCacheStrategy diskCacheStrategy, ResourceCallback cb) {
Util.assertMainThread();
long startTime = LogTime.getLogTime();

    final String id = fetcher.getId();
    //创建每一次任务的加载的唯一标识key
    EngineKey key = keyFactory.buildKey(id, signature, width, height, loadProvider.getCacheDecoder(),
            loadProvider.getSourceDecoder(), transformation, loadProvider.getEncoder(),
            transcoder, loadProvider.getSourceEncoder());
    // 通过key查找内存缓存中是否存在引擎资源,如果有就直接可以拿来用,触发onResourceReady回调(一级缓存)
    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;
    }
    // 通过key查找是否存在弱引用可以利用(二级缓存)
    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;
    }
    //再没有就去本地的map列表中通过key查找是否存在EngineJob,这与上面的EngineResource不同(三级缓存)
    EngineJob current = jobs.get(key);
    if (current != null) {
        //加入内部的callback队列,最终也会执行如上的onResourceReady回调
        current.addCallback(cb);
        if (Log.isLoggable(TAG, Log.VERBOSE)) {
            logWithTimeAndKey("Added to existing load", startTime, key);
        }
        return new LoadStatus(cb, current);
    }
    //如果都没有,去创建一个EngineJob,去做加载请求
    EngineJob engineJob = engineJobFactory.build(key, isMemoryCacheable);
    DecodeJob<T, Z, R> decodeJob = new DecodeJob<T, Z, R>(key, width, height, fetcher, loadProvider, transformation,
            transcoder, diskCacheProvider, diskCacheStrategy, priority);
    EngineRunnable runnable = new EngineRunnable(engineJob, decodeJob, priority);
    jobs.put(key, engineJob);
    engineJob.addCallback(cb);
    //执行job
    engineJob.start(runnable);

    if (Log.isLoggable(TAG, Log.VERBOSE)) {
        logWithTimeAndKey("Started new load", startTime, key);
    }
    return new LoadStatus(cb, engineJob);
}
    这里涉及到多级缓存,以及最终的请求任务加载,我们打开`EngineRunnable`,来看下最终是怎么请求的。

  * ###藏得最深的DecodeJob
     EngineRunnable.java
```@Override
    public void run() {
        if (isCancelled) {
            return;
        }

        Exception exception = null;
        Resource<?> resource = null;
        try {
            resource = decode();
        } catch (Exception e) {
            if (Log.isLoggable(TAG, Log.VERBOSE)) {
                Log.v(TAG, "Exception decoding", e);
            }
            exception = e;
        }

        if (isCancelled) {
            if (resource != null) {
                resource.recycle();
            }
            return;
        }

        if (resource == null) {
            onLoadFailed(exception);
        } else {
            onLoadComplete(resource);
        }
    }
private Resource<?> decode() throws Exception {
        if (isDecodingFromCache()) {
            //缓存中已经有了我们需要的data
            return decodeFromCache();
        } else {
           //缓存中还没有我们需要的data,我们需要先去获得data,再将data转为我们需要的model类型
            return decodeFromSource();
        }
    }

两个方法,一个是缓存中处理过此类请求,直接从缓存中请求,我们直接看第二个decodeFromSource
decodeFromSource()->DecodeJob.decodeFromSource()->DecodeJob.decodeSource()->DecodeJob.decodeFromSourceData(),
最终,我们找到这一句

   //将data进行decode,变成我们需要的decoded类型
   decoded = loadProvider.getSourceDecoder().decode(data, width, height);

包括,走另一条decodeFromCache,也会走到这一句。经过多次的倒推与查找,我们发现在Glide的构造函数中,有着这么一坨代码

dataLoadProviderRegistry = new DataLoadProviderRegistry();

        StreamBitmapDataLoadProvider streamBitmapLoadProvider =
                new StreamBitmapDataLoadProvider(bitmapPool, decodeFormat);
        dataLoadProviderRegistry.register(InputStream.class, Bitmap.class, streamBitmapLoadProvider);

        FileDescriptorBitmapDataLoadProvider fileDescriptorLoadProvider =
                new FileDescriptorBitmapDataLoadProvider(bitmapPool, decodeFormat);
        dataLoadProviderRegistry.register(ParcelFileDescriptor.class, Bitmap.class, fileDescriptorLoadProvider);

        ImageVideoDataLoadProvider imageVideoDataLoadProvider =
                new ImageVideoDataLoadProvider(streamBitmapLoadProvider, fileDescriptorLoadProvider);
        dataLoadProviderRegistry.register(ImageVideoWrapper.class, Bitmap.class, imageVideoDataLoadProvider);

        GifDrawableLoadProvider gifDrawableLoadProvider =
                new GifDrawableLoadProvider(context, bitmapPool);
        dataLoadProviderRegistry.register(InputStream.class, GifDrawable.class, gifDrawableLoadProvider);

        dataLoadProviderRegistry.register(ImageVideoWrapper.class, GifBitmapWrapper.class,
                new ImageVideoGifDrawableLoadProvider(imageVideoDataLoadProvider, gifDrawableLoadProvider, bitmapPool));

        dataLoadProviderRegistry.register(InputStream.class, File.class, new StreamFileDataLoadProvider());

        register(File.class, ParcelFileDescriptor.class, new FileDescriptorFileLoader.Factory());
        register(File.class, InputStream.class, new StreamFileLoader.Factory());
        register(int.class, ParcelFileDescriptor.class, new FileDescriptorResourceLoader.Factory());
        register(int.class, InputStream.class, new StreamResourceLoader.Factory());
        register(Integer.class, ParcelFileDescriptor.class, new FileDescriptorResourceLoader.Factory());
        register(Integer.class, InputStream.class, new StreamResourceLoader.Factory());
        register(String.class, ParcelFileDescriptor.class, new FileDescriptorStringLoader.Factory());
        register(String.class, InputStream.class, new StreamStringLoader.Factory());
        register(Uri.class, ParcelFileDescriptor.class, new FileDescriptorUriLoader.Factory());
        register(Uri.class, InputStream.class, new StreamUriLoader.Factory());
        register(URL.class, InputStream.class, new StreamUrlLoader.Factory());
        register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());
        register(byte[].class, InputStream.class, new StreamByteArrayLoader.Factory());

其实Glide早就已经把基本所有的加载请求情况都已经考虑在内了。
dataLoadProviderRegistry注册的是将data转换成resource的情况
register方法注册是将model转换成data。

DecodeJob中的两个重要的类就是和上面的东西相关的

private final DataFetcher<A> fetcher;
    private final DataLoadProvider<A, T> loadProvider;

fetcher负责把model转换成data
loadProvider再负责把data转换成我们需要的资源类型resource。
在上面的decode流程中的decodeSource()方法我们能看到fetcher的调用,也只有在这个方法中我们可以看到fetcher的调用,因为只有缓存中没有现存的data,我们才会去做一次model转换成data。
追根溯源,你会发现,最终这个fetcher就是上面register方法中的XXXXXLoader.getResourceFetcher返回的DataFetcher对象,我们就取一个Http的请求看一下:

 register(GlideUrl.class, InputStream.class, new HttpUrlGlideUrlLoader.Factory());]

此处说明一下,之所以选择GlideUrl转换成输入流是因为,所有的http和https的网络url最后都会被转换成GlideUrl,具体原因见UriLoader.java

@Override
    public final DataFetcher<T> getResourceFetcher(Uri model, int width, int height) {
        ...
        if (isLocalUri(scheme)) {
            ...
        } else if (urlLoader != null && ("http".equals(scheme) || "https".equals(scheme))) {
            result = urlLoader.getResourceFetcher(new GlideUrl(model.toString()), width, height);
        }

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

网络请求就是在这里去执行的,使用的HttpUrlConnection。

  • 总结

    一次请求流程大致如下
    • 首先通过Glide.with方法生成RequestManager对象来管理请求
    • 调用RequestManager.load方法来讲我们的modelType传入,并得到相应的RequestBuilder对象。
    • 通过构造者模式不断去给RequestBuilder增加条件,比如裁剪,优先级,占位图等等
    • 通过into方法传入目的target,并开启请求
    • 查看目标View的tag中获取看看是否有request,如果有则清除。然后用新建的request来覆盖。
    • 执行request,三级缓存策略,先看缓存中是否存在EngineResource,再看是否有EngineResource的若引用,最后看Map中是否存在EngineJob。如果有则直接返回结果并进行相应加载
    • new并执行EngineRunnable这个DecoderJob的封装
    • 在DecoderJob内部查看是否存在相应的InputStream或者是ParcelFileDescriptor,如果已经存在,则直接将其通过loadProviderdecode成相应的Bitmap,gif等。否则就通过fetcher先将我们通过load传入的路径进行解析成InputStream、ParcelFileDescriptor,再decode。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容