View:事件传递流程

1 Touch事件底层传递过程

Touch的整体派发流程,遵循如下逻辑:

  1. Android中Touch事件的分发分服务端和应用端。Server端由WindowManagerService(WMS,窗体管理服务)负责采集和分发,client端则是由ViewRootImpl(内部有个mView变量指向View树的根 ,负责控制View树的UI绘制和事件消息的分发)负责分发的。
  2. WMS在启动之后,会在native层启动两个线程:InputReaderThread和InputDispatchThread。前者用来读取输入事件,后者用来分发输入事件,输入事件经过nativie层的层层传递,终于会传递到java层的ViewRootImpl中。调用ViewPostImeInputStage
    (ViewRootImpl的内部类)中的各个方法来分发不同的事件,而Touch事件是在processPointerEvent方法进行分发的。
  3. processPointerEvent方法中调用mView.dispatchPointerEvent(event)方法,进行判断event类型,如果是Touch事件则调用dispatchTouchEvent将该事件分发DecorView。


    Touch事件从WMS传递到ViewRootImp.png

    ① Touch事件从server端传递到client端採用的IPC方式并非Binder,而是共享内存和管道。至于为什么不採用Binder,应该是共享内存的效率更高,而管道(两个管道,分别负责不同方向的读和写)仅仅负责通知是否有事件发生,传递的仅仅是一个非常easy的字符串,因此并不会太多地影响到IPC的效率。
    ② 在sever端中,InputReader和InputDispatcher是native 层的两个线程。InputReader不断地从EventHub中读取事件,InputDispatcher则不断地分发InputReader读取到的事件,而实际的分发操作时在InputPublish中进行的。InputPublish里面保存的有一个指向server端InputChannel端的指针和一个指向ShareMemory(共享内存)的指针。
    ③ 当有事件要分发时,它会将事件写入到ShareMemory中,而且传递一个特定的字符串给InputChannel,由inutChannel将该字符串写入到管道中。在client端,一旦InputChannel从管道中读取到有事件分发过来,便会通知InPutConsumer从ShareMemory中读取详细的事件,并传递到framework层的InputQueue中。一旦事件消费完毕,client端会通过管道告诉server端,事件已经消费完毕,流程与上面的相似。

2 Touch事件应用层传递过程

2.1 基础信息概念

2.1.1 MotionEvent

包括Touch的位置、时间、历史记录以及第几个手指(多指触摸)等,分为如下事件:

  • MotionEvent.ACTION_DOWN
    当我们手指按下屏幕的第一个事件便是ACTION_DOWN了,也就是意味着事件的开始。
  • MotionEvent.ACTION_MOVE
    当我们手指按下屏幕后,在屏幕上滑动的过程,此事件就会不断的触发。
  • MotionEvent.ACTION_UP
    此事件在我们手指从屏幕抬起的时候会触发。
  • MotionEvent.ACTION_CANCEL
    这个事件说起来稍微复杂一点,举个栗子:当我们的外层View将事件传递给内层View去处理时,外层View的拦截方法一般会返回false。但是当某个条件触发后,外层View想自己处理接下来的事件,就拦截了事件分发,此时内层View就会收到ACTION_CANCEL的事件。
  • MotionEvent.ACTION_OUTSIDE
    这个事件我们不常用到,考虑这种场景:我们有一个Diallog弹出,当我们按Dialog以外的屏幕将Dialog消失掉。这个时候可以考虑监听这个事件,要想使用这个事件我们必须对当前的Window设置一个Flag:FLAG_WATCH_OUTSIDE_TOUCH。
2.1.2 处理方法
  • dispatchTouchEvent(MotionEvent event)
    这个方法是用来处理向下分发事件逻辑的,会调用onIntercepteTouchEvent和onTouchEvent方法。
  • onInterceptTouchEvent(MotionEvent event)
    用来申明是否拦截事件继续向下分发,如果返回true,事件将不会继续向下分发,而是交由自己的onTouchEvent方法处理。
  • onTouchEvent(MotionEvent event)
    事件处理的方法。
  • onTouch(MotionEvent event)
    这个方法是在我们对某一个setOnTouchListener时回调,也就是在传递事件的时候,在交给View本身的onTouchEvent处理之前判断是否有监听的TouchListener,如果有优先调用TouchListener的onTouch方法处理。
