View的工作原理二(requestLayout,invalidate)

View的工作原理(一)我们知道,Activity.setContentView()就是用来初始化View树,确定界面主题,样式,确定View的层次结构,以及添加我们自定义的布局到View树的。在这个过程中,系统添加内置布局到DecorView,添加我们的自定义布局到contentParent,都是调用的addView(),包括我们平时用一个ViewGroup添加子view的时候也是用这个。那么addView()这个操作到底做了什么呢,为什么添加完了,就能在界面看到添加进去的子View?我们就来看看addView()的源码,看看到底做了什么操作.

public void addView(View child) {
    addView(child, -1);
}

public void addView(View child, int index) {
    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }
    LayoutParams params = child.getLayoutParams();
    if (params == null) {
        params = generateDefaultLayoutParams();
        if (params == null) {
            throw new IllegalArgumentException("generateDefaultLayoutParams() cannot return null");
        }
    }
    addView(child, index, params);
}

//最终都是调用三个参数的
public void addView(View child, int index, LayoutParams params) {
    if (DBG) {
        System.out.println(this + " addView");
    }

    if (child == null) {
        throw new IllegalArgumentException("Cannot add a null child view to a ViewGroup");
    }

    // addViewInner() will call child.requestLayout() when setting the new LayoutParams
    // therefore, we call requestLayout() on ourselves before, so that the child's request
    // will be blocked at our level
    // addViewInner()将调用child.requestLayout() ,当我们设置了新的布局参数的时候。因此
   //我们在这之前调用了requestLayout(),以便child的request在我们这个层级被阻止
    requestLayout();
    invalidate(true);
    addViewInner(child, index, params, false);
}

可以看到addView()最终做了两个重要的操作,也就是本篇要学习的重点:View刷新UI的两个非常重要的方法 requestLayout()和 invalidate()

View.requestLayout()

requestLayout()是View中的方法:

/**
 * Call this when something has changed which has invalidated the
 * layout of this view. This will schedule a layout pass of the view
 * tree. This should not be called while the view hierarchy is currently in a layout
 * pass ({@link #isInLayout()}. If layout is happening, the request may be honored at the
 * end of the current layout pass (and then layout will run again) or after the current
 * frame is drawn and the next layout occurs.
 *
 * <p>Subclasses which override this method should call the superclass method to
 * handle possible request-during-layout errors correctly.</p>
 */
//注释翻译:view的布局要改变的时候调用.调用这个方法将会使整个View树重新布局.
//如果View树正在布局过程中,这个布局请求会在布局过程结束或者当前帧绘制完成并且下一个布局发生时再执行
@CallSuper
public void requestLayout() {
    //mMeasureCache 是LongSparseLongArray类型(比HashMap更高效的容器)
    if (mMeasureCache != null) mMeasureCache.clear();
   //mViewRequestingLayout 是View类型的变量,用于跟踪记录哪个View发起requestLayout()
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == null) {
        // Only trigger request-during-layout logic if this is the view requesting it,
        // not the views in its parent hierarchy
        ViewRootImpl viewRoot = getViewRootImpl();
        //viweRoot不为null并且view树正在进行布局
        if (viewRoot != null && viewRoot.isInLayout()) {
           //如果不处理view的布局请求
            if (!viewRoot.requestLayoutDuringLayout(this)) {
                return;
            }
        }
        //记录发起布局请求的view
        mAttachInfo.mViewRequestingLayout = this;
    }
    //给当前view添加布局标记和重绘标记
    mPrivateFlags |= PFLAG_FORCE_LAYOUT;
    mPrivateFlags |= PFLAG_INVALIDATED;
    
    if (mParent != null && !mParent.isLayoutRequested()) {
      //调用父控件的requestLayout
        mParent.requestLayout();
    }
  //如果发起布局请求View的是当前view,则置空
    if (mAttachInfo != null && mAttachInfo.mViewRequestingLayout == this) {
        mAttachInfo.mViewRequestingLayout = null;
    }
}

