源码解析:Android源码GLSurfaceView源码解析

前言

这篇文章就带着大家简单过一下Android的GLSurfaceView源码的一些主要的处理流程。

GLSurfaceView怎么用

在开始分析源码前,非常有必要先看看GLSurfaceView的基本使用方法:

mGLView= (GLSurfaceView) findViewById(R.id.gl_view);
mGLView.setEGLContextClientVersion(2);
//在setRenderer之前,可以调用以下方法来进行EGL设置
//mGLView.setEGLConfigChooser();    //颜色、深度、模板等等设置
//mGLView.setEGLWindowSurfaceFactory(); //窗口设置
//mGLView.setEGLContextFactory();   //EGLContext设置
//设置渲染器,渲染主要就是由渲染器来决定
mGLView.setRenderer(new GLSurfaceView.Renderer(){
    @Override
    public void onSurfaceCreated(GL10 gl, EGLConfig config) {
        //todo surface被创建后需要做的处理
    }
    @Override
    public void onSurfaceChanged(GL10 gl, int width, int height) {
        //todo 渲染窗口大小发生改变的处理
    }
    @Override
    public void onDrawFrame(GL10 gl) {
        //todo 执行渲染工作
    }
});
/*渲染方式,RENDERMODE_WHEN_DIRTY表示被动渲染,RENDERMODE_CONTINUOUSLY表示持续渲染*/
mGLView.setRenderMode(GLSurfaceView.RENDERMODE_WHEN_DIRTY);

源码分析

入口方法

我们先从setRenderer(Renderer renderer)这个入口开始。
它主要做了两件事:
1、检查环境和变量同步配置

//检测环境
checkRenderThreadState();
//同步配置项,如果没有设置取默认项(懒加载模式)
if (mEGLConfigChooser == null) {
    mEGLConfigChooser = new SimpleEGLConfigChooser(true);
}
if (mEGLContextFactory == null) {
    mEGLContextFactory = new DefaultContextFactory();
}
if (mEGLWindowSurfaceFactory == null) {
    mEGLWindowSurfaceFactory = new DefaultWindowSurfaceFactory();
}
mRenderer = renderer;

函数checkRenderThreadState()检查了mRenderer是否已经存在,存在则抛出异常;换句话说,我们不能在同一个GLSurfaceView调用多次setRenderer(Renderer renderer),会挂!
mEGLConfigChoosermEGLContextFactorymEGLWindowSurfaceFactory是用户在setRenderer之前,可以调用相关方法来进行EGL设置,如果没有设置则采用默认实现。
mEGLConfigChooser主要是指定了OpenGL ES一些颜色深度、缓存区深度的一些设定。
mEGLContextFactory主要是提供了创建和销毁EGL Context上下文的一些处理。
mEGLWindowSurfaceFactory主要是提供了创建和销毁EGLSurface的一些处理。
2、启动一个GL线程

mGLThread = new GLThread(mThisWeakRef);
mGLThread.start();

GLThread就是我们一直所说的GL线程,主要是用于与OpenGL ES环境进行交互的线程。
入参mThisWeakRef是一个弱引用,指向了GLSurfaceView本身。

GL线程

接下来我们需要分析一下GLThread这个GL线程,跟进到GLThreadrun()方法。
我们发现run()里面有一个方法guardedRun(),这个也就是GL线程的主要逻辑函数,由两个while(true)循环组成。

public void run() {
    try {
        //最最最重要的逻辑
        guardedRun();
    } 
    catch (InterruptedException e) {} 
    finally {}
}

转进去查看guardedRun()的代码。

我们先来查看初始化创建相关的代码。
在代码开头创建了一个EglHelperEglHelper是一个封装了一些EGL通用操作的工具类。

mEglHelper = new EglHelper(mGLSurfaceViewWeakRef);

首次循环,逻辑会走到这个位置:

if (! mHaveEglContext) {
    if (askedToReleaseEglContext) {
        askedToReleaseEglContext = false;
    } else {
        try {
            //进行OpenGL ES环境初始化
            mEglHelper.start();
        } catch (RuntimeException t) {}
        mHaveEglContext = true;
        createEglContext = true;
    }
}

