OpenGL升级打怪之 GLSurfaceView源码分析

一、背景

Android对OpenGL这块封装是非常好的,也是非常隐蔽的,一般使用者直接使用GLSurfaceView即可达到需求。最近项目中将很多功能下层到c++层,这样必须对OpenGL 底层逻辑有所了解。Android虽然提供OpenGL 各个版本的So库,但是并没有对底层api做封装,所以如果是自己想用C++写OpenGL,最好的方式学习Android源码。

二、GLSurfaceView如何使用

在分析GLSurfaceView源码之前我们非常有必要介绍一下GLSurfaceView的使用方法:

 surfaceView = findViewById(R.id.triangle_api_surfaceView)
        surfaceView.setEGLContextClientVersion(3)
        surfaceView.setRenderer(object : GLSurfaceView.Renderer {
            /**
             * Called when the surface is created or recreated.
             *
             *
             * Called when the rendering thread
             * starts and whenever the EGL context is lost. The EGL context will typically
             * be lost when the Android device awakes after going to sleep.
             *
             *
             * Since this method is called at the beginning of rendering, as well as
             * every time the EGL context is lost, this method is a convenient place to put
             * code to create resources that need to be created when the rendering
             * starts, and that need to be recreated when the EGL context is lost.
             * Textures are an example of a resource that you might want to create
             * here.
             *
             *
             * Note that when the EGL context is lost, all OpenGL resources associated
             * with that context will be automatically deleted. You do not need to call
             * the corresponding "glDelete" methods such as glDeleteTextures to
             * manually delete these lost resources.
             *
             *
             * @param gl the GL interface. Use `instanceof` to
             * test if the interface supports GL11 or higher interfaces.
             * @param config the EGLConfig of the created surface. Can be used
             * to create matching pbuffers.
             */
            override fun onSurfaceCreated(gl: GL10?, config: EGLConfig?) {
                TODO ("surface被创建后需要做的处理,包括顶点、纹理数据的处理")
            }

            /**
             * Called when the surface changed size.
             *
             *
             * Called after the surface is created and whenever
             * the OpenGL ES surface size changes.
             *
             *
             * Typically you will set your viewport here. If your camera
             * is fixed then you could also set your projection matrix here:
             * <pre class="prettyprint">
             * void onSurfaceChanged(GL10 gl, int width, int height) {
             * gl.glViewport(0, 0, width, height);
             * // for a fixed camera, set the projection too
             * float ratio = (float) width / height;
             * gl.glMatrixMode(GL10.GL_PROJECTION);
             * gl.glLoadIdentity();
             * gl.glFrustumf(-ratio, ratio, -1, 1, 1, 10);
             * }
            </pre> *
             * @param gl the GL interface. Use `instanceof` to
             * test if the interface supports GL11 or higher interfaces.
             * @param width
             * @param height
             */
            override fun onSurfaceChanged(gl: GL10?, width: Int, height: Int) {
                TODO ("渲染窗口大小发生改变的处理,在视频播放是可以用来调整分辨率变化的视图窗口,或者3D动画中坐标系转换等")
            }

            /**
             * Called to draw the current frame.
             *
             *
             * This method is responsible for drawing the current frame.
             *
             *
             * The implementation of this method typically looks like this:
             * <pre class="prettyprint">
             * void onDrawFrame(GL10 gl) {
             * gl.glClear(GL10.GL_COLOR_BUFFER_BIT | GL10.GL_DEPTH_BUFFER_BIT);
             * //... other gl calls to render the scene ...
             * }
            </pre> *
             * @param gl the GL interface. Use `instanceof` to
             * test if the interface supports GL11 or higher interfaces.
             */
            override fun onDrawFrame(gl: GL10?) {
                TODO("加载顶点、纹理数据并执行渲染工作")
            }
        })
        surfaceView.renderMode = GLSurfaceView.RENDERMODE_CONTINUOUSLY

可以看到使用GLSurfaceView非常简单,只要APP开发者实现以下三个方法:

  • onSurfaceCreated(gl: GL10?, config: EGLConfig?)
  • onSurfaceChanged(gl: GL10?, width: Int, height: Int)
  • onDrawFrame(gl: GL10?)

