Android 面试准备进行曲 (Android基础进阶 一 )v1.3

update time 2019年11月28日 15:27:20 / 添加 假目录。哪位大佬有比较好的可以供他人查看的好目录

原CSDN 地址

View相关

View的绘制流程

自定义控件:
1、组合控件。这种自定义控件不需要我们自己绘制,而是使用原生控件组合成的新控件。如标题栏。
2、继承原有的控件。这种自定义控件在原生控件提供的方法外,可以自己添加一些方法。如制作圆角,圆形图片。
3、完全自定义控件:这个View上所展现的内容全部都是我们自己绘制出来的。比如说制作水波纹进度条。

View的绘制流程:OnMeasure()——>OnLayout()——>OnDraw()

第一步:OnMeasure():测量视图大小。从顶层父View到子View递归调用measure方法,measure方法又回调OnMeasure。

第二步:OnLayout():确定View位置,进行页面布局。从顶层父View向子View的递归调用view.layout方法的过程,即父View根据上一步measure子View所得到的布局大小和布局参数,将子View放在合适的位置上。

第三步:OnDraw():绘制视图。ViewRoot创建一个Canvas对象,然后调用OnDraw()。


在这里插入图片描述

View,ViewGroup事件分发

Touch事件分发中只有两个主角:ViewGroup和View。ViewGroup包含onInterceptTouchEvent、dispatchTouchEvent、onTouchEvent三个相关事件。View包含dispatchTouchEvent、onTouchEvent两个相关事件。其中ViewGroup又继承于View。

2.ViewGroup和View组成了一个树状结构,根节点为Activity内部包含的一个ViwGroup。

3.触摸事件由Action_Down、Action_Move、Aciton_UP组成,其中一次完整的触摸事件中,Down和Up都只有一个,Move有若干个,可以为0个。

4.当Acitivty接收到Touch事件时,将遍历子View进行Down事件的分发。ViewGroup的遍历可以看成是递归的。分发的目的是为了找到真正要处理本次完整触摸事件的View,这个View会在onTouchuEvent结果返回true。

5.当某个子View返回true时,会中止Down事件的分发,同时在ViewGroup中记录该子View。接下去的Move和Up事件将由该子View直接进行处理。由于子View是保存在ViewGroup中的,多层ViewGroup的节点结构时,上级ViewGroup保存的会是真实处理事件的View所在的ViewGroup对象:如ViewGroup0-ViewGroup1-TextView的结构中,TextView返回了true,它将被保存在ViewGroup1中,而ViewGroup1也会返回true,被保存在ViewGroup0中。当Move和UP事件来时,会先从ViewGroup0传递至ViewGroup1,再由ViewGroup1传递至TextView。

6.当ViewGroup中所有子View都不捕获Down事件时,将触发ViewGroup自身的onTouch事件。触发的方式是调用super.dispatchTouchEvent函数,即父类View的dispatchTouchEvent方法。在所有子View都不处理的情况下,触发Acitivity的onTouchEvent方法。

7.onInterceptTouchEvent有两个作用:1.拦截Down事件的分发。2.中止Up和Move事件向目标View传递,使得目标View所在的ViewGroup捕获Up和Move事件。

在这里插入图片描述

view 事件分发流程


在这里插入图片描述

ViewGroup 时间分发流程


在这里插入图片描述

整体Activity - ViewGroup - view 分发流程
在这里插入图片描述

View 事件分发及源码讲解

MeasureSpec 相关知识

MeasureSpec 是一个32位int值,高2位代表SpecMode(测量模式),低30位代表SpecSize( 某种测量模式下的规格大小)。通过宽测量值widthMeasureSpec和高测量值heightMeasureSpec决定View的大小
SpecMode 代表的三种测量模式分别为:

  1. UNSPECIFIED:父容器不对View有任何限制,要多大有多大。常用于系统内部。

  2. EXACTLY(精确模式):父视图为子视图指定一个确切的尺寸SpecSize。对应LyaoutParams中的match_parent或具体数值。

  3. AT_MOST(最大模式):父容器为子视图指定一个最大尺寸SpecSize,View的大小不能大于这个值。对应LayoutParams中的wrap_content。

决定因素:值由子View的布局参数LayoutParams和父容器的MeasureSpec值共同决定。见下图:

在这里插入图片描述

参考图片 及讲解地址

SurfaceView和View的区别