流程很明显,将会调用EglHelper.start()进行OpenGL ES环境的初始化。
然后将标示量createEglContext设置为true。

查看一下EglHelper.start()的实现:

public void start() {
    //获取一个EGL实例
    mEgl = (EGL10) EGLContext.getEGL();
    //获取显示设备
    mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);
    //检测EglDisplay是否正常
    if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
        throw new RuntimeException("eglGetDisplay failed");
    }
    //初始化EGL的内部数据结构,返回EGL实现的主版本号和次版本号。
    int[] version = new int[2];
    if(!mEgl.eglInitialize(mEglDisplay, version)) {
        throw new RuntimeException("eglInitialize failed");
    }
    //获取GLSurfaceView的引用
    GLSurfaceView view = mGLSurfaceViewWeakRef.get();
    if (view == null) {
        mEglConfig = null;
        mEglContext = null;
    } else {
        //看到这里,大家还记得前面setEGLConfigChooser()、setEGLContextFactory()这两个方法么
        //就是让我们自己配置EGL参数或者自己创建EglContext的方法
        mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);
        mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
    }
    mEglSurface = null;
}

如果我们没有调用setEGLContextFactory()这个方法(除非特殊需求,一般来说也没有用到),那么GLSurfaceView会默认取DefaultContextFactory()来代替。

DefaultContextFactory中有一个创建EglContext的方法:

//最简单的创建EGLContext的方式,如果需要用到多线程共享一个OpenGL ES环境的话,需要自己实现这个方法处理。
public EGLContext createContext(EGL10 egl, EGLDisplay display, EGLConfig config) {
    int[] attrib_list = {EGL_CONTEXT_CLIENT_VERSION, mEGLContextClientVersion,
            EGL10.EGL_NONE };
    return egl.eglCreateContext(display, config, EGL10.EGL_NO_CONTEXT,
            mEGLContextClientVersion != 0 ? attrib_list : null);
}

OpenGL ES环境的初始化就完成了,但是我们知道应该还有一个EGL的Surface需要创建。

后续的代码就是处理 EGL的Surface创建 的业务:

//我们要记得这里将createEglSurface和createGlInterface都设置为true了
if (mHaveEglContext && !mHaveEglSurface) {
    mHaveEglSurface = true;
    createEglSurface = true;
    createGlInterface = true;
    sizeChanged = true;
}
//由上面代码可知 mHaveEglSurface == true
if (mHaveEglSurface) {
    //mSizeChanged是在SurfaceView回调surfaceChanged会设置会true
    //首次初始化mSizeChanged默认为true
    if (mSizeChanged) {
        sizeChanged = true;
        //更新宽高
        w = mWidth;
        h = mHeight;
        mWantRenderNotification = true;
        createEglSurface = true;
        mSizeChanged = false;
    }
}

我们需要关注这个变量createEglSurface,在下面有一段代码:

if (createEglSurface) {
    //这里主要逻辑就是 mEglHelper.createSurface()
    if (mEglHelper.createSurface()) {
        synchronized(sGLThreadManager) {
            mFinishedCreatingEglSurface = true;
        }
    } else {
        synchronized(sGLThreadManager) {
            mFinishedCreatingEglSurface = true;
            mSurfaceIsBad = true;
        }
        continue;
    }
    createEglSurface = false;
}

代码又借助了EglHelper类,调用了其的createSurface()方法:

public boolean createSurface() {
    //检测环境,由前面可知,这个都是有值的
    if (mEgl == null) {
        throw new RuntimeException("egl not initialized");
    }
    if (mEglDisplay == null) {
        throw new RuntimeException("eglDisplay not initialized");
    }
    if (mEglConfig == null) {
        throw new RuntimeException("mEglConfig not initialized");
    }
    //如果已经创建过EglSurface,这里先销毁掉呗
    destroySurfaceImp();
    GLSurfaceView view = mGLSurfaceViewWeakRef.get();
    if (view != null) {
        //调用mEGLWindowSurfaceFactory工程创建一个EglSurface
        //如果我们没有指定,会默认触发DefaultWindowSurfaceFactory的逻辑
        mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,
                mEglDisplay, mEglConfig, view.getHolder());
    } 
    //检测创建是否成功
    if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
        return false;
    }
    //将EglContext上下文加载到当前线程环境
    if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
        return false;
    }
    return true;
}