可以看到requestLayout()方法做的操作就是:
1,判断view树是否在进行布局,如果是,那么当前view发起的布局请求不处理,直接return
2,给当前view添加布局标记和重绘标记
3,如果当前view的父View不为null,就调用父View的requestLayout()
4,如果发起布局请求的是当前view,置空发起变量

这里有个关键点:子View发起布局请求会调用父view的requestLayout(),而父View也就是ViewGroup,ViewGroup并没有复写View的requestLayout(),调用的requestLayout()还是View的requestLayout()。这样父View就也会调用它的父View的requestLayout(),层层往上传递,直到View树的顶层View,也就是DecorView。说到这里,有个问题:View怎么确定他的父View是谁呢,或者说View的mParent,也就是父View在哪里赋值的呢?我们就看一下View的mParent赋值的地方:

void assignParent(ViewParent parent) {
    if (mParent == null) {
        mParent = parent;
    } else if (parent == null) {
        mParent = null;
    } else {
        throw new RuntimeException("view " + this + " being added, but"
                + " it already has a parent");
    }
}

这个函数就是为mParent方法赋值的,它是在View里面定义的,而它又被ViewGroup的addViewInner()调用,通过前面addView()的源码我们知道,addViewInner又是在addView()被调用,所以,我们只要调用了ViewGroup的addView(),就会去调用assignParent()绑定一个View和它的父View。

但是我们看到它的参数类型不是ViewGroup,而是ViewParent。前面我们说View的父View不是ViewGroup吗,难道说错了?其实没有错,ViewParent是一个接口,ViewGroup就实现了这个接口,而且除了ViewGroup实现了它以外,还有一个类也实现了这个接口:ViewRootImpl。通过这种继承关系,我们可以有个猜想:只要是实现了ViewParent的类就是父View,ViewRootImpl肯定也是父View.

其实ViewRootImpl就是DecorView的mParent,但由于ViewRootImpl不是一个View,所以我们在View里面找不到给DecorView的mParent赋值的地方。指定ViewRootImpl为DecorView的mParent的操作是在Activity启动之后,ActivityThread 会调用 WindowManager#addView(),而这个 addView() 最终其实是调用了 WindowManagerGlobal 的 addView() 方法。由于Activity的启动过程巨复杂,涉及的知识点太多,就不展开看具体源码了,这里直接跳到WindowManagerGlobal 的 addView() :

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {

    ..........省略部分代码

    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {

       ........省略

        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);
    }

    // do this last because it fires off messages to start doing things
    try {
        //第一个参数view就是DecorView
        root.setView(view, wparams, panelParentView);
    } catch (RuntimeException e) {
        // BadTokenException or InvalidDisplayException, clean up.
        synchronized (mLock) {
            final int index = findViewLocked(view, false);
            if (index >= 0) {
                removeViewLocked(index, true);
            }
        }
        throw e;
    }
}

看一下ViewRootImpl的setView():

 /**
   * We have one child
   */
public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
    synchronized (this) {

            .........


            // Schedule the first layout -before- adding to the window
            // manager, to make sure we do the relayout before receiving
            // any other events from the system.
            requestLayout();

            ...........
            //指定DecorView为ViewRootImpl的子View
            view.assignParent(this);
          
           ...........

    }
}

这里就把DecorView和ViewRootImpl绑定了。看到这里,再通过requestLayout()源码我们可以知道,requestLayout()最终会调用到ViewRootImpl的requestLayout()。我们看一下ViewRootImpl的requestLayout():

 @Override
public void requestLayout() {
    //没有处理布局请求
    if (!mHandlingLayoutInLayoutRequest) {
        //检测是否在主线程
        checkThread();
        //修改布局请求处理标记为true
        mLayoutRequested = true;
        //view的绘制过程触发
        scheduleTraversals();
    }
}

在ViewRootImpl.requestLayout()里面会去调用scheduleTraversals(),这个方法就是View的绘制过程的起点,具体怎么触发view的绘制过程,下篇再分析。