SurfaceView是从View基类中派生出来的显示类,他和View的区别有:

  • View需要在UI线程对画面进行刷新,而SurfaceView可在子线程进行页面的刷新,View适用于主动更新的情况,View频繁刷新会阻塞主线程,导致界面卡顿
  • SurfaceView在底层已实现双缓冲机制,而View没有,因此SurfaceView更适用于被动更新,需要频繁刷新、刷新时数据处理量很大的页面,而SurfaceView适用于

invalidate()和postInvalidate()的区别

invalidate()与postInvalidate()都用于刷新View,主要区别是invalidate()在主线程中调用,若在子线程中使用需要配合handler;而postInvalidate()可在子线程中直接调用。
我们通过 postInvalidate 如何在子线程中更新的

    // 系统代码
    public void postInvalidateDelayed(long delayMilliseconds) {
        // We try only with the AttachInfo because there's no point in invalidating
        // if we are not attached to our window
        final AttachInfo attachInfo = mAttachInfo;
        if (attachInfo != null) {
            attachInfo.mViewRootImpl.dispatchInvalidateDelayed(this, delayMilliseconds);
        }
    }

接下来我们看下

    // 系统代码
     public void dispatchInvalidateDelayed(View view, long delayMilliseconds) {
        Message msg = mHandler.obtainMessage(MSG_INVALIDATE, view);
        mHandler.sendMessageDelayed(msg, delayMilliseconds);
    }

我们可以看到 postInvalidate它是向主线程发送个Message,然后handleMessage时,调用了invalidate()函数。(系统帮我们 写好了 Handle部分)

Android 动画

Android中的几种动画

帧动画:指通过指定每一帧的图片和播放时间,有序的进行播放而形成动画效果,比如想听的律动条。

补间动画:指通过指定View的初始状态、变化时间、方式,通过一系列的算法去进行图形变换,从而形成动画效果,主要有Alpha、Scale、Translate、Rotate四种效果。注意:只是在视图层实现了动画效果,并没有真正改变View的属性,比如滑动列表,改变标题栏的透明度。

属性动画:在Android3.0的时候才支持,通过不断的改变View的属性,不断的重绘而形成动画效果。相比于视图动画,View的属性是真正改变了。比如view的旋转,放大,缩小。

属性动画和补间动画区别

  • 补间动画仅仅是 Parents View 对子View里面的画布进行操作,新位置并不响应点击事件,原位置响应。
  • 属性动画是通过修改view属性实现动画,新位置响应点击事件

属性动画为何在新位置还能响应事件

ViewGroup 在 getTransformedMotionEvent() 方法中会通过子 View 的 hasIdentityMatrix() 方法来判断子 View 是否应用过位移、缩放、旋转之类的属性动画。如果应用过的话,那还会调用子 View 的 getInverseMatrix() 做「反平移」操作,然后再去判断处理后的触摸点是否在子 View 的边界范围内。

属性动画点击解密

属性动画原理

属性动画要求 动画作用的对象提供该属性的set方法,属性动画根据你传递的该熟悉的初始值和最终值,以动画的效果多次去调用set方法,每次传递给set方法的值都不一样,确切来说是随着时间的推移,所传递的值越来越接近最终值。如果动画的时候没有传递初始值,那么还要提供get方法,因为系统要去拿属性的初始值。

// 系统代码
void setAnimatedValue(Object target) {
        if (mProperty != null) {
            mProperty.set(target, getAnimatedValue());
        }
        if (mSetter != null) {
            try {
                mTmpValueArray[0] = getAnimatedValue();
                mSetter.invoke(target, mTmpValueArray);
            } catch (InvocationTargetException e) {
                Log.e("PropertyValuesHolder", e.toString());
            } catch (IllegalAccessException e) {
                Log.e("PropertyValuesHolder", e.toString());
            }
        }
}

属性动画源码解析

Handler 详解

Handler的原理

Android中主线程是不能进行耗时操作的,子线程是不能进行更新UI的。所以就有了handler,它的作用就是实现线程之间的通信。
handler整个流程中,主要有四个对象,handlerMessage,MessageQueue,Looper。当应用创建的时候,就会在主线程中创建handler对象。

对于Message

在线程之间传递的消息,它的内部持有Handler和Runnable的引用以及消息类型。可以使用what、arg1、arg2字段携带一些整型数据,使用obj字段携带Object对象;其中有一个obtain()方法,该方法的内部是先通过消息池获取消息,没有再创建,实现了对message对象的复用。其内部有一个target引用,就是对Handler对象的引用,在Looper.loop方法中的消息处理就是通过message的target引用来调用Handler的dispatchMessage()方法来实现消息的处理。