可以看看DefaultWindowSurfaceFactorycreateWindowSurface()是怎么创建EglSurface的:

public EGLSurface createWindowSurface(EGL10 egl, EGLDisplay display,
                EGLConfig config, Object nativeWindow) {
    EGLSurface result = null;
    try {
        result = egl.eglCreateWindowSurface(display, config, nativeWindow, null);
    } catch (IllegalArgumentException e) {
    }
    return result;
}

也就是说我们可以通过自己实现createWindowSurface()方法实现我们需要的EGLSurface创建方式。

还有一个destroySurfaceImp(),也顺便瞧一瞧呗:

private void destroySurfaceImp() {
    if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
        //清理当前线程的EglContext上下文环境
        mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
                EGL10.EGL_NO_SURFACE,
                EGL10.EGL_NO_CONTEXT);
        GLSurfaceView view = mGLSurfaceViewWeakRef.get();
        if (view != null) {
            //我们需要销毁前面创建出来的EGLSurface
            view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
        }
        mEglSurface = null;
    }
}

DefaultWindowSurfaceFactory的逻辑看完了。

回归上面创建流程,紧接着可以看到createGlInterface这个标示量的代码逻辑块:

if (createGlInterface) {
    gl = (GL10) mEglHelper.createGL();
    createGlInterface = false;
}

继续跟进mEglHelper.createGL()实现:

GL createGL() {
    //获得OpenGL ES的编程接口
    GL gl = mEglContext.getGL();
    GLSurfaceView view = mGLSurfaceViewWeakRef.get();
    if (view != null) {
        //如果我们没调用setGLWrapper(GLWrapper glWrapper)这个接口,view.mGLWrapper == null
        if (view.mGLWrapper != null) {
            gl = view.mGLWrapper.wrap(gl);
        }
    return gl;
}

然后可以关注下createEglContext这个标示量的代码块:

if (createEglContext) {
    GLSurfaceView view = mGLSurfaceViewWeakRef.get();
    if (view != null) {
        try {
            view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
        } finally {
        }
    }
    createEglContext = false;
}

这个回调到上层自定义的Renderer的onSurfaceCreated(),说明GLSurfaceView的GL环境已经准备完毕了。

顺便看看size变化的通知是怎么实现的:

if (sizeChanged) { //在前面我们可以指定当surfaceview回调宽高变更时,sizeChanged会为true
    GLSurfaceView view = mGLSurfaceViewWeakRef.get();
    if (view != null) {
        try {
            view.mRenderer.onSurfaceChanged(gl, w, h);
        } finally {}
    }
    sizeChanged = false;
}

这个回调到上层自定义的Renderer的onSurfaceChanged(),标识GLSurfaceView的宽高已经发生改变。

上面整个流程已经将GLSurfaceView的EGL环境准备完毕了,接下来我们看看是怎么触发渲染的。

渲染业务

还是在guardedRun()这个函数中,有这么一段代码:

{
    GLSurfaceView view = mGLSurfaceViewWeakRef.get();
    if (view != null) {
        try {
            view.mRenderer.onDrawFrame(gl);
        } finally {}
    }
}
int swapError = mEglHelper.swap();

如果逻辑能走到这里,就能触发到上层Renderer的onDrawFrame()实现,也就是说就能开始OpenGL ES的绘制工作。

我们还记得开头说过,GLSurfaceView有两种刷新模式:RENDERMODE_WHEN_DIRTYRENDERMODE_CONTINUOUSLY
那么下面我们就来分析一下这两种是怎么进行工作的。

首先我们先看看RENDERMODE_CONTINUOUSLY循环刷新模式。
先看看设置入口,嗯没啥重要逻辑。

public void setRenderMode(int renderMode) {
    synchronized(sGLThreadManager) {
        mRenderMode = renderMode;
    }
}

搜索一下代码,我们发现mRenderMode在这个readyToDraw()函数使用到:

private boolean readyToDraw() {
    //需要非暂停状态、EGLSurface已经创建、并宽高不为0
    return (!mPaused) && mHasSurface && (!mSurfaceIsBad)
        && (mWidth > 0) && (mHeight > 0)
        //这个就需要仔细看,如果我们设置RENDERMODE_CONTINUOUSLY的话 这条件是成立的!
        && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY));
}