View.invalidate()
 /**
 * Invalidate the whole view. If the view is visible,
 * {@link #onDraw(android.graphics.Canvas)} will be called at some point in
 * the future.
 * <p>
 * This must be called from a UI thread. To call from a non-UI thread, call
 * {@link #postInvalidate()}.
 */
 //如果view是visible,就重绘这个view.  onDraw()将会在将来某个时间点被调用,
 //这个方法必须要在UI线程中使用,如果要在非UI线程中重绘view,使用postInvalidate()
public void invalidate() {
    invalidate(true);
}

//invalidateCache 绘制缓存是否也要被重绘
public void invalidate(boolean invalidateCache) {
    invalidateInternal(0, 0, mRight - mLeft, mBottom - mTop, invalidateCache, true);
}

从注释中可以看到要在UI线程中重绘调用invalidate(),如果要在工作线程中重绘要用postInvalidate().我们看一下实际的调用者invalidateInternal():

void invalidateInternal(int l, int t, int r, int b, boolean invalidateCache,
        boolean fullInvalidate) {
    if (mGhostView != null) {
        mGhostView.invalidate(true);
        return;
    }
  //如果view不可见或者正在做动画,跳过重绘
    if (skipInvalidate()) {
        return;
    }
  //如果正在绘制并且有范围(View大小确定了) 或者绘制缓存有效 或者没有重绘标记 或者 不透明
    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)) == (PFLAG_DRAWN | PFLAG_HAS_BOUNDS)
            || (invalidateCache && (mPrivateFlags & PFLAG_DRAWING_CACHE_VALID) == PFLAG_DRAWING_CACHE_VALID)
            || (mPrivateFlags & PFLAG_INVALIDATED) != PFLAG_INVALIDATED
            || (fullInvalidate && isOpaque() != mLastIsOpaque)) {
        //全部重绘
        if (fullInvalidate) {
          //记录是否透明
            mLastIsOpaque = isOpaque();
          //取消正在绘制的标记
            mPrivateFlags &= ~PFLAG_DRAWN;
        }
       //添加变脏标记
        mPrivateFlags |= PFLAG_DIRTY;
        //如果绘制缓存也要重绘,一般为true
        if (invalidateCache) {
            //添加重绘标记
            mPrivateFlags |= PFLAG_INVALIDATED;
            //移除绘制缓存有效标记
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }

        // Propagate the damage rectangle to the parent view.
        //传递重绘区域给父View
        final AttachInfo ai = mAttachInfo;
        final ViewParent p = mParent;
        if (p != null && ai != null && l < r && t < b) {
            final Rect damage = ai.mTmpInvalRect;
            damage.set(l, t, r, b);
            //重绘子View
            p.invalidateChild(this, damage);
        }

        // Damage the entire projection receiver, if necessary.
        if (mBackground != null && mBackground.isProjected()) {
            final View receiver = getProjectionReceiver();
            if (receiver != null) {
                receiver.damageInParent();
            }
        }
    }
}

接着看ViewGroup的invalidateChild(View child, final Rect dirty):