这三个方法注释分别其作用,这里就不多赘述。下篇文章会写如何使用GLSurfaceView渲染。
这里需要注意的是:这三个方法都是在GL线程,OpenGL内部渲染线程。 OpenGL的核心逻辑都在GL线程中。

三、源码分析

入口方法

我们先从setRenderer入手
它主要做了两件事分别是:
1、检查环境和变量同步配置:

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

checkRenderThreadState()检查了GLThread是否存在,存在则会抛异常,也就是不能在同一个GLSurfaceView多次调用setRenderer(Renderer renderer)方法,会crash。则就限制了一个渲染业务只能在一个GLThread中,如果需要在同一个线程中做个渲染业务,则需要学习更高级的功能比如FBO,多目标渲染等。mEGLConfigChoosermEGLContextFactorymEGLWindowSurfaceFactory是用户在setRenderer之前,可以调用相关方法来进行EGL设置,如果没有设置则采用默认实现。
mEGLConfigChooser用于指定OpenGL颜色、深度、模版等设置。
mEGLContextFactory用于提供EGLContext创建和销毁的处理。
mEGLWindowSurfaceFactory用于提供EGLSurface创建和销毁的处理。
2、启动一个GL线程:

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

这里就是鼎鼎大名的GL线程。主要是用于和OpenGL API环境的交互以及渲染的上下文切换和异常场景的处理。入参mThisWeakRef是一个弱引用,指向了GLSurfaceView本身。

GLThread线程 --- OpenGL的核心逻辑

我们先看下GLThread的实现,先看下run方法:

public void run() {
            setName("GLThread " + getId());
            if (LOG_THREADS) {
                Log.i("GLThread", "starting tid=" + getId());
            }

            try {
                guardedRun();
            } catch (InterruptedException e) {
                // fall thru and exit normally
            } finally {
                sGLThreadManager.threadExiting(this);
            }
        }

继续追踪代码到guardedRun(),这个方法很长,是一个死循环,我们这里就把关键的几个方法列出来,重点说明下这些方法是做什么和其调用顺序