2.1.3 大概处理流程
  • 消息分发流程
    从上到下,从父到子:Activity -> PhoneWindow -> DecorView -> ViewGroup1->ViewGroup1的子ViewGroup2->…->Target View

  • 消息响应流程
    从下到上,从子到父:Target View->…->ViewGroup1的子ViewGroup2->ViewGroup1-> DecorView -> PhoneWindow -> Activity

2.2 源码分析

2.2.1 Activity中分发过程

Activity.dispatchTouchEvent(..)

public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

代码会走到PhoneWindow.superDispatchTouchEvent(...),看一下代码:

PhoneWindow.superDispatchTouchEvent(..)

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

调用了DecorView的superDispatchTouchEvent方法,再进去看看:

DecorView.superDispatchTouchEvent(..)

public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

DecorView的dispatchTouchEvent终于都会调用到自己父亲FrameLayout的dispatchTouchEvent方法。而我们在FrameLayout中找不到dispatchTouchEvent方法,所以会去运行ViewGroup的dispatchTouchEvent方法。

2.2.2 Activity中事件消费
public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }
    return false;
}

PhoneWindow.shouldCloseOnTouch(..)

public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
            && isOutOfBounds(context, event) && peekDecorView() != null) {
        return true;
    }
    return false;
}

假设设置了mCloseOnTouchOutside属性为true(相应xml中的android:windowCloseOnTouchOutside属性),且当前事件为down事件,且down事件发生在该Activity范围之外,而且DecorView不为null,就返回true。非常明显dialog形式的Activity可能会发生这种情况。

