Listview 局部item刷新的机制

对于一个有限性列表,最长不大于10,做flutter的同事选择了逐个控件生成,自行构建list的方式来实现。

I/flutter (29896): ************callApi complete:2019-04-08 15:26:58.294069
I/flutter (29896): ************ips buildView:2019-04-08 15:26:58.605058
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.606843
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.612228
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.614787
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.617380
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.620302
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.623194
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.625957
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.628533
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.631107
I/flutter (29896): ************IpsItem build:2019-04-08 15:26:58.633696

整个过程耗时倒是也不算长,但是其实更新的Item只☝️个,逐个生成明显不太必要。
所以回去看了看RecyclerView是怎么考虑的单个数据更新,应该用的还是比较多的

mAdapter.notifyItemChanged(pos);

位于RecyclerView.Adapter

public final void notifyItemChanged(int position) {
    mObservable.notifyItemRangeChanged(position, 1);
}

其中mObservable是AdapterDataObservable(继承自Observable)的对象

public void notifyItemRangeChanged(int positionStart, int itemCount) {
    notifyItemRangeChanged(positionStart, itemCount, null);
}

public void notifyItemRangeChanged(int positionStart, int itemCount,
        @Nullable Object payload) {
    // since onItemRangeChanged() is implemented by the app, it could do anything, including
    // removing itself from {@link mObservers} - and that could cause problems if
    // an iterator is used on the ArrayList {@link mObservers}.
    // to avoid such problems, just march thru the list in the reverse order.
    for (int i = mObservers.size() - 1; i >= 0; i--) {
        mObservers.get(i).onItemRangeChanged(positionStart, itemCount, payload);
    }
}

mObservers.get(i)来自Observable<AdapterDataObserver>,所以最终走的是AdapterDataObserver.onItemRangeChanged

private class RecyclerViewDataObserver extends AdapterDataObserver {
    RecyclerViewDataObserver() {
    }

    @Override
    public void onChanged() {
        assertNotInLayoutOrScroll(null);
        mState.mStructureChanged = true;

        processDataSetCompletelyChanged(true);
        if (!mAdapterHelper.hasPendingUpdates()) {
            requestLayout();
        }
    }

    @Override
    public void onItemRangeChanged(int positionStart, int itemCount, Object payload) {
        assertNotInLayoutOrScroll(null);
        if (mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)) {
            triggerUpdateProcessor();
        }
    }

    @Override
    public void onItemRangeInserted(int positionStart, int itemCount) {
        assertNotInLayoutOrScroll(null);
        if (mAdapterHelper.onItemRangeInserted(positionStart, itemCount)) {
            triggerUpdateProcessor();
        }
    }

    @Override
    public void onItemRangeRemoved(int positionStart, int itemCount) {
        assertNotInLayoutOrScroll(null);
        if (mAdapterHelper.onItemRangeRemoved(positionStart, itemCount)) {
            triggerUpdateProcessor();
        }
    }

    @Override
    public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
        assertNotInLayoutOrScroll(null);
        if (mAdapterHelper.onItemRangeMoved(fromPosition, toPosition, itemCount)) {
            triggerUpdateProcessor();
        }
    }

    void triggerUpdateProcessor() {
        if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
            ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
        } else {
            mAdapterUpdateDuringMeasure = true;
            requestLayout();
        }
    }
}

好,我们继续mAdapterHelper.onItemRangeChanged(positionStart, itemCount, payload)

/**
 * @return True if updates should be processed.
 */
boolean onItemRangeChanged(int positionStart, int itemCount, Object payload) {
    if (itemCount < 1) {
        return false;
    }
    mPendingUpdates.add(obtainUpdateOp(UpdateOp.UPDATE, positionStart, itemCount, payload));
    mExistingUpdateTypes |= UpdateOp.UPDATE;
    return mPendingUpdates.size() == 1;
}

在mPendingUpdates添加了个UpdateOp.UPDATE事件。
triggerUpdateProcessor