private void guardedRun() throws InterruptedException {
    mEglHelper = new EglHelper(mGLSurfaceViewWeakRef);
    mHaveEglContext = false;
    mHaveEglSurface = false;
    mWantRenderNotification = false;
    try {
        ...
        while (true) {
            synchronized (sGLThreadManager) {
                while (true) {
                    //用于暂停、推出等状态恢复
                    ...
                    if (readyToDraw()) {
                        ...
                        //创建EGLContext上下文
                        mEglHelper.start();
                        ...
                    }
                    ...                    
               } 
               ...
               //创建EGLSurface,本质是申请一块内存
               if (mEglHelper.createSurface()) {
                    ...
                }
                ...
                //获取GL对象,包装OpenGL API环境,这里使用GL10
                gl = (GL10) mEglHelper.createGL();
                ...
                //回调外部Renderer对象的`onSurfaceCreated()`方法
                GLSurfaceView view = mGLSurfaceViewWeakRef.get();
                view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
                ...
                if (sizeChanged) {
                    ...
                    //回调外部Renderer对象的`onSurfaceChanged()`方法
                    view.mRenderer.onSurfaceChanged(gl, w, h);
                    ...
                }
               ...
               //回调外部Renderer对象的`onDrawFrame()`方法
               view.mRenderer.onDrawFrame(gl);
               ...
               //Egl交互内存,opengl使用的双内存缓冲,一个进行显示,另一个则后台进行绘制,绘制OK后,交互内存进行显示
               int swapError = mEglHelper.swap();
               ...
        }
    } finally {
                /*
                 * clean-up everything...
                 */
                synchronized (sGLThreadManager) {
                    stopEglSurfaceLocked();
                    stopEglContextLocked();
                }
   }
}

在方法开头创建了一个EglHelper,EglHelper是一个封装了一些EGL通用操作的工具类。
1、 先看下readyToDraw()方法:

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

mPaused用于设置是否已暂停。mHasSurface用于标记是否Surface是否已经创建,这里的Surface区别于EGLSurface,后面我会深入挖掘两者的差异。mSurfaceIsBad用于标记EGLSurface是否可用,mWidthmHeight则是Surface的宽和高。mRequestRender标记用户是否请求刷新。mRenderMode是指刷新模式,GLSurfaceView有两种刷新模式:

    /**
     * The renderer only renders
     * when the surface is created, or when {@link #requestRender} is called.
     *
     * @see #getRenderMode()
     * @see #setRenderMode(int)
     * @see #requestRender()
     */
    public final static int RENDERMODE_WHEN_DIRTY = 0;
    /**
     * The renderer is called
     * continuously to re-render the scene.
     *
     * @see #getRenderMode()
     * @see #setRenderMode(int)
     */
    public final static int RENDERMODE_CONTINUOUSLY = 1;

RENDERMODE_WHEN_DIRTY字面意思理解就是当画面脏模式,这种模式只有在Surface创建和当Renderer调用时才会渲染
RENDERMODE_CONTINUOUSLY字面理解是持续模式,这种模式会一直持续不断渲染
回到readyToDraw(),当不在暂停状态,切换Surface和EGLSurface都是正常状态时,既可以渲染。

2、那接下来就会执行mEglHelper.start()看下其内部代码:

 public void start() {
            if (LOG_EGL) {
                Log.w("EglHelper", "start() tid=" + Thread.currentThread().getId());
            }
            /*
             * Get an EGL instance
             */
            mEgl = (EGL10) EGLContext.getEGL();

            /*
             * Get to the default display.
             */
            mEglDisplay = mEgl.eglGetDisplay(EGL10.EGL_DEFAULT_DISPLAY);

            if (mEglDisplay == EGL10.EGL_NO_DISPLAY) {
                throw new RuntimeException("eglGetDisplay failed");
            }

            /*
             * We can now initialize EGL for that display
             */
            int[] version = new int[2];
            if(!mEgl.eglInitialize(mEglDisplay, version)) {
                throw new RuntimeException("eglInitialize failed");
            }
            GLSurfaceView view = mGLSurfaceViewWeakRef.get();
            if (view == null) {
                mEglConfig = null;
                mEglContext = null;
            } else {
                mEglConfig = view.mEGLConfigChooser.chooseConfig(mEgl, mEglDisplay);

                /*
                * Create an EGL context. We want to do this as rarely as we can, because an
                * EGL context is a somewhat heavy object.
                */
                mEglContext = view.mEGLContextFactory.createContext(mEgl, mEglDisplay, mEglConfig);
            }
            if (mEglContext == null || mEglContext == EGL10.EGL_NO_CONTEXT) {
                mEglContext = null;
                throwEglException("createContext");
            }
            if (LOG_EGL) {
                Log.w("EglHelper", "createContext " + mEglContext + " tid=" + Thread.currentThread().getId());
            }

            mEglSurface = null;
        }

EGLContext是用来链接渲染和视图窗口的上下文,上述代码的目的就是生成mEglContext。我们可以看下mEGLContextFactorycreateContext()实现, 如果外部没有设置mEGLContextFactory,则其会使用默认DefaultContextFactory,我们可以看下DefaultContextFactory的实现

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);
}

最终调到egleglCreateContext()方法,这是EGL10的的API,如果继续追代码,发现最终会调到Native的API。后面有机会搞份Android系统源码可以看下Framework层实现。

3、 接着看下mEglHelper.createSurface()代码

  public boolean createSurface() {
            if (LOG_EGL) {
                Log.w("EglHelper", "createSurface()  tid=" + Thread.currentThread().getId());
            }
            /*
             * Check preconditions.
             */
            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");
            }

            /*
             *  The window size has changed, so we need to create a new
             *  surface.
             */
            destroySurfaceImp();

            /*
             * Create an EGL surface we can render into.
             */
            GLSurfaceView view = mGLSurfaceViewWeakRef.get();
            if (view != null) {
                mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,
                        mEglDisplay, mEglConfig, view.getHolder());
            } else {
                mEglSurface = null;
            }

            if (mEglSurface == null || mEglSurface == EGL10.EGL_NO_SURFACE) {
                int error = mEgl.eglGetError();
                if (error == EGL10.EGL_BAD_NATIVE_WINDOW) {
                    Log.e("EglHelper", "createWindowSurface returned EGL_BAD_NATIVE_WINDOW.");
                }
                return false;
            }

            /*
             * Before we can issue GL commands, we need to make sure
             * the context is current and bound to a surface.
             */
            if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
                /*
                 * Could not make the context current, probably because the underlying
                 * SurfaceView surface has been destroyed.
                 */
                logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
                return false;
            }

            return true;
}

