Glide遇坑记---Glide与CircleImageView (一)

Glide遇坑记之背景

Glide是一个专注于平滑滚动和图片加载的缓存库。Glide提供了易用的API,高性能、可扩展的图片解码管道,以及自动的资源池技术。

在现如今有着众多的图片加载库Android-Universal-ImageLoaderGlideFrescoPicasso的背景之下,Glide依旧屹立不倒在于:

  • Glide不仅是一个图片缓存,它支持 Gif、缩略图,甚至是 Video。并支持拉取,解码和展示视频快照,所以更该当做一个媒体缓存。
  • 与Activity/Fragment生命周期一致,支持trimMemory。Glide 对每个 context 都保持一个 RequestManager,通过 FragmentTransaction 保持与 Activity/Fragment 生命周期一致,并且有对应的 trimMemory 接口实现可供调用。
  • 友好的内存缓存设计。Glide从内存中读取数据时,不像一般的实现用 get,而是用 remove,再将这个缓存数据放到一个 value 为软引用的 activeResources map 中,并计数引用数,在图片加载完成后进行判断,如果引用计数为空则回收掉。
  • Glide的Bitmap格式默认使用 RGB_565 ,虽然清晰度稍差,但内存开销要比ARGB_8888格式小一半。
  • Glide易用的API,使开发者可以插入和替换成自己喜爱的任何网络栈。默认情况下,Glide使用的是一个定制化的基于HttpUrlConnection的栈,但同时也提供了与Google Volley和Square OkHttp快速集成的工具库。

API

Glide 使用简明的流式语法API,允许你在大部分情况下一行代码搞定需求

Glide.with(context).load(url).into(imageView);

性能

Glide 充分考虑了Android图片加载性能的两个关键方面

  • 图片解码速度
  • 解码图片带来的资源压力

为了让用户拥有良好的App使用体验,图片不仅要快速加载,而且还不能因为过多的主线程I/O或频繁的垃圾回收导致页面的闪烁和抖动现象。Glide使用了多个步骤来确保在Android上加载图片尽可能的快速和平滑:

  • 自动、智能地下采样(downsampling)和缓存(caching),以最小化存储开销和 解码次数;
  • 积极的资源重用,例如字节数组和Bitmap,以最小化昂贵的垃圾回收和堆碎片影响;
  • 深度的生命周期集成,以确保仅优先处理活跃的Fragment和Activity的请求,并有利于应用在必要时释放资源以避免在后台时被杀掉。

Glide遇坑记之阐述

在使用Glide中加载图片的过程中,第一次只显示占位符placeHolder,但之后从内存或磁盘中加载图片则可正常显示。问题仅会出现在使用CircleImageView与placeHolder,且在进行网络图片加载的前提之下。

对于此问题有以下解决方法

  • 不设置占位符placeHolder
  • 使用dontAnimation方法禁止认动画
  • 抛弃CircleImageView,使用ImageView
  • 使用GlideTransformation API自定义圆形图片

Glide遇坑记之分析

通过上面的问题阐述和解决方法我们可以进行推测,问题与Glide的占位符、默认动画、缓存机制,并与CircleImageView存在一定的关联。为了探究问题出现在何方,我们先从Glide的源码入手,尝试探究图片加载具体实现逻辑,来查明问题所在。

Glide.with(context).load(url).into(imageView);

Glide源码分析(一)

  • Glide.with(context)
public static RequestManager with(Context context) {
    RequestManagerRetriever retriever = RequestManagerRetriever.get();
    return retriever.get(context);
}

Glide有四个静态的重载方法with(),其内部都是先调用RequestManagerRetriever的静态 get()方法获取一个RequestManagerRetriever对象。再根据传入with()方法的参数调用RequestManagerRetriever的实例get()方法,获取RequestManager对象。

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

不管你在Glide.with()方法中传入的是ActivityFragmentActivityv4包下的Fragment、还是app包下的Fragment,最终的流程都是,通过向当前的Activity当中添加一个隐藏的Fragment,将Glide的加载请求与Activity/Fragment的生命周期绑定而自动执行请求,暂停操作。

  • RequestManager.load(url)
public DrawableTypeRequest<String> load(String string) {
    return (DrawableTypeRequest<String>) fromString().load(string);
}