void triggerUpdateProcessor() {
    if (POST_UPDATES_ON_ANIMATION && mHasFixedSize && mIsAttached) {
        ViewCompat.postOnAnimation(RecyclerView.this, mUpdateChildViewsRunnable);
    } else {
        mAdapterUpdateDuringMeasure = true;
        requestLayout();
    }
}
static final boolean POST_UPDATES_ON_ANIMATION = Build.VERSION.SDK_INT >= 16;
由于我没有动过mHasFixedSize,所以它默认是false的。
public void setHasFixedSize(boolean hasFixedSize) {
    mHasFixedSize = hasFixedSize;
}
也就是说整个方法最后走的是else, 即mAdapterUpdateDuringMeasure置为true。

马上我们在onMeasure里就见到它了。

@Override
protected void onMeasure(int widthSpec, int heightSpec) {
    if (mLayout == null) {
        defaultOnMeasure(widthSpec, heightSpec);
        return;
    }
    if (mLayout.isAutoMeasureEnabled()) {
        final int widthMode = MeasureSpec.getMode(widthSpec);
        final int heightMode = MeasureSpec.getMode(heightSpec);

        /**
         * This specific call should be considered deprecated and replaced with
         * {@link #defaultOnMeasure(int, int)}. It can't actually be replaced as it could
         * break existing third party code but all documentation directs developers to not
         * override {@link LayoutManager#onMeasure(int, int)} when
         * {@link LayoutManager#isAutoMeasureEnabled()} returns true.
         */
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);

        final boolean measureSpecModeIsExactly =
                widthMode == MeasureSpec.EXACTLY && heightMode == MeasureSpec.EXACTLY;
        if (measureSpecModeIsExactly || mAdapter == null) {
            return;
        }

        if (mState.mLayoutStep == State.STEP_START) {
            dispatchLayoutStep1();
        }
        // set dimensions in 2nd step. Pre-layout should happen with old dimensions for
        // consistency
        mLayout.setMeasureSpecs(widthSpec, heightSpec);
        mState.mIsMeasuring = true;
        dispatchLayoutStep2();

        // now we can get the width and height from the children.
        mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);

        // if RecyclerView has non-exact width and height and if there is at least one child
        // which also has non-exact width & height, we have to re-measure.
        if (mLayout.shouldMeasureTwice()) {
            mLayout.setMeasureSpecs(
                    MeasureSpec.makeMeasureSpec(getMeasuredWidth(), MeasureSpec.EXACTLY),
                    MeasureSpec.makeMeasureSpec(getMeasuredHeight(), MeasureSpec.EXACTLY));
            mState.mIsMeasuring = true;
            dispatchLayoutStep2();
            // now we can get the width and height from the children.
            mLayout.setMeasuredDimensionFromChildren(widthSpec, heightSpec);
        }
    } else {
        if (mHasFixedSize) {
            mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
            return;
        }
        // custom onMeasure
        // mAdapterUpdateDuringMeasure 这货我们刚才整成true了
        if (mAdapterUpdateDuringMeasure) {
            startInterceptRequestLayout();
            onEnterLayoutOrScroll();
            processAdapterUpdatesAndSetAnimationFlags(); // 我们看看这个
            onExitLayoutOrScroll();

            if (mState.mRunPredictiveAnimations) {
                mState.mInPreLayout = true;
            } else {
                // consume remaining updates to provide a consistent state with the layout pass.
                mAdapterHelper.consumeUpdatesInOnePass();
                mState.mInPreLayout = false;
            }
            mAdapterUpdateDuringMeasure = false;
            stopInterceptRequestLayout(false);
        } else if (mState.mRunPredictiveAnimations) {
            // If mAdapterUpdateDuringMeasure is false and mRunPredictiveAnimations is true:
            // this means there is already an onMeasure() call performed to handle the pending
            // adapter change, two onMeasure() calls can happen if RV is a child of LinearLayout
            // with layout_width=MATCH_PARENT. RV cannot call LM.onMeasure() second time
            // because getViewForPosition() will crash when LM uses a child to measure.
            setMeasuredDimension(getMeasuredWidth(), getMeasuredHeight());
            return;
        }

        if (mAdapter != null) {
            mState.mItemCount = mAdapter.getItemCount();
        } else {
            mState.mItemCount = 0;
        }
        startInterceptRequestLayout();
        mLayout.onMeasure(mRecycler, mState, widthSpec, heightSpec);
        stopInterceptRequestLayout(false);
        mState.mInPreLayout = false; // clear
    }
}
private void processAdapterUpdatesAndSetAnimationFlags() {
    if (mDataSetHasChangedAfterLayout) {
        // Processing these items have no value since data set changed unexpectedly.
        // Instead, we just reset it.
        mAdapterHelper.reset();
        if (mDispatchItemsChangedEvent) {
            mLayout.onItemsChanged(this);
        }
    }
    // simple animations are a subset of advanced animations (which will cause a
    // pre-layout step)
    // If layout supports predictive animations, pre-process to decide if we want to run them
    if (predictiveItemAnimationsEnabled()) {
        mAdapterHelper.preProcess();
    } else {
        mAdapterHelper.consumeUpdatesInOnePass();  // 走了这里
    }
    boolean animationTypeSupported = mItemsAddedOrRemoved || mItemsChanged;
    mState.mRunSimpleAnimations = mFirstLayoutComplete
            && mItemAnimator != null
            && (mDataSetHasChangedAfterLayout
            || animationTypeSupported
            || mLayout.mRequestedSimpleAnimations)
            && (!mDataSetHasChangedAfterLayout
            || mAdapter.hasStableIds());
    mState.mRunPredictiveAnimations = mState.mRunSimpleAnimations
            && animationTypeSupported
            && !mDataSetHasChangedAfterLayout
            && predictiveItemAnimationsEnabled();
}
private boolean predictiveItemAnimationsEnabled() {
    return (mItemAnimator != null && mLayout.supportsPredictiveItemAnimations());
}
mItemAnimator 默认不为空 但是mLayout.supportsPredictiveItemAnimations()默认是false
ItemAnimator mItemAnimator = new DefaultItemAnimator();