public final void invalidateChild(View child, final Rect dirty) {
    final AttachInfo attachInfo = mAttachInfo;
    if (attachInfo != null && attachInfo.mHardwareAccelerated) {
        // HW accelerated fast path
        //使用硬件加速绘制
        onDescendantInvalidated(child, child);
        return;
    }
    //局部变量parent初始值为自己
    ViewParent parent = this;
    if (attachInfo != null) {
        // If the child is drawing an animation, we want to copy this flag onto
        // ourselves and the parent to make sure the invalidate request goes
        // through
        final boolean drawAnimation = (child.mPrivateFlags & PFLAG_DRAW_ANIMATION) != 0;

        // Check whether the child that requests the invalidate is fully opaque
        // Views being animated or transformed are not considered opaque because we may
        // be invalidating their old position and need the parent to paint behind them.
        Matrix childMatrix = child.getMatrix();
        final boolean isOpaque = child.isOpaque() && !drawAnimation &&
                child.getAnimation() == null && childMatrix.isIdentity();
        // Mark the child as dirty, using the appropriate flag
        // Make sure we do not set both flags at the same time
        int opaqueFlag = isOpaque ? PFLAG_DIRTY_OPAQUE : PFLAG_DIRTY;
      //默认LayerType就是LAYER_TYPE_NONE
        if (child.mLayerType != LAYER_TYPE_NONE) {
            mPrivateFlags |= PFLAG_INVALIDATED;
            mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        }

        final int[] location = attachInfo.mInvalidateChildLocation;
        //记录要重绘的子view的相对于父View的坐标
        location[CHILD_LEFT_INDEX] = child.mLeft;
        location[CHILD_TOP_INDEX] = child.mTop;
        if (!childMatrix.isIdentity() ||
                (mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
            RectF boundingRect = attachInfo.mTmpTransformRect;
            boundingRect.set(dirty);
            Matrix transformMatrix;
            if ((mGroupFlags & ViewGroup.FLAG_SUPPORT_STATIC_TRANSFORMATIONS) != 0) {
                Transformation t = attachInfo.mTmpTransformation;
                boolean transformed = getChildStaticTransformation(child, t);
                if (transformed) {
                    transformMatrix = attachInfo.mTmpMatrix;
                    transformMatrix.set(t.getMatrix());
                    if (!childMatrix.isIdentity()) {
                        transformMatrix.preConcat(childMatrix);
                    }
                } else {
                    transformMatrix = childMatrix;
                }
            } else {
                transformMatrix = childMatrix;
            }
            transformMatrix.mapRect(boundingRect);
            dirty.set((int) Math.floor(boundingRect.left),
                    (int) Math.floor(boundingRect.top),
                    (int) Math.ceil(boundingRect.right),
                    (int) Math.ceil(boundingRect.bottom));
        }

        do {
            View view = null;
          //如果局部变量parent属于View,就给view赋值
            if (parent instanceof View) {
                view = (View) parent;
            }

            if (drawAnimation) {
                if (view != null) {
                    view.mPrivateFlags |= PFLAG_DRAW_ANIMATION;
                } else if (parent instanceof ViewRootImpl) {
                    ((ViewRootImpl) parent).mIsAnimating = true;
                }
            }

            // If the parent is dirty opaque or not dirty, mark it dirty with the opaque
            // flag coming from the child that initiated the invalidate
            if (view != null) {
                if ((view.mViewFlags & FADING_EDGE_MASK) != 0 &&
                        view.getSolidColor() == 0) {
                    opaqueFlag = PFLAG_DIRTY;
                }
                if ((view.mPrivateFlags & PFLAG_DIRTY_MASK) != PFLAG_DIRTY) {
                    view.mPrivateFlags = (view.mPrivateFlags & ~PFLAG_DIRTY_MASK) | opaqueFlag;
                }
            }
           //调用自己的(parent初始值为自己)invalidateChildInParent(location, dirty),返回的却是自己的mParent,
           //也就是自己的父View.while循环判断父View是否为null,不为null,一直向上传递,直到ViewRootImp
            parent = parent.invalidateChildInParent(location, dirty);
            if (view != null) {
                // Account for transform on current parent
                Matrix m = view.getMatrix();
                if (!m.isIdentity()) {
                    RectF boundingRect = attachInfo.mTmpTransformRect;
                    boundingRect.set(dirty);
                    m.mapRect(boundingRect);
                    dirty.set((int) Math.floor(boundingRect.left),
                            (int) Math.floor(boundingRect.top),
                            (int) Math.ceil(boundingRect.right),
                            (int) Math.ceil(boundingRect.bottom));
                }
            }
        } while (parent != null);
    }
}

再看一下ViewGroup的invalidateChildInParent(location, dirty):

public ViewParent invalidateChildInParent(final int[] location, final Rect dirty) {
    //当前ViewGroup有正在绘制标志位或者绘制缓存有效标志位
    if ((mPrivateFlags & (PFLAG_DRAWN | PFLAG_DRAWING_CACHE_VALID)) != 0) {
        // either DRAWN, or DRAWING_CACHE_VALID
        if ((mGroupFlags & (FLAG_OPTIMIZE_INVALIDATE | FLAG_ANIMATION_DONE))
                != FLAG_OPTIMIZE_INVALIDATE) {
            dirty.offset(location[CHILD_LEFT_INDEX] - mScrollX,
                    location[CHILD_TOP_INDEX] - mScrollY);
            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == 0) {
                dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
            }

            final int left = mLeft;
            final int top = mTop;

            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                if (!dirty.intersect(0, 0, mRight - left, mBottom - top)) {
                    dirty.setEmpty();
                }
            }

            location[CHILD_LEFT_INDEX] = left;
            location[CHILD_TOP_INDEX] = top;
        } else {

            if ((mGroupFlags & FLAG_CLIP_CHILDREN) == FLAG_CLIP_CHILDREN) {
                dirty.set(0, 0, mRight - mLeft, mBottom - mTop);
            } else {
                // in case the dirty rect extends outside the bounds of this container
                dirty.union(0, 0, mRight - mLeft, mBottom - mTop);
            }
            location[CHILD_LEFT_INDEX] = mLeft;
            location[CHILD_TOP_INDEX] = mTop;

            mPrivateFlags &= ~PFLAG_DRAWN;
        }
      //移除绘制缓存有效标记
        mPrivateFlags &= ~PFLAG_DRAWING_CACHE_VALID;
        if (mLayerType != LAYER_TYPE_NONE) {
            mPrivateFlags |= PFLAG_INVALIDATED;
        }
        返回当前ViewGroup的父View
        return mParent;
    }

    return null;
}