对于Message Queue:

指的是消息队列,是通过一个 单链表 的数据结构维护消息列表的,在插入和删除有优势。其中主要包括两个操作:插入和读取,读取操作本身伴随着删除操作。插入操作是enqueueMessage()方法,就是插入一条消息到MessageQueue中;读取操作是next()方法,它是一个无限循环,如果有消息就返回并从单链表中移除;没有消息就一直阻塞(此时主线程会释放CPU进入休眠状态)。

对于Looper:

Looper在消息机制中进行消息循环,像一个泵,不断地从MessageQueue中查看是否有新消息并提取,交给handler处理。Handler机制一定要Looper,在线程中通过Looper.prepare()为当前线程创建一个Looper,并使用Looper.loop()来开启消息的读取。为什么在平常Activity主线程使用时没有使用到Looper呢?因为对于主线程(UI线程),会自动创建一个Looper 驱动消息队列获取消息,所以Looper可以通过getMainLooper获取到主线程的Looper。
通过quit/quitSafely可以退出Looper,区别在于quit会直接退出,quitSafely会把消息队列已有的消息处理完毕后才退出Looper。

对于Handler

Handler可以发送和接收消息。发送消息(就是往MessageQueue里面插入一条Message)通过post方法和send方法,而post方法最终也是通过send方法来发送的,最终就会调用sendMessageAtTime这个方法(内部就是调用MessageQueue的enqueueMessage()方法,往MessageQueue里面插入一条消息),同时也会给msg的target赋值为handler本身,进入MessageQueue中。处理消息就是Looper调用loop()方法进入无限循环,获取到消息后就会调用msg.target(Handler本身)的dispatchMessage()方法,进而调用handlerMessage()方法处理消息。

Handler导致内存泄露问题

一般我们写Handler:

Handler mHandler = new Handler() {
    @Override
    public void handleMessage(Message msg) {
        mImageView.setImageBitmap(mBitmap);
    }
}

当使用内部类(包括匿名类)来创建Handler的时候,Handler对象会隐式地持有一个外部类对象(通常是一个Activity)的引用,而常常在Activity退出后,消息队列还有未被处理完的消息,此时activity依然被handler引用,导致内存无法回收而内存泄露。

在Handler中增加一个对Activity的弱引用(WeakReference):

static class MyHandler extends Handler {
    WeakReference mActivityReference;

    MyHandler(Activity activity) {
        mActivityReference= new WeakReference(activity);
    }

    @Override
    public void handleMessage(Message msg) {
        final Activity activity = mActivityReference.get();
        if (activity != null) {
            mImageView.setImageBitmap(mBitmap);
        }
    }
}

如果在非自定义 Handler 情况下,还可以通过 Activity 生命周期来及时清除消息,从而及时回收 Activity

override fun onDestroy() {
        super.onDestroy()
        if (mHandler != null){
            mHandler.removeCallbacksAndMessages(null)
        }
    }

Handler的post方法原理

mHandler.post(new Runnable()
        {
            @Override
            public void run()
            {
                Log.e(“TAG”, Thread.currentThread().getName());
                mTxt.setText(“yoxi”);
            }
        });

然后run方法中可以写更新UI的代码,其实这个Runnable并没有创建什么线程,而是发送了一条消息,下面看源码:

public final boolean post(Runnable r)
   {
      return  sendMessageDelayed(getPostMessage(r), 0);
   }

最终和handler.sendMessage一样,调用了sendMessageAtTime,然后调用了enqueueMessage方法,给msg.target赋值为handler,最终加入MessagQueue.