2.2.3 ViewGroup.dispatchTouchEvent(...)
public boolean dispatchTouchEvent(MotionEvent ev) {
    //调试用的
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
    }

    //handled为返回值,true表示是否有view消费了该事件
    boolean handled = false;
    //是否要过滤掉该Touch事件
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;

        if (actionMasked == MotionEvent.ACTION_DOWN) {
           //1、清空掉曾经消费事件的目标view,这里主要指清空掉mFirstTouchTarget链表(保存接受Touch事件的单链表),并将mFirstTouchTarget置为null。
           //2、重置触摸状态,重置了disallowIntercept和mPrivateFlags相应的标志位
           // 一般在发生app的切换或者ANR等情况时代码会走到这里,这一点源代码的注释里也有。
           cancelAndClearTouchTargets(ev);
           resetTouchState();
        }

        // 标记是否要拦截该Touch事件:true表示拦截,false表示不拦截
        final boolean intercepted;
        
        //假设当前事件为down事件或者可接受Touch事件的链表不为空,就运行if语句里的逻辑。这里注意:
        //1、因为down事件是一个完整事件序列的的起点,因此当发生down事件时,逻辑走到这里。由于还没有找到消费down事件的view,因此mFirstTouchTarget为null。
        //2、对于后面的move和up事件,假设前面的down事件被某个view消费掉了,则mFirstTouchTarget不为null。
        // 上面两种情况都会使代码进入if分支中来。
        if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
            //是否不同意拦截,默认得false,可以通过requestDisallowInterceptTouchEvent方法来设置其值
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            
            // 假设同意拦截,则onInterceptTouchEvent有效,依据我们覆写的该方法的返回值来推断是否拦截
            if (!disallowIntercept) {
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            // 假设当前事件不是down事件,且之前在分发down事件的时候没有找到消费down事件的目标view,也即mFirstTouchTarget为null,则直接拦截该事件。
            intercepted = true;
        }

        // 检查当前事件是否被取消
        final boolean canceled = resetCancelNextUpFlag(this)
                || actionMasked == MotionEvent.ACTION_CANCEL;

        // Update list of touch targets for pointer down, if needed.
        final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
     
        //保存消费事件的目标View所相应的 TouchTarget对象
        TouchTarget newTouchTarget = null;
        
        //事件是否已经分发到了目标View中
        boolean alreadyDispatchedToNewTouchTarget = false;
     
        // 假设没有被取消而且没有被拦截,就分发该事件。
        // 注意仅仅有down事件才会走到这里去分发,对于move和up实践,则会跳过这里。直接从 mFirstTouchTarget链表中找到之前消耗down事件的目标View,直接将move和up事件传递给它
        if (!canceled && !intercepted) {           
            // 仅仅有down事件会走到这里 
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {        
                //Touch事件的index,对于单点触控,一直为0,这里不用深究
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;
            
                // Clean up earlier touch targets for this pointer id in case they
                // have become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);
            
                //该ViewGroup中子View的个数
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    //当前事件发生的位置
                    final float x = ev.getX(actionIndex);
                    final float y = ev.getY(actionIndex);
               
                    //保存该ViewGroup中子View
                    final View[] children = mChildren;          
                    final boolean customOrder = isChildrenDrawingOrderEnabled();
                
                    //遍历子View,找到能消费该down事件的子View,对于类型为ViewGroup的子View,在分发的时候会递归调用到它的dispatchTouchEvent方法继续进行分发。
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = customOrder ?
                                getChildDrawingOrder(childrenCount, i) : i;
                        final View child = children[childIndex];
                    
                        //假设当前子View能够消费该down事件而且该down事件发生的位置在当前子View的范围内,则继续运行。
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            continue;
                        }
                    
                        //接受down事件的child是否已经在mFirstTouchTarget链表中,假设在的话,说明child已经消费掉了该down事件,直接跳出循环。
                        // 我在写demo跟代码时,没有一次走到这里的,临时不是非常清楚
                        newTouchTarget = getTouchTarget(child);
                        if (newTouchTarget != null) {
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                            break;
                        }
            
                        resetCancelNextUpFlag(child);
                        //假设该child还没有消费掉该down事件,就直接调用dispatchTransformedTouchEvent方法将该down事件传递给该child,该方法里面会调用到child的dispatchTouchEvent方法
                        //假设该方法返回true,则说明child消费掉了该down事件,那么就运行if语句里的逻辑,将child添加到mFirstTouchTarget链表的表头,而且将该表头赋值给newTouchTarget(參见addTouchTarget方法)
                        //同时将alreadyDispatchedToNewTouchTarget置为true,说明有子view消费掉了该down事件。
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            // Child wants to receive touch within its bounds.
                            mLastTouchDownTime = ev.getDownTime();
                            mLastTouchDownIndex = childIndex;
                            mLastTouchDownX = ev.getX();
                            mLastTouchDownY = ev.getY();
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                    }
                }
            
                //假设newTouchTarget为null而且 mFirstTouchTarget不为null,即没找到子View来消耗该事件,可是保存Touch事件的链表不为空,
                //则把newTouchTarget赋值为最早加进(Least Recently added)mFirstTouchTarget链表的target。临时没全然搞明确这里的详细意思,跟代码都没有走到这里。
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }

        //后面在处理MOVE和UP事件时会直接依据上次的DOWN是否被消费掉来直接进行相应的处理
        if (mFirstTouchTarget == null) {
            //假设没有子view接受该事件,则直接把当前的ViewGroup当作普通的View看待,把事件传递给自己(详见dispatchTransformedTouchEvent方法,注意第三个參数传递的是null)
            handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
        } else {
            //假设之前的DOWN事件被子view消费掉了,就会直接找到该子View相应的Target,将MOVE或UP事件传递给它们。
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {            
                    //假设该事件已经被消费掉了,则不再进行分发(该分支主要针对DOWN事件)
                    handled = true;
                } else {
                    //直接将DOWN或UP事件分发给目标Target(之前消费DOWN事件的view相应的target,注意dispatchTransformedTouchEvent的第三个參数为target.child)
                    //这里要注意的是,假设intercepted为true,也就是MOVE或UP事件被拦截了,则cancelChild为true,则会分发一次CANCLE事件(注意dispatchTransformedTouchEvent的第二个參数)。
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }

        // 假设当前事件是CANCLE或UP会调用resetTouchState方法。清空Touch状态,同时清空mFirstTouchTarget链表,并将mFirstTouchTarget置为null
        if (canceled
                || actionMasked == MotionEvent.ACTION_UP
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
            removePointersFromTouchTargets(idBitsToRemove);
        }
    }

    if (!handled && mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}

整体流程可以概括如下图所示:


ViewGroup-dispatchTouchEvent流程.png
2.2.4 View.dispatchTouchEvent(...)
public boolean dispatchTouchEvent(MotionEvent event) {
    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onTouchEvent(event, 0);
    }

    if (onFilterTouchEventForSecurity(event)) {
        //noinspection SimplifiableIfStatement
        ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED
                && li.mOnTouchListener.onTouch(this, event)) {
            return true;
        }

        if (onTouchEvent(event)) {
            return true;
        }
    }

    if (mInputEventConsistencyVerifier != null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
    }
    return false;
}

先推断该View有没有绑定OnTouchListener监听器,如果绑定且消费掉事件则直接返回,所以Touch事件的先后顺序:onTouch先于onTouchEvent。

2.2.5 View.onTouchEvent(...)
public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX();
    final float y = event.getY();
    final int viewFlags = mViewFlags;
    final int action = event.getAction();

    if ((viewFlags & ENABLED_MASK) == DISABLED) {
        //view如果是disabled状态,该view仍然消费该事件,但是不会做出UI的相应
        return (((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
    }

    if (mTouchDelegate != null) {
        if (mTouchDelegate.onTouchEvent(event)) {
            return true;
        }
    }

    if (((viewFlags & CLICKABLE) == CLICKABLE ||
            (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
            (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
        switch (action) {
            case MotionEvent.ACTION_UP:
                ...
                break;
            case MotionEvent.ACTION_DOWN:
                ...
                }
                break;
            case MotionEvent.ACTION_CANCEL:
                ...
                break;
            case MotionEvent.ACTION_MOVE:
                ...
                break;
        }
        return true;
    }
    return false;
}
View-onTouchEvent流程.png

2.3 Touch事件派发模型

ViewGroup派发消费流程.jpeg
  1. dispatchTouchEvent(...)方法
  • return true:消费掉事件,终止传递。
  • return false: 事件停止往子View传递,将事件传递给上一级View的onTouchEvent()方法。如果是Activity的dispatchTouchEvent()方法,则也是消费掉事件,终止传递。
  • return super:如果是Activity,则传给下一级view(viewGroup)的dispatchTouchEvent;如果是ViewGroup,则传给自己的onInterceptTouchEvent();如果是View,则传给自己的onTouchEvent()。
  1. onTouchEvent(...)方法
  • return true:消费掉事件,终止传递。
  • return false/super:将事件传递给上一级view的onTouchEvent()方法。
  1. onInterceptTouchEvent(...)方法
  • return true:将事件传递给ViewGroup自己的onTouchEvent()方法处理。
  • return false/super:将事件传递给下一级View的dispatchTouchEvent()。

说明:为什么只有ViewGroup有onInterceptTouchEvent()方法呢,从上面的整个触摸事件分发传递机制我们可以发现,ViewGroup本身的dispatchTouchEvent()方法无论返回什么都不能将事件传递给自己的onTouchEvent()方法处理,那就只好设计了一个这样子的方法,作为拦截器,拦截事件交给自己处理了。只要onInterceptTouchEvent()return true就可以实现触摸事件拦截。

3 举例说明

3.2 自定义视图

自定义视图ViewGroup-1,ViewGroup-2,View,每个自定义类复写其父类方法。视图层级结构如下:


View层级.png

按照上面的分析,其流程如下:


TouchEvent分发流程.png

说明:只有调用super方法,on每种类型的分析过程可以参考链接[5]

3.2 OnTouchListener接口

if (li != null && li.mOnClickListener != null) {  
    playSoundEffect(SoundEffectConstants.CLICK);  
    li.mOnClickListener.onClick(this);
    return true;  
}

结论:onTouchListener的onTouch方法优先级比onTouchEvent高,会先触发。假若onTouchListener中的onTouch方法返回true,表示此次事件已经被消费了,那onTouchEvent是接收不到消息的。假如onTouch方法返回false,会接着触发onTouchEvent。内置诸如click事件的实现等等都基于onTouchEvent,假如onTouch返回true,这些事件将不会被触发。

参考链接:

[1] Android Touch事件传递机制全面解析
[2] 进阶必备-Android事件分发机制
[3] Android touch 事件传递机制
[4] View事件传递 touch事件分发
[5] 一步步探索学习Android Touch事件分发传递机制(一)
[6] Android Deeper(00) - Touch事件分发响应机制
[7] 图解 Android 事件分发机制 ★√
[8] Activity touch事件传递流程分析
[9] View事件分发机制

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

推荐阅读更多精彩内容