先调用fromString()方法,再调用load()方法,然后把传入的图片URL地址传进去。而fromString()方法也仅是调用了loadGeneric()方法,并且指定参数为String.class,因为load()方法参数为字符串。

private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass) {
    ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
    ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =Glide.buildFileDescriptorModelLoader(modelClass, context);
    if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
        throw new IllegalArgumentException("");
    }

    return optionsApplier.apply(new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context, glide, requestTracker, lifecycle, optionsApplier));
}

loadGeneric()方法返回一个DrawableTypeRequest对象,在loadGeneric()方法中利用获得的ModelLoader对象和其他参数new了一个DrawableTypeRequest对象,然后调用DrawableTypeRequestBuilder的load方法。load()方法的具体逻辑都是在DrawableRequestBuilder的父类GenericRequestBuilder当中,load()方法仅是对一些简单变量进行相应的赋值操作。DrawableRequestBuilder中有多个方法,这些方法就是Glide绝大多数的API了。

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

into()方法的具体逻辑都是在DrawableRequestBuilder的父类GenericRequestBuilder当中

public Target<TranscodeType> into(ImageView view) {
    Util.assertMainThread();
    if (!isTransformationSet && view.getScaleType() != null) {
        switch (view.getScaleType()) {
            case CENTER_CROP:
                applyCenterCrop();
                break;
            case FIT_CENTER:
            case FIT_START:
            case FIT_END:
                applyFitCenter();
                break;
            default:
        }
    }

    return into(glide.buildImageViewTarget(view, transcodeClass));
}

GenericRequestBuilder提供多个重载的into()方法,ImageView参数、Target参数、int参数。最后一行代码先是调用了glide.buildImageViewTarget()方法,这个方法会构建出一个Target对象,Target对象则是用来最终展示图片用的。

<R> Target<R> buildImageViewTarget(ImageView imageView, Class<R> transcodedClass) {
    return imageViewTargetFactory.buildTarget(imageView, transcodedClass);
}

调用了ImageViewTargetFactorybuildTarget()方法

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

buildTarget()方法中会根据传入的class参数来构建不同的Target对象。默认情况下DrawableTypeRequest在构造函数调用super()方法中向父类中的transcodeClass变量传递GlideDrawable.class参数,buildTarget()方法利用ImageView构建的GlideDrawableImageViewTarget对象。如果你在链式调用之中使用asBitmap()方法,BitmapTypeRequest则在构造函数中向父类传递Bitmap.class参数,那么buildTarget()方法则利用ImageView构建出BitmapImageViewTarget对象。通过 glide.buildImageViewTarget()方法,我们构建出了一个GlideDrawableImageViewTarget对象。回到into()方法的最后一行,可以看到,这里又将Target对象传入到了GenericRequestBuilder另一个接收Target对象的into()方法当中。

public <Y extends Target<TranscodeType>> Y into(Y target) {
    Util.assertMainThread();

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

这个重载的into()方法利用传入的Target对象和buildRequest()方法来构建Request对象。buildRequest()方法内部通过调用buildRequestRecursive来构建一个GenericRequest对象。在通过buildRequest创建请求成功后,使用了target.setRequest(request)将请求设置到target,并通过addListener()方法将Target加入到Lifecycle。上面执行了那么多都只是请求创建,请求的执行时通过requestTracker.runRequest(request)开始的。

private Request buildRequestRecursive(Target<TranscodeType> target, ThumbnailRequestCoordinator parentCoordinator) {
    if (thumbnailRequestBuilder != null) {
        ThumbnailRequestCoordinator coordinator = new ThumbnailRequestCoordinator(parentCoordinator);
        Request fullRequest = obtainRequest(target, sizeMultiplier, priority, coordinator);     
        Request thumbRequest = thumbnailRequestBuilder.buildRequestRecursive(target, coordinator);
        coordinator.setRequests(fullRequest, thumbRequest);
        return coordinator;
    } else if (thumbSizeMultiplier != null) {
        // Base case: thumbnail multiplier generates a thumbnail request, but cannot recurse.
        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);
    }
}