createSurface()主要做两件事:

a) 生成EGLSurface


 mEglSurface = view.mEGLWindowSurfaceFactory.createWindowSurface(mEgl,
                        mEglDisplay, mEglConfig, view.getHolder());

同理,如果没有设置mEGLWindowSurfaceFactory,默认使用DefaultWindowSurfaceFactory看下其实现,调用到eglCreateWindowSurface()方法


 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) {
                // This exception indicates that the surface flinger surface
                // is not valid. This can happen if the surface flinger surface has
                // been torn down, but the application has not yet been
                // notified via SurfaceHolder.Callback.surfaceDestroyed.
                // In theory the application should be notified first,
                // but in practice sometimes it is not. See b/4588890
                Log.e(TAG, "eglCreateWindowSurface", e);
            }
            return result;
        }

b) 将EGLSurface和前面生成的EGLContext进行绑定

if (!mEgl.eglMakeCurrent(mEglDisplay, mEglSurface, mEglSurface, mEglContext)) {
                /*
                 * Could not make the context current, probably because the underlying
                 * SurfaceView surface has been destroyed.
                 */
                logEglErrorAsWarning("EGLHelper", "eglMakeCurrent", mEgl.eglGetError());
                return false;
            }

这一步执行完后,所有的渲染前环境准备已经完毕。接下来主角Renderer开始干活了。
接下来是Renderer三步走:

   //回调外部Renderer对象的`onSurfaceCreated()`方法
                GLSurfaceView view = mGLSurfaceViewWeakRef.get();
                view.mRenderer.onSurfaceCreated(gl, mEglHelper.mEglConfig);
                ...
                if (sizeChanged) {
                    ...
                    //回调外部Renderer对象的`onSurfaceChanged()`方法
                    view.mRenderer.onSurfaceChanged(gl, w, h);
                    ...
                }
               ...
               //回调外部Renderer对象的`onDrawFrame()`方法
               view.mRenderer.onDrawFrame(gl);

执行到这一步其实渲染数据也准备好了,要想把Renderer的数据显示出来,必须调用mEglHelper.swap();

4、看下mEglHelper.swap()的代码
SurfaceView使用双内存缓冲机制,内部有两个Frambuffer,一个进行前台显示,另一个则后台进行绘制,绘制OK后,交互内存进行显示。eglSwapBuffers的目的是渲染好的FrameBuffer和前台的Framebuffer交互后显示出来

     /**
         * Display the current render surface.
         * @return the EGL error code from eglSwapBuffers.
         */
        public int swap() {
            if (! mEgl.eglSwapBuffers(mEglDisplay, mEglSurface)) {
                return mEgl.eglGetError();
            }
            return EGL10.EGL_SUCCESS;
        }

总结一下:

  • 所有的渲染逻辑必须在同一个线程,EGLContext、EGLSurface是和线程绑定。
  • Render执行渲染必须在EGLContext和EGLSurface生成并绑定后才能进行。
  • Render渲染后的数据要想在视图窗口显示,必须调用eglSwapBuffers交换Framebuffer

GLThread如何暂停和恢复

1、GLThread如何实现暂停
在GLThread的onPause()方法中,会将mRequestPaused = true并通知所有线程。

mRequestPaused = true;
sGLThreadManager.notifyAll();

我们看下mRequestPaused还有哪里用到?
在GLThread的guardedRun()方法中可以看到mRequestPaused被使用,当mPaused != mRequestPaused会将局部变量pausing赋值。我个人理解将这mRequestPaused赋值给局部变量,是因为这是一个死循环线程,赋值完可以释放mRequestPaused变量,而且暂停时需要销毁EGLContext和EGLSurface耗时。

boolean pausing = false;
if (mPaused != mRequestPaused) {
    pausing = mRequestPaused;
    mPaused = mRequestPaused;
    sGLThreadManager.notifyAll();
    if (LOG_PAUSE_RESUME) {
        Log.i("GLThread", "mPaused is now " + mPaused + " tid=" + getId());
    }
}