可以看到View.invalidate()主要内容有:
1,调用真正的重绘方法invalidateInternal();
2,将要绘制区域传递给父View,调用父View的invalidateChild();
3,调用invalidateChildInParent()处理child的dirty矩阵与ViewGroup可显示矩阵的关系,同时返回该父View的Parent以便下次循环接着调用(mParent就当做是ViewGroup了)

这里层层往上传递的过程就和requestLayout类似了,唯一的不同是,invalidate()最终调用的ViewRootImpl的invalidateChildInParent():

 @Override
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
    checkThread();
    if (DEBUG_DRAW) Log.v(mTag, "Invalidate child: " + dirty);
    //重绘区域为null,再次请求重绘
    if (dirty == null) {
        invalidate();
        return null;
    } else if (dirty.isEmpty() && !mIsAnimating) {
        return null;
    }

    if (mCurScrollY != 0 || mTranslator != null) {
        mTempRect.set(dirty);
        dirty = mTempRect;
        if (mCurScrollY != 0) {
            dirty.offset(0, -mCurScrollY);
        }
        if (mTranslator != null) {
            mTranslator.translateRectInAppWindowToScreen(dirty);
        }
        if (mAttachInfo.mScalingRequired) {
            dirty.inset(-1, -1);
        }
    }
    //在屏幕上重绘刷新区域
    invalidateRectOnScreen(dirty);

    return null;
}

接着看ViewRootImpl的 invalidateRectOnScreen:

private void invalidateRectOnScreen(Rect dirty) {
    final Rect localDirty = mDirty;
    if (!localDirty.isEmpty() && !localDirty.contains(dirty)) {
        mAttachInfo.mSetIgnoreDirtyState = true;
        mAttachInfo.mIgnoreDirtyState = true;
    }

    // Add the new dirty rect to the current one
    localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
    // Intersect with the bounds of the window to skip
    // updates that lie outside of the visible region
    final float appScale = mAttachInfo.mApplicationScale;
    final boolean intersected = localDirty.intersect(0, 0,
            (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
    if (!intersected) {
        localDirty.setEmpty();
    }
    //如果马上就要绘制,且重绘区域有变化或正在动画
    if (!mWillDrawSoon && (intersected || mIsAnimating)) {
        //触发view的绘制过程
        scheduleTraversals();
    }
}

至此,View的两个非常重要的方法requestLayout(),invalidate分析完毕
(待完善:1,requestLayout与invalidate的区别,2,invalidate和postInvalidate的区别,3,invalidate如何控制只绘制发起重绘请求的view,而不是所有的View)

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

推荐阅读更多精彩内容