buildRequestRecursive()方法中大部分代码都是在处理缩略图buildRequestRecursive()中调用obtainRequest()方法来获取一个发送图片加载请求的GenericRequest对象。

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

obtain()方法需要传入非常多的参数,而其中很多的参数我们都是比较熟悉的,像placeholderIderrorPlaceholderdiskCacheStrategy等。load()方法中调用的所有API,在这里组装到Request对象当中。

obtainRequest()方法内部调用GenericRequestobtain()方法,GenericRequestobtain()方法实际上获得的就是一个GenericRequest对象。另外在GenericRequestobtain()方法内部又调用了GenericRequestinit()方法,init()方法里面主要就是一些赋值的代码,将传入的这些参数赋值到GenericRequest的成员变量当中。

现在解决了构建Request对象的问题,接下来我们看一下这个Request对象又是怎么执行的。其实是在into()方法中通过调用requestTracker.runRequest()方法来去执行这个Request

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

在不为暂停状态下调用Requestbegin()方法来执行Request开始执行请求,否则的话就先将Request添加到待执行队列里面,等暂停状态解除之后再执行。

@Override
public void begin() {
    startTime = LogTime.getLogTime();
    if (model == null) {
        onException(null);
        return;
    }

    status = Status.WAITING_FOR_SIZE;
    if (Util.isValidDimensions(overrideWidth, overrideHeight)) {
        onSizeReady(overrideWidth, overrideHeight);
    } else {
        target.getSize(this);
    }

    if (!isComplete() && !isFailed() && canNotifyStatusChanged()) {
        target.onLoadStarted(getPlaceholderDrawable());
    }
}

begin()方法中进行具体图片加载操作。这里要分两种情况,一种是你使用了override() API为图片指定了一个固定的宽高,一种是没有指定。如果指定了的话,就会调用onSizeReady()方法。如果没指定的话,就会用target.getSize()方法。这个target.getSize()方法的内部会根据ImageViewlayout_widthlayout_height值做一系列的计算,来算出图片应该的宽高,在计算完成之后它也会调用onSizeReady()方法。也就是说,不管是哪种情况,最终都会调用到onSizeReady()方法,在这里通过Engine及相关类完成网络请求和图片获取之后,最终会回调至GenericRequestonResourceReady()方法之中。篇幅有限,网络请求部分不为重点,对此不再进行分析。

@Override
public void onLoadStarted(Drawable placeholder) {
    view.setImageDrawable(placeholder);
}

在发起图片加载请求之后,通过!isComplete() && !isFailed() && canNotifyStatusChanged()对Glide请求状态进行判断,若加载请求正常进行则调用targetonLoadStarted方法,从而在加载前显示占位图,当然设置加载错误图片占位图的原理也是一样,只不过回调执行时机不同。

以上,我们对Glide的源码从 with()load()、再是into()有了一个大致的了解,下面我们将带着问题继续探究在获取到图片之后Glide是如何通过onResourceReady()方法显示到ImageView之中。

Glide源码分析(二)

通过问题分析及对Glide源码的解读,占位符、默认动画、缓存机制都对animate()方法在不同方面产生影响。

public final DrawableRequestBuilder<ModelType> crossFade() {
    super.animate(new DrawableCrossFadeFactory<GlideDrawable>());
    return this;
}

DrawableRequestBuilder在构造函数中默认调用crossFade()函数,开启渐显动画效果。crossFade()方法的具体实现逻辑都是在DrawableRequestBuilder的父类GenericRequestBuilder的实例方法animate()当中。

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

crossFade()方法主要是通过调用animation()animationFactory变量进行赋值操作。赋值之后animationFactory变量含有DrawableCrossFadeFactory对象的引用。

public GenericRequestBuilder<ModelType, DataType, ResourceType, TranscodeType> dontAnimate() {
    GlideAnimationFactory<TranscodeType> animation = NoAnimation.getFactory();
    return animate(animation);
}

dontAnimate()方法内部通过调用获取一个对象,并传入,animationFactory变量则会指向NoAnimationFactory对象。


回到GenericRequest.onResourceReady()方法。