接着我们看看哪里用到这个readyToDraw()函数,在guardedRun()中:

while (true) {
    if (readyToDraw()) {
        if (! mHaveEglContext) {
            //......
        }

        if (mHaveEglContext && !mHaveEglSurface) {
            //......
        }

        if (mHaveEglSurface) {
            //......
            //触发一次后重置为false。
            mRequestRender = false;
            //......
            break;
        }
    }
    //......
    sGLThreadManager.wait();
}
//......
GLSurfaceView view = mGLSurfaceViewWeakRef.get();
if (view != null) {
    try {
        view.mRenderer.onDrawFrame(gl);
    } finally {}
}

可以得知,如果readyToDraw()返回true的话,逻辑会走到break退出当前while循环。
自然而然会调用到我们上层OpenGL ES的渲染实现。

如果mRenderMode == RENDERMODE_CONTINUOUSLY的话,那么readyToDraw()在成功初始化后就是true。
也就是在子线程运行时间里会不断while循环调用view.mRenderer.onDrawFrame()这个回调。

那么当mRenderMode的值为RENDERMODE_WHEN_DIRTY时,由于mRequestRender默认为false。
那么readyToDraw()返回了false,然后逻辑走到了sGLThreadManager.wait()导致线程阻塞。

很明显,我们需要看看mRequestRender这个变量被赋值的位置,也就是函数requestRender()中:

public void requestRender() {
    synchronized(sGLThreadManager) {
        //重点这里,将mRequestRender设置为true,也就是说接下来readyToDraw()返回true
        mRequestRender = true;
        //通知guardedRun()里面的sGLThreadManager.wait()解除阻塞继续运行
        sGLThreadManager.notifyAll();
    }
}

可以得出结论,在RenderMode的值为RENDERMODE_WHEN_DIRTY时,我们调用一次requestRender()的话,
相对应的就能触发一次onDrawFrame()进行OpenGL的渲染。

关于初始化和渲染触发的源码流程分析,就是上面这些了,剩下来的是一些小功能的源码简单解释。

暂停与恢复

一般来说,GLSurfaceView需要跟随Activity或者Fragment的生命周期调用对应的
onPause()onResume()方法,代码最终会调用到GLThead类的相关方法:
请求暂停相关:

public void onPause() {
    synchronized (sGLThreadManager) {
        //将要暂停的状态标示量
        mRequestPaused = true;
        //通知GL线程解除阻塞
        sGLThreadManager.notifyAll();
        //这里是为了保证onPause()调用完毕后,GL线程已经是Paused状态了
        while ((! mExited) && (! mPaused)) {
            try {
                sGLThreadManager.wait();
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

解除暂停相关:

public void onResume() {
    synchronized (sGLThreadManager) {
        //解除暂停的状态标示量
        mRequestPaused = false;
        //顺便请求重绘一次GL
        mRequestRender = true;
        mRenderComplete = false;
        sGLThreadManager.notifyAll();
        //这里是为了保证onResume()调用完毕后,GL线程已经是Resume状态了
        while ((! mExited) && mPaused && (!mRenderComplete)) {
            try {
                sGLThreadManager.wait();
            } catch (InterruptedException ex) {
                Thread.currentThread().interrupt();
            }
        }
    }
}

我们看看这个状态是怎么工作的,还是在guardedRun()这个函数里面:

boolean pausing = false;
//如果mPaused与mRequestPaused,肯定是上层修改了mRequestPaused
if (mPaused != mRequestPaused) {
    pausing = mRequestPaused;
    mPaused = mRequestPaused;
    sGLThreadManager.notifyAll();
}
if (pausing && mHaveEglSurface) {
    //销毁了EGL Surface,并将mHaveEglSurface设置false
    stopEglSurfaceLocked();
}
if (pausing && mHaveEglContext) {
    //销毁了EGL Context,并将mHaveEglContext设置false
    GLSurfaceView view = mGLSurfaceViewWeakRef.get();
    boolean preserveEglContextOnPause = view == null ?
            false : view.mPreserveEGLContextOnPause;
    if (!preserveEglContextOnPause) {
        stopEglContextLocked();
    }
}

顺便看看释放了什么东西:
stopEglSurfaceLocked()最终会回调到destroySurfaceImp()函数。

private void destroySurfaceImp() {
    //将当前线程跟EGL环境解除绑定
    if (mEglSurface != null && mEglSurface != EGL10.EGL_NO_SURFACE) {
        mEgl.eglMakeCurrent(mEglDisplay, EGL10.EGL_NO_SURFACE,
                EGL10.EGL_NO_SURFACE,
                EGL10.EGL_NO_CONTEXT);
        GLSurfaceView view = mGLSurfaceViewWeakRef.get();
        if (view != null) {
            //调用EGL Surface的销毁逻辑
            view.mEGLWindowSurfaceFactory.destroySurface(mEgl, mEglDisplay, mEglSurface);
        }
        mEglSurface = null;
    }
}

stopEglContextLocked()最终会回调到EglHelperfinish()函数。

public void finish() {
    if (mEglContext != null) {
        GLSurfaceView view = mGLSurfaceViewWeakRef.get();
        if (view != null) {
            //调用到Context销毁的工厂方法
            view.mEGLContextFactory.destroyContext(mEgl, mEglDisplay, mEglContext);
        }
        mEglContext = null;
    }
    if (mEglDisplay != null) {
        //销毁掉EglDisplay
        mEgl.eglTerminate(mEglDisplay);
        mEglDisplay = null;
    }
}

接着,我们怎么知道暂停状态,不然逻辑会跑到渲染回调那边,但是会在哪里阻塞住呢?看回到readyToDraw():

private boolean readyToDraw() {
    return (!mPaused) && mHasSurface && (!mSurfaceIsBad)
        && (mWidth > 0) && (mHeight > 0)
        && (mRequestRender || (mRenderMode == RENDERMODE_CONTINUOUSLY));
}

因为mPaused的值是true,所以这里返回false,由上面可知逻辑将会阻塞住等待唤醒。

关于onResume()就简单了,就是一次重新的EGL环境初始化过程,这里不做详细分析。

同步与控制

关于GLThreadManager这个类,其主要提供的是线程同步控制的功能,因为在GLSurfaceView里面存在两个线程,分别是GL线程和调用线程。
所以我们需要引入synchronized来保证逻辑的一致性,涉及状态量变动例如mShouldExit、mRequestPaused、mRequestRender必须用synchronized
还有一个主要是提供waitnotifyAll的功能,进行逻辑阻塞等待唤醒。

外部调用GL线程

由于操作OpenGL环境只能通过GL线程,所以GLSurfaceView帮我们提供了queueEvent(Runnable r)这个方法:

public void queueEvent(Runnable r) {
    synchronized(sGLThreadManager) {
        mEventQueue.add(r);
        sGLThreadManager.notifyAll();
    }
}

如果我们queueEvent了一个Runnable,也就会在guardedRun()里面触发到这个逻辑:

while(true) {
    //......
    if (! mEventQueue.isEmpty()) {
        event = mEventQueue.remove(0);
        break;
    }
    //......
}
if (event != null) {
    event.run();
    event = null;
    continue;
}

也就是在GL线程回调了我们传入的Runnable的run方法。

释放销毁

最后我们来简单看一下怎么销毁释放整个GLSurfaceView,我们可以定位到onDetachedFromWindow(),其会将mShouldExit设置为true。
然后返回guardedRun()可以看到进行return退出线程逻辑:

if (mShouldExit) {
    return;
}

但是我们需要关注到这块逻辑:

finally {
    synchronized (sGLThreadManager) {
        stopEglSurfaceLocked();
        stopEglContextLocked();
    }
}

这里将EGL相关的Context、Surface这些进行释放,并且退出了GL线程的循环。

结语

GLSurfaceView不仅帮我们简化了OpenGL ES的使用流程,而且帮我们提供了一个使用EGL的开发范例,了解下源码的流程对于我们开发GL还是大有裨益的。

本文同步发布于简书CSDN

End!

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

推荐阅读更多精彩内容