/**
 * @return true if this LayoutManager supports predictive item animations, false otherwise.
 */
public boolean supportsPredictiveItemAnimations() {
    return false;
}

经上所述, 最后mAdapterHelper.consumeUpdatesInOnePass(), 记得咱们的事件是UpdateOp.UPDATE

AdapterHelper.java
/**
 * Skips pre-processing and applies all updates in one pass.
 */
void consumeUpdatesInOnePass() {
    // we still consume postponed updates (if there is) in case there was a pre-process call
    // w/o a matching consumePostponedUpdates.
    consumePostponedUpdates();
    final int count = mPendingUpdates.size();
    for (int i = 0; i < count; i++) {
        UpdateOp op = mPendingUpdates.get(i);
        switch (op.cmd) {
            case UpdateOp.ADD:
                mCallback.onDispatchSecondPass(op);
                mCallback.offsetPositionsForAdd(op.positionStart, op.itemCount);
                break;
            case UpdateOp.REMOVE:
                mCallback.onDispatchSecondPass(op);
                mCallback.offsetPositionsForRemovingInvisible(op.positionStart, op.itemCount);
                break;
            case UpdateOp.UPDATE:
                mCallback.onDispatchSecondPass(op);
                mCallback.markViewHoldersUpdated(op.positionStart, op.itemCount, op.payload);
                break;
            case UpdateOp.MOVE:
                mCallback.onDispatchSecondPass(op);
                mCallback.offsetPositionsForMove(op.positionStart, op.itemCount);
                break;
        }
        if (mOnItemProcessedCallback != null) {
            mOnItemProcessedCallback.run();
        }
    }
    recycleUpdateOpsAndClearList(mPendingUpdates);
    mExistingUpdateTypes = 0;
}

callback肯定在RecyclerView里面

@Override
public void onDispatchSecondPass(AdapterHelper.UpdateOp op) {
    dispatchUpdate(op);
}

void dispatchUpdate(AdapterHelper.UpdateOp op) {
    switch (op.cmd) {
        case AdapterHelper.UpdateOp.ADD:
            mLayout.onItemsAdded(RecyclerView.this, op.positionStart, op.itemCount);
            break;
        case AdapterHelper.UpdateOp.REMOVE:
            mLayout.onItemsRemoved(RecyclerView.this, op.positionStart, op.itemCount);
            break;
        case AdapterHelper.UpdateOp.UPDATE:
            mLayout.onItemsUpdated(RecyclerView.this, op.positionStart, op.itemCount,
                    op.payload);
            break;
        case AdapterHelper.UpdateOp.MOVE:
            mLayout.onItemsMoved(RecyclerView.this, op.positionStart, op.itemCount, 1);
            break;
    }
}