private void onResourceReady(Resource<?> resource, R result) {
    boolean isFirstResource = isFirstReadyResource();
    status = Status.COMPLETE;
    this.resource = resource;

    if (requestListener == null || !requestListener.onResourceReady(result, model, target,loadedFromMemoryCache,isFirstResource)) {
        GlideAnimation<R> animation = animationFactory.build(loadedFromMemoryCache, isFirstResource);
        target.onResourceReady(result, animation);
    }

    notifyLoadSuccess();
}

onResourceReady()方法中先是调用resource.get()方法获取封装图片的GlideDrawable对象,接着调用另一重载的onResourceReady()方法之中的animationFactory.build()方法并传入是否来自内存缓存是否首次加载两个布尔值来获取GlideAnimation对象。

public static class NoAnimationFactory<R> implements GlideAnimationFactory<R> {

    public GlideAnimation<R> build(boolean isFromMemoryCache, boolean isFirstResource) {
        return (GlideAnimation<R>) NO_ANIMATION;
    }
}

NoAnimationFactory对象简单粗暴的通过animationFactory.build()方法直接返回一个NoAnimation对象实例。

public GlideAnimation<T> build(boolean isFromMemoryCache, boolean isFirstResource) {
    if (isFromMemoryCache) {
        return NoAnimation.get();
    } else if (isFirstResource) {
        return getFirstResourceAnimation();
    } else {
        return getSecondResourceAnimation();
    }
}

DrawableCrossFadeFactory对象的build()方法内部则根据布尔值调用相应函数,并返回对应的GlideAnimation实例。从内存中获取已缓存图片,调用NoAnimation.get()方法获取NoAnimation对象实例;从网络中首次获取图片,调用getFirstResourceAnimation()方法创建并返回一个DrawableCrossFadeViewAnimation对象实例;从网络中获取已加载图片,调用getSecondResourceAnimation()方法创建并返回一个DrawableCrossFadeViewAnimation对象实例

private GlideAnimation<T> getFirstResourceAnimation() {
    if (firstResourceAnimation == null) {
        GlideAnimation<T> defaultAnimation = animationFactory.build(false /*isFromMemoryCache*/, true /*isFirstResource*/);
        firstResourceAnimation = new DrawableCrossFadeViewAnimation<T>(defaultAnimation, duration);
    }
    return firstResourceAnimation;
}

getFirstResourceAnimation()方法内部先是调用animationFactory.build()获取一个defaultAnimation对象。这里的animationFactoryDrawableCrossFadeFactory的构造函数中进行赋值,是一个ViewAnimationFactory对象实例,然后将defaultAnimation传入构造函数获取DrawableCrossFadeViewAnimation对象实例。

getSecondResourceAnimation()getFirstResourceAnimation()方法仅是传入不同的参数从而从animationFactory.build()方法中获取对应的对象实例。getSecondResourceAnimation()方法通过调用animationFactory.build()获取一个NoAnimation对象。

public GlideAnimation<R> build(boolean isFromMemoryCache, boolean isFirstResource) {
    if (isFromMemoryCache || !isFirstResource) {
        return NoAnimation.get();
    }

    if (glideAnimation == null) {
        glideAnimation = new ViewAnimation<R>(animationFactory);
    }

    return glideAnimation;
}

ViewAnimationFactorybuild()方法与DrawableCrossFadeFactorybuild()方法类似,同为获取GlideAnimation对象实例,DrawableCrossFadeFactory返回的对象在特定情况下(启用默认动画且未设置占位符),其实是利用ViewAnimationFactory.build()方法所返回的ViewAnimationNoAnimation对象实例来展示图片并完成动画效果。

若图片来自内存或不为首次加载则返回NoAnimation对象,其他情况下直接返回ViewAnimation对象。然后将对应对象实例传入DefaultAnimationFactory对象构造函数。


回到GenericRequest.onResourceReady()方法,继续执行,接着调用target.onResourceReady()方法并传入GlideDrawableGlideAnimation对象。这个target又是什么呢?在into()方法的最后一行,调用glide.buildImageViewTarget()方法来构建出一个target,而这个target就是一个GlideDrawableImageViewTarget对象。