pausing = true时,会release EGLSurface和EGLContext, 释放后mHaveEglSurfacemHaveEglContext会被置false, 而此时readyToDraw()一直返回false, 代码会在最里面的while循环里面运行无法跳到外面的循环,处于等待的过程中

 // When pausing, release the EGL surface:
if (pausing && mHaveEglSurface) {
    if (LOG_SURFACE) {
        Log.i("GLThread", "releasing EGL surface because paused tid=" + getId());
    }
    stopEglSurfaceLocked();
}

// When pausing, optionally release the EGL Context:
if (pausing && mHaveEglContext) {
    GLSurfaceView view = mGLSurfaceViewWeakRef.get();
    boolean preserveEglContextOnPause = view == null ? false : view.mPreserveEGLContextOnPause;
    if (!preserveEglContextOnPause) {
        stopEglContextLocked();
        if (LOG_SURFACE) {
            Log.i("GLThread", "releasing EGL context because paused tid=" + getId());
        }
    }
}

2、GLThread如何进行恢复
在执行onResume()会将mRequestPaused置成false, mRequestRender置成true,此时readyToDraw()返回true,当mEglHelper.start()返回true后就可以跳出最里面的循环。此时就可以正常执行渲染逻辑。

GLThread如何退出

当时看这个代码时在想一个死循环,如何才能停止呢?看下代码,发现外部并没有直接停止线程的方法,刚开始以为是surfaceDestroyed()被调用时线程就可以停止了,最后测试发现当surfaceDestroyed()执行时,GLThread线程有可能还在渲染,此时Surface已经销毁了,这个时候执行到egl.swapBuffers()时则报0x300D的错误码,也就是EGL_BAD_SURFACE,此时我看到源码中有这段注释,似乎我明白了, 也就是当执行到surfaceDestroyed时,此时Surface已经销毁了,而EGLSurface还存在,并没有来得及销毁。

switch (swapError) {
....
        default:
            // Other errors typically mean that the current surface is bad,
            // probably because the SurfaceView surface has been destroyed,
            // but we haven't been notified yet.
            // Log the error to help developers understand why rendering stopped.
            EglHelper.logEglErrorAsWarning("GLThread", "eglSwapBuffers", swapError);

            synchronized(sGLThreadManager) {
                mSurfaceIsBad = true;
                sGLThreadManager.notifyAll();
            }
            break;
}

那真正停止线程的逻辑在哪呢?
从上述逻辑分析可知,停止线程的逻辑必须在Surface销毁之前通知GLThread线程,进行销毁处理,不然会出现异常。我搜索了很久,终于让我发现一段代码和GLThread线程之间的关系。我们先看下GLThread中可以停止线程的逻辑只有mShouldExit这个变量

while (true) {
    synchronized (sGLThreadManager) {
        while (true) {
            if (mShouldExit) {
                return;
            }
            ...
        }
    }
}

追踪一下代码发现mShouldExitrequestExitAndWait()方法中,

        public void requestExitAndWait() {
            // don't call this from GLThread thread or it is a guaranteed
            // deadlock!
            synchronized(sGLThreadManager) {
                mShouldExit = true;
                sGLThreadManager.notifyAll();
                while (! mExited) {
                    try {
                        sGLThreadManager.wait();
                    } catch (InterruptedException ex) {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        }

继续追踪代码,终于真相大白了,GLThread线程时在onDetachedFromWindow()方法中被中止的。

    @Override
    protected void onDetachedFromWindow() {
        if (LOG_ATTACH_DETACH) {
            Log.d(TAG, "onDetachedFromWindow");
        }
        if (mGLThread != null) {
            mGLThread.requestExitAndWait();
        }
        mDetached = true;
        super.onDetachedFromWindow();
    }

结语:

  1. 从GLSurfaceView源码分析了中学习了GLThread的作用和原理,也模仿GLThread自己在C++调用EGL和OpenGL ES API实现了一遍。
  2. 从代码分析中发现了自己的不足,比如SurfaceView双缓冲机制的具体细节,Surface和EGLSurface在Framework层的关系,接下来有时间会继续深入挖掘底层的实现。

Github代码:https://github.com/JianYeung/BBQVideo

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