LayoutManager mLayout; layout 是layoutManager, onItemsUpdated也没复写好像没做什么。
@Override
public void markViewHoldersUpdated(int positionStart, int itemCount, Object payload) {
    viewRangeUpdate(positionStart, itemCount, payload);
    mItemsChanged = true;
}

/**
 * Rebind existing views for the given range, or create as needed.
 *
 * @param positionStart Adapter position to start at
 * @param itemCount Number of views that must explicitly be rebound
 */
void viewRangeUpdate(int positionStart, int itemCount, Object payload) {
    final int childCount = mChildHelper.getUnfilteredChildCount();
    final int positionEnd = positionStart + itemCount;

    for (int i = 0; i < childCount; i++) {
        final View child = mChildHelper.getUnfilteredChildAt(i);
        final ViewHolder holder = getChildViewHolderInt(child);
        if (holder == null || holder.shouldIgnore()) {
            continue;
        }
        if (holder.mPosition >= positionStart && holder.mPosition < positionEnd) {
            // We re-bind these view holders after pre-processing is complete so that
            // ViewHolders have their final positions assigned.
            holder.addFlags(ViewHolder.FLAG_UPDATE);
            holder.addChangePayload(payload);
            // lp cannot be null since we get ViewHolder from it.
            ((LayoutParams) child.getLayoutParams()).mInsetsDirty = true; // 是getItemDecorInsetsForChild里会用到的 
        }
    }
    mRecycler.viewRangeUpdate(positionStart, itemCount);
}

flag设置完以后就requestLayout了。关键点就是在哪里区分holder.addFlags(ViewHolder.FLAG_UPDATE);

@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    TraceCompat.beginSection(TRACE_ON_LAYOUT_TAG);
    dispatchLayout();
    TraceCompat.endSection();
    mFirstLayoutComplete = true;
}
void dispatchLayout() {
    if (mAdapter == null) {
        Log.e(TAG, "No adapter attached; skipping layout");
        // leave the state in START
        return;
    }
    if (mLayout == null) {
        Log.e(TAG, "No layout manager attached; skipping layout");
        // leave the state in START
        return;
    }
    mState.mIsMeasuring = false;
    if (mState.mLayoutStep == State.STEP_START) {
        dispatchLayoutStep1();
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else if (mAdapterHelper.hasUpdates() || mLayout.getWidth() != getWidth()
            || mLayout.getHeight() != getHeight()) {
        // First 2 steps are done in onMeasure but looks like we have to run again due to
        // changed size.
        mLayout.setExactMeasureSpecsFrom(this);
        dispatchLayoutStep2();
    } else {
        // always make sure we sync them (to ensure mode is exact)
        mLayout.setExactMeasureSpecsFrom(this);
    }
    dispatchLayoutStep3();
}

最正常的时候,刷新走的路径如下:

onMeasure 
Element 4 set.
onLayout 
onDraw 
onDraw

偶尔不正常的时候,比如第一次调用,刷新走的路径会是两个Element被刷新了,我还没太细看,先记着:

onMeasure 
Element 1 set.
Element 5 set.
onLayout 
onDraw 
onDraw

还是从log和stack来看,这样比较容易一把看完整个流程

@Override
public void onBindViewHolder(ViewHolder viewHolder, final int position) {
    Log.d(TAG, "Element " + position + " set.", new RuntimeException());
}
RecyclerView
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
    super.onLayout(changed, l, t, r, b);
    Log.d(TAG, "onLayout ");
}

onBindViewHolder其实是super.onLayout调用的。