Handler 其他问题

  1. Looper.loop()和MessageQueue.next()取延时消息时,主线程中使用死循环为什么不会卡死?

    答: 在MessageQueue在取消息时,如果是延时消息就会计算得到该延时消息还需要延时多久nextPollTimeoutMillis。然后再继续循环的时候,发现nextPollTimeoutMillis不等于0,就会执行nativePollOnce阻塞线程nextPollTimeoutMillis毫秒,而阻塞了之后被唤醒的时机就是阻塞的时间到了或者又有新的消息添加进来执行enqueueMessage方法调用nativeWake唤醒阻塞线程,再继续执行获取消息的代码,如果有消息就返回,如果还是需要延时就继续和上边一样阻塞。而Android所有的事件要在主线程中改变的都会通过主线程的Handler发送消息处理,所以就完全保证了不会卡死。

    其中nativePollOnce的位置也有考究,刚好在synchronized的外边,所以在阻塞的时候也能保证添加消息是可以执行的,而取消息 时添加消息就需要等待。

  1. MessageQueue是队列吗?
    答: MessageQueue不是队列,它内部使用一个Message链表实现消息的存和取。

  2. Handler的postDelay,时间准吗?它用的是system.currentTime吗?
    答: 不准,因为looper里边从MessageQueue里取出来的消息执行也是串行的,如果前一个消息是比较耗时的,那么等到执行之前延时的消息时时间难免可能会超过延时的时间。postDelay时用的是System.uptimeMillis,也就是开机时间。

  3. 子线程run方法中如何使用Handler?
    答 : 先要使用Lopper.prepare方法,然后使用该looper创建一个Handler,最后调用Looper.loop方法;Looper.loop方法之后就不要执行写代码了,因为是loop是死循环除非退出,所以Handler的创建也必须写在loop之前。

  4. ThreadLocal是如何实现一个线程一个Looper的?
    答: Looper的使用最终都需要执行loop方法,而loop方法中去获取的Looper是从sThreadLocal中获取的,所以Looper就需要和sThreadLocal建立关系,在不考虑反射的情况下,就只能通过Looper的prepare方法进行关联,这里边就会引入一个threadLocalMap,该对象又是和thread一一对应,而threadLocal的get方法实际使用的就是threadLocalMap的get方法,而key就是Looper中的静态变量sThreadLocal,value则就是当前looper对象,而prepare方法只能被执行一次,也就保证了一个线程只有一个looper。ThreadLocalMap对key和value的存取和hashMap类似。

  5. 假设先 postDelay 10ms, 再postDelay 1ms,这两个消息会有什么不同的经历。
    答: 先传入一个延时为10ms的消息进入MessageQueue中,因为该消息延时,假设当前消息队列中没有消息,则会直接将消息放入队列,因为loop一直在取消息,但是这里有延时就会阻塞10ms,当然这不考虑代码执行的时间;然后延时1ms的消息进入时,会和之前的10ms的消息进行比较,根据延时的大小进行排序插入,延时小的在前边,所以这时候就把1ms的消息放在10ms的前边,然后唤醒,不阻塞,继续执行取消息的操作,发现还是有延时1ms,所以也会继续阻塞1ms,直到阻塞1ms之后或者又有新的消息进入队列唤醒,直到获取到1ms延时消息,在loop中,通过调用handler的dispatchMessage方法,判断消息的callback或者Handler的callback不为null就回调对应的callback,否则就执行handler的handleMessage方法,我们就可以根据情况处理消息了;10ms的延时消息的处理也是一致,延时的时间到了就交给返回给looper,然后给handler处理。

  6. HandlerThread ?
    答: HandlerThread是Thread的一个子类,只是内部创建了一个Handler,这个Handler是子线程的handler,其中子线程的looper的创建和管理也提供了方法方便使用。

  7. 你对 Message.obtain() 了解吗?
    答: Message.obtain其实是从缓冲的消息池中取出第一个消息来使用,避免消息对象的频繁创建和销毁;消息池其实是使用Message链表结构实现,在消息在loop中被handler分发消费之后会执行回收的操作,将该消息内部数据清空并添加到消息链表最前边。

多线程相关问题

如何创建多线程

  1. 继承Thread类,重写run函数方法
  2. 实现Runnable接口,重写run函数方法
  3. 实现Callable接口,重写call函数方法
  4. HandlerThread
  5. AsyncTask很老的一种= =

多线程间同步问题

  1. volatile关键字,在get和set的场景下是可以的,由于get和set的时候都加了读写内存屏障,在数据可见性上保证数据同步。但是对于++这种非原子性操作,数据会出现不同步

  2. synchronized对代码块或方法加锁,结合wait,notify调度保证数据同步

  3. reentrantLock加锁结合Condition条件设置,在线程调度上保障数据同步

  4. CountDownLatch简化版的条件锁,在线程调度上保障数据同步

  5. cas=compare and swap(set), 在保证操作原子性上,确保数据同步

  6. 参照UI线程更新UI的思路,使用handler把多线程的数据更新都集中在一个线程上,避免多线程出现脏读

  7. 当然如果只是部分变量存在多线程修改的可能性 建议使用 原子类AtomicInteger AtomicBoolean等 这样会更方便一点。