@Override
public void onResourceReady(GlideDrawable resource, GlideAnimation<? super GlideDrawable> animation) {
    if (!resource.isAnimated()) {
        if (Math.abs(viewRatio - 1f) <= SQUARE_RATIO_MARGIN
                    && Math.abs(drawableRatio - 1f) <= SQUARE_RATIO_MARGIN) {
                resource = new SquaringDrawable(resource, view.getWidth());
            }
    }
    super.onResourceReady(resource, animation);
    this.resource = resource;
    resource.setLoopCount(maxLoopCount);
    resource.start();
}

GlideDrawableImageViewTargetonResourceReady()方法中做了一些逻辑处理,包括如果是GIF图片的话,就调用resource.start()方法开始播放图片。通过调用super.onResourceReady()方法,将GlideDrawable显示到ImageView上,GlideDrawableImageViewTarget的父类是ImageViewTarget

@Override
public void onResourceReady(Z resource, GlideAnimation<? super Z> glideAnimation) {
    if (glideAnimation == null || !glideAnimation.animate(resource, this)) {
        setResource(resource);
    }
}

onResourceReady()方法通过对glideAnimation进行空值判断和对glideAnimation.animate()返回值进行分析,来决定是否执行setResource()方法。ImageViewTargetsetResource()方法是一个抽象方法,具体执行逻辑由GlideDrawableImageViewTarget实现。

protected void setResource(GlideDrawable resource) {
    view.setImageDrawable(resource);
}

调用view.setImageDrawable()方法,将GlideDrawable显示到ImageView上。这个view就是在调用ImageViewTargetFactorybuildTarget()方法,对GlideDrawableImageViewTarget进行实例化时传入的ImageView

setResource()方法的调用条件是glideAnimation为空或glideAnimation.animate()方法返回 false

根据animationFactory引用工厂对象的不同,onResourceReady()方法可能传入DrawableCrossFadeViewAnimationNoAnimation对象,这就是glideAnimation所对应对象实例。

DrawableCrossFadeViewAnimationNoAnimation两者也人如其名,差异主要体现在animate()方法之上。

@Override
public boolean animate(Object current, ViewAdapter adapter) {
    return false;
}

NoAnimationanimate()方法直接返回false,未实现任何动画效果。恰好符合setResource()方法调用条件,通过view.setImageDrawable()显示图片。

@Override
public boolean animate(T current, ViewAdapter adapter) {
    Drawable previous = adapter.getCurrentDrawable();
    if (previous != null) {
        TransitionDrawable transitionDrawable = new TransitionDrawable(new Drawable[] { previous, current });
        transitionDrawable.setCrossFadeEnabled(true);
        transitionDrawable.startTransition(duration);
        adapter.setDrawable(transitionDrawable);
        return true;
    } else {
        defaultAnimation.animate(current, adapter);
        return false;
    }
}

DrawableCrossFadeViewAnimation.animate()方法内部先是获取GlideDrawableImageViewTarget的占位符previous。如果previous不为空,即先前通过placeHolder()设置占位符,则通过TransitionDrawable设置动画并添加图片至ImageView。否则通过defaultAnimation展示图片。

这里的defaultAnimation是一个ViewAnimationNoAnimation对象。由ViewAnimationFactorybuild()方法创建并返回。不知道快去回到👆看看getFirstResourceAnimation()方法的分析。

public boolean animate(R current, ViewAdapter adapter) {
    View view = adapter.getView();
    if (view != null) {
        view.clearAnimation();
        Animation animation = animationFactory.build();
        view.startAnimation(animation);
    }

    return false;
}

ViewAnimation.animate()方法内部通过view.startAnimation(animation)实现动画效果,并返回false。这里的animationFactoryDrawableCrossFadeFactory的静态内部类DefaultAnimationFactoryanimationFactory.build()方法内部仅创建并返回一个AlphaAnimation对象实例。并调用view.startAnimation()执行AlphaAnimation对应的淡入淡出动画。

NoAnimation则只是简单的返回false。在previous为空的情况下,无论调用ViewAnimation还是NoAnimationanimate()方法,图片加载都将交给view.setImageDrawable()执行。

分析至此,结合问题及Glide源码,占位符、默认动画、缓存机制的共性在于都对TransitionDrawable.animate()方法在不同方面产生影响。一张图片胜过千言万语,快来通过👇这张图来梳理下Glide相关问题得以解决的原因。

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