at android.support.v7.widget.RecyclerView$Adapter.onBindViewHolder(RecyclerView.java:6673)
    at android.support.v7.widget.RecyclerView$Adapter.bindViewHolder(RecyclerView.java:6714)
    at android.support.v7.widget.RecyclerView$Recycler.tryBindViewHolderByDeadline(RecyclerView.java:5647)
    at android.support.v7.widget.RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5913)
    at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5752)
    at android.support.v7.widget.RecyclerView$Recycler.getViewForPosition(RecyclerView.java:5748)
    at android.support.v7.widget.LinearLayoutManager$LayoutState.next(LinearLayoutManager.java:2232)
    at android.support.v7.widget.LinearLayoutManager.layoutChunk(LinearLayoutManager.java:1559)
    at android.support.v7.widget.LinearLayoutManager.fill(LinearLayoutManager.java:1519)
    at android.support.v7.widget.LinearLayoutManager.onLayoutChildren(LinearLayoutManager.java:614)
    at android.support.v7.widget.RecyclerView.dispatchLayoutStep2(RecyclerView.java:3812)
    at android.support.v7.widget.RecyclerView.dispatchLayout(RecyclerView.java:3529)
    at android.support.v7.widget.RecyclerView.onLayout(RecyclerView.java:4082)
    at com.mango.irene.recyclerviewdemo.MyRecyclerView.onLayout(MyRecyclerView.java:35)
    at android.view.View.layout(View.java:17702)
    at android.view.ViewGroup.layout(ViewGroup.java:5577)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1741)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1585)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1494)
    at android.view.View.layout(View.java:17702)
    at android.view.ViewGroup.layout(ViewGroup.java:5577)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
    at android.view.View.layout(View.java:17702)
    at android.view.ViewGroup.layout(ViewGroup.java:5577)
    at android.support.v7.widget.ActionBarOverlayLayout.onLayout(ActionBarOverlayLayout.java:443)
    at android.view.View.layout(View.java:17702)
    at android.view.ViewGroup.layout(ViewGroup.java:5577)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
    at android.view.View.layout(View.java:17702)
    at android.view.ViewGroup.layout(ViewGroup.java:5577)
    at android.widget.LinearLayout.setChildFrame(LinearLayout.java:1741)
    at android.widget.LinearLayout.layoutVertical(LinearLayout.java:1585)
    at android.widget.LinearLayout.onLayout(LinearLayout.java:1494)
    at android.view.View.layout(View.java:17702)
    at android.view.ViewGroup.layout(ViewGroup.java:5577)
    at android.widget.FrameLayout.layoutChildren(FrameLayout.java:323)
    at android.widget.FrameLayout.onLayout(FrameLayout.java:261)
    at com.android.internal.policy.DecorView.onLayout(DecorView.java:729)
    at android.view.View.layout(View.java:17702)
    at android.view.ViewGroup.layout(ViewGroup.java:5577)
    at android.view.ViewRootImpl.performLayout(ViewRootImpl.java:2426)
    at android.view.ViewRootImpl.performTraversals(ViewRootImpl.java:2148)
    at android.view.ViewRootImpl.doTraversal(ViewRootImpl.java:1334)
    at android.view.ViewRootImpl$TraversalRunnable.run(ViewRootImpl.java:6499)
    at android.view.Choreographer$CallbackRecord.run(Choreographer.java:934)
    at android.view.Choreographer.doCallbacks(Choreographer.java:746)
    at android.view.Choreographer.doFrame(Choreographer.java:677)
    at android.view.Choreographer$FrameDisplayEventReceiver.run(Choreographer.java:920)
    at android.os.Handler.handleCallback(Handler.java:754)
    at android.os.Handler.dispatchMessage(Handler.java:95)
    at android.os.Looper.loop(Looper.java:163)
    at android.app.ActivityThread.main(ActivityThread.java:6401)

真正用到ViewHolder.FLAG_UPDATE是在RecyclerView$Recycler.tryGetViewHolderForPositionByDeadline(RecyclerView.java:5913)

boolean bound = false;
if (mState.isPreLayout() && holder.isBound()) {
    // do not update unless we absolutely have to.
    holder.mPreLayoutPosition = position;
} else if (!holder.isBound() || holder.needsUpdate() || holder.isInvalid()) {
    if (DEBUG && holder.isRemoved()) {
        throw new IllegalStateException("Removed holder should be bound and it should"
                + " come here only in pre-layout. Holder: " + holder
                + exceptionLabel());
    }
    final int offsetPosition = mAdapterHelper.findPositionOffset(position);
    bound = tryBindViewHolderByDeadline(holder, offsetPosition, position, deadlineNs);
}

这么多年也没怎么写过流程,这逻辑还是有点混乱。
不过初略上看明白应该问题不大,继续努力

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

推荐阅读更多精彩内容