Android 优化

OOM

  1. 根据java的内存模型会出现内存溢出的内存有堆内存、方法区内存、虚拟机栈内存、native方法区内存;一般说的OOM基本都是针对堆内存;

  2. 对于堆内存溢出主的根本原因有两种
    (1)app进程内存达到上限
    (2)手机可用内存不足,这种情况并不是我们app消耗了很多内存,而是整个手机内存不足

而我们需要解决的主要是 app的内存达到上限

  1. 对于app内存达到上限只有两种情况
    (1)申请内存的速度超出gc释放内存的速度
    (2)内存出现泄漏,gc无法回收泄漏的内存,导致可用内存越来越少

  2. 对于申请内存速度超出gc释放内存的速度主要有2种情况
    (1)往内存中加载超大文件
    (2)循环创建大量对象

  3. 一般申请内存的速度超出gc释放内存基本不会出现,内存泄漏才是出现问题的关键所在

导致内存泄漏情况

内存泄漏的根本原因在于生命周期长的对象持有了生命周期短的对象的引用

  1. 资源对象没关闭造成的内存泄漏(如: Cursor、File等)

  2. 全局集合类强引用没清理造成的内存泄漏( static 修饰的集合)

  3. 接收器、监听器注册没取消造成的内存泄漏,如广播,eventsbus

  4. Activity 的 Context 造成的泄漏,可以使用 ApplicationContext

  5. Handler 造成的内存泄漏问题(一般由于 Handler 生命周期比其外部类的生命周期长引起的)

注1:ListView 的 Adapter 中缓存用的 ConvertView ,主要缓存的是 移除屏幕外的View,就算没有复用,暂时 只会 内存溢出,和泄漏还是有区别的。

注2 :Bitmap 对象到底要不要调用 recycle() 释放内存。结论 Android 3.0 以前需要,因为像素数据与对象本身分开存储,像素数据存储在native层;对象存储在java层。 3.0之后 像素数据与Bitmap对象数据一起关联存储在Dalvik堆中。所以,这个时候,就可以考虑用GC来自动回收。所以我们不用的时候直接 将Bitmap对象设置为Null 即可。参考博客地址

我们列举了 大部分常见的 内存泄漏出现的时机,那么我也简要的列举下 常见的避免内存泄漏的方法(仅供参考);

  1. 为应用申请更大内存,把manifest上的largdgeheap设置为true

  2. 减少内存的使用
    ①使用优化后的集合对象,比如SpaseArray;
    ②使用微信的mmkv替代sharedpreference;
    ③对于经常打log的地方使用StringBuilder来组拼,替代String拼接
    ④统一带有缓存的基础库,特别是图片库,如果用了两套不一样的图片加载库就会出现2个图片各自维护一套图片缓存
    ⑤给ImageView设置合适尺寸的图片,列表页显示缩略图,查看大图显示原图
    ⑥优化业务架构设计,比如省市区数据分批加载,需要加载省就加载省,需要加载市就加载失去,避免一下子加载所有数据

  3. 避免内存泄漏

    编码规范上:

    ①资源对象用完一定要关闭,最好加finally
    ②静态集合对象用完要清理
    ③接收器、监听器使用时候注册和取消成对出现
    ④context使用注意生命周期,如果是静态类引用直接用ApplicationContext
    ⑤使用静态内部类
    ⑥结合业务场景,设置软引用,弱引用,确保对象可以在合适的时机回收

建设内存监控体系:

线下监控:
①使用ArtHook检测图片尺寸是否超出imageview自身宽高的2倍

②编码阶段Memery Profile看app的内存使用情况,是否存在内存抖动,内存泄漏,结合Mat分析内存泄漏

线上监控:

①上报app使用期间待机内存、重点模块内存、OOM率
②上报整体及重点模块的GC次数,GC时间
③使用LeakCannery自动化内存泄漏分析

ANR

Android系统中,AMS和WMS会检测App的响应时间,如果App在主线程进行耗时操作,导致用户的操作没得到及时的响应 就会报出 Application Not Response 的问题 即ANR 。

  1. activity 、键盘输入事件和触摸事件超过五秒
  2. 前台广播10秒没有完成 后台60秒
  3. 服务前台20秒 后台200秒

主要的 规避方案

解决笼统一下尽量使用 子线程,避免死锁 的出现,使用子线程来处理耗时操作或阻塞任务。服务内容提供者尽量不要执行太长时间的任务。

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

推荐阅读更多精彩内容