Android-JNI解析

目录

JNI概述

MediaRecorder框架中的JNI

Java Framework层的MediaRecorder

JNI层的MediaRecorder

Native方法注册

数据类型的转换

方法签名

解析JNIEnv

参考《Android进阶解密》

JNI概述

JNI(Java Native Interface,Java本地接口),是Java与其他语言通信的桥梁。这不是Android系统所独有的,而是Java所有,当出现一些用Java语言无法处理的任务时,就可以使用JNI技术来实现。

JNI不只是应用于Android开发,它有着非常广泛的应用场景。JNI在Android中的应用主要有:音视频开发、热修复、插件话、逆向开发、系统源码调用等等。为了方便使用JNI技术,Android提供了NDK这个工具集合,NDK开发是基于JNI的,它和JNI开发本质上并没有区别,理解JNI原理,NDK开发也会很容易掌握。

Android系统按语言来划分的话分为两个层面:分别是Java层Native层。通过JNIJava层可以访问Native层,同样的Native层也可以访问Java层。下面以MediaRecorder框架中的JNI举例来理解系统中的JNI

MediaRecorder框架中的JNI

MediaRecorder是Android系统提供给我们用于录音和录像的框架。Java Framework层对应的是MediaRecorder.java,也就是我们平时开发在应用中直接调用的类。JNI层对应的是libmedia_jni.so,它是JNI的一个动态库。Native层对应的是libmedia.so,这个动态库完成来实际的功能。

Java Framework层的MediaRecorder

我们先看一下MediaRecorder.java的源码:

frameworks/base/media/java/android/media/MediaRecorder.java

public class MediaRecorder{
    static {
        //加载名字为media_jni的动态库
        System.loadLibrary("media_jni");
        native_init();
    }
    ......
    //JNI注册
    private static native final void native_init();
    ......
    public native void start() throws IllegalStateException;
    ......
}

上述代码指截取部分JNI相关的代码:

  • 在静态代码块中首先加载名字为media_jni的动态库,也就是libmedia_jni.so
  • 然后接着调用了native_init ()方法,该方法会调用Native层方法,用来完成JNI的注册。
  • start()方法也是一个Native方法。

对于Java Framework层来说只需要加载对应的JNI库,接着声明native方法就可以了,剩下的工作由JNI层来完成。

JNI层的MediaRecorder

MediaRecorderJNI层是由android_media_MediaRecorder.cpp实现,native方法:native_initstart的代码实现如下:

frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;
    clazz = env->FindClass("android/media/MediaRecorder");
    if (clazz == NULL) {
        return;
    }
    ......
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");
    if (fields.post_event == NULL) {
        return;
    }
}

static void
android_media_MediaRecorder_start(JNIEnv *env, jobject thiz)
{
    ALOGV("start");
    sp<MediaRecorder> mr = getMediaRecorder(env, thiz);
    if (mr == NULL) {
        jniThrowException(env, "java/lang/IllegalStateException", NULL);
        return;
    }
    process_media_recorder_call(env, mr->start(), "java/lang/RuntimeException", "start failed.");
}

android_media_MediaRecorder_native_init方法native_init方法JNI层的实现;android_media_MediaRecorder_start方法start方法JNI层的实现。那么它们是如何找到对应的方法的呢?下面我们首先了解一下JNI方法注册的知识。

Native方法注册

Native方法注册分为动态注册静态注册,其中静态注册多用于NDK开发,而动态注册多用于Framework开发。下面分别来看一下这两种注册方式。

静态注册

在Android Studio中新建一个Java Library,命名为media,仿照系统的MediaRecorder.java,代码如下:


public class MediaRecorder {
    static{
        System.loadLibrary("media_jni");
        native_init();
    }
    private static native final void native_init();
    public native void start() throws IllegalStateException;
}

编写完成后,对MediaRecorder.java进行编译和生成JNI方法:进入项目的media/src/main/java目录中,执行以下命令:

javac com/example/media/MediaRecorder.java //编译
javah com.example.media.MediaRecorder //生成头文件

第二个命令会生成com_example_media_MediaRecorder.h文件,内容如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class com_example_media_MediaRecorder */
#ifndef _Included_com_example_media_MediaRecorder
#define _Included_com_example_media_MediaRecorder
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     com_example_media_MediaRecorder
 * Method:    native_init
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_media_MediaRecorder_native_1init
  (JNIEnv *, jclass);
/*
 * Class:     com_example_media_MediaRecorder
 * Method:    start
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_com_example_media_MediaRecorder_start
  (JNIEnv *, jobject);

#ifdef __cplusplus
}
#endif
#endif

在Java中的native_init()方法被声明为Java_com_example_media_MediaRecorder_native_1init方法,以“Java”开头说明是在Java平台中调用JNI方法的,后面的com_example_media_MediaRecorder_native_1init指的是包名 + 类名 + 方法名的格式。我们会发现还多了一个1 ,这是因为Java中的native_init方法包含了"_",转换成JNI方法后变成了“_1”

此外方法还多了几个参数:

  • JNIEnv:是Native层中Java环境的代表,通过该类型的指针就可以在Native层中访问Java层的代码,它只在创建它的线程中有效,不能跨线程传递。
  • jclass:是JNI的属性类型,对应Java的java.lang.Class实例。
  • jobject:同样也是JNI属性类型,对应Java的Object。

当我们在Java中调用native_init()方法时,就会从JNI中寻找Java_com_example_media_MediaRecorder_native_1init函数,如果没有就会报错,如果有就会为native_initJava_com_example_media_MediaRecorder_native_1init建立关联,其实就是报错JNI函数指针。这样再次调用的时候直接使用这个函数指针就可以了。

静态注册就是根据方法名,将Java方法和JNI函数建立关联,这样会有一些缺点:

  • JNI层函数名过长。
  • 声明native方法的类需要用javah生成头文件。
  • 初次调用native方法时需要建立关联,影响效率。

动态注册

JNI中有一种结构用来记录Java的native方法和JNI方法的关联关系,它就是JNINativeMethod,它在jni.h中被定义:

typedef struct {
    const char* name;//Java方法名字
    const char* signature;//Java方法的签名
    void*       fnPtr;//JNI中对应方法的指针
} JNINativeMethod;

系统的MediaRecorder采用的是动态注册,下面看一下它的JNI层是怎么做的:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

//JNINativeMethod类型的数组,数组名字为gMethods
static const JNINativeMethod gMethods[] = {
    ......
    {"start",         "()V", (void *)android_media_MediaRecorder_start},
    {"stop",          "()V", (void *)android_media_MediaRecorder_stop},
    {"pause",         "()V", (void *)android_media_MediaRecorder_pause},
    {"resume",        "()V", (void *)android_media_MediaRecorder_resume},
    {"native_reset",  "()V", (void *)android_media_MediaRecorder_native_reset},
    {"release",       "()V", (void *)android_media_MediaRecorder_release},
    {"native_init",   "()V", (void *)android_media_MediaRecorder_native_init},
    ......
};

上面定义了一个JNINativeMethod类型的数组,数组的名字是gMethods,里面存储的是native方法于JNI层函数的对应关系。只定义是没有用的,还需要注册它,注册的函数为:register_android_media_MediaRecorder:

// This function only registers the native methods, and is called from
// JNI_OnLoad in android_media_MediaPlayer.cpp
int register_android_media_MediaRecorder(JNIEnv *env)
{
    return AndroidRuntime::registerNativeMethods(env,
                "android/media/MediaRecorder", gMethods, NELEM(gMethods));
}

通过该方法的注释我们知道该方法是在JNI_OnLoad函数中调用的。这个函数会在System.loadLibrary函数后调用,因为多媒体框架中很多框架都要进行JNINativeMethod类型的数组注册,因此函数注册被统一定义在android_media_MediaPlayer.cppJNI_OnLoad函数中,该函数的代码如下:
frameworks/base/media/jni/android_media_MediaPlayer.cpp

jint JNI_OnLoad(JavaVM* vm, void* /* reserved */)
{
    JNIEnv* env = NULL;
    jint result = -1;

    if (vm->GetEnv((void**) &env, JNI_VERSION_1_4) != JNI_OK) {
        ALOGE("ERROR: GetEnv failed\n");
        goto bail;
    }
    assert(env != NULL);
    ......
    if (register_android_media_MediaPlayer(env) < 0) {
        ALOGE("ERROR: MediaPlayer native registration failed\n");
        goto bail;
    }

    if (register_android_media_MediaRecorder(env) < 0) {
        ALOGE("ERROR: MediaRecorder native registration failed\n");
        goto bail;
    }
    ......
    /* success -- return valid version number */
    result = JNI_VERSION_1_4;

bail:
    return result;
}

register_android_media_MediaRecorder方法中返回了AndroidRuntime::registerNativeMethods函数,该函数的代码如下:
frameworks/base/core/jni/AndroidRuntime.cpp

/*static*/ int AndroidRuntime::registerNativeMethods(JNIEnv* env,
    const char* className, const JNINativeMethod* gMethods, int numMethods)
{
    return jniRegisterNativeMethods(env, className, gMethods, numMethods);
}

在该方法中又返回了jniRegisterNativeMethods函数,该函数被定义在JNI的帮助类JNIHelp.cpp中:
libnativehelper/JNIHelp.cpp

extern "C" int jniRegisterNativeMethods(C_JNIEnv* env, const char* className,
    const JNINativeMethod* gMethods, int numMethods)
{
    ......
    if ((*env)->RegisterNatives(e, c.get(), gMethods, numMethods) < 0) {
        char* tmp;
        const char* msg;
        if (asprintf(&tmp, "RegisterNatives failed for '%s'; aborting...", className) == -1) {
            // Allocation failed, print default warning.
            msg = "RegisterNatives failed; aborting...";
        } else {
            msg = tmp;
        }
        e->FatalError(msg);
    }

    return 0;
}

从上面代码可以看出最终调用的是JNIEnvRegisterNatives函数来完成JNI注册的。
动态注册要比静态注册复杂一些。但是它解决来静态注册的缺点,可以说一劳永逸。

动态注册是直接存储Java的native方法与它对应的JNI中的函数指针。

数据类型的转换

Java层的数据类型到来JNI层就需要转换为JNI层的数据类型。Java的数据类型分为基本数据类型引用数据类型,JNI层对于这两种数据类型也做来区分,下面就分别来看一下。

基本数据类型的转换

Java Native Signature
byte jbyte B
char jchar C
double jdouble D
float jfloat F
int jint I
short jshort S
long jlong J
boolean jboolean Z
void void V

除了最后一个void,其他的数据类型只需要在前面加上“j”就可以了。Signature表示的是签名格式。

引用数据类型转换

Java Native Signature
Object jobject L + classname + ;
Class jclass Ljava/lang/Class;
String jstring Ljava/lang/String;
Throwable jthrowable Ljava/lang/Throwable;
object[] jobjectArray [L + classname + ;
byte[] jbyteArray [B
char[] jcharArray [C
double[] jdoubleArray [D
float[] jfloatArray [F
int[] jintArray [I
short[] jshortArray [S
long[] jlongArray [J
boolean[] jbooleanArray [Z
继承关系

下面以MediaRecorder为例看一下类型的转换:
frameworks/base/media/java/android/media/MediaRecorder.java

private native void _setOutputFile(FileDescriptor fd, long offset, long length)
    throws IllegalStateException, IOException;

_setOutputFile方法对应的JNI层的方法为:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_setOutputFileFD(JNIEnv *env, jobject thiz, jobject fileDescriptor, jlong offset, jlong length)
{
    ......
}

对比以上两个方法可以看到** FileDescriptor被转换成了 jobject long被转换成了 jlong**。

方法签名

方法签名是由签名格式组成的,上面在介绍数据类型转换的时候每种数据类型都给出了对应的签名格式。那么方法签名有什么用呢?我们先看一下方法签名是什么样子的:
``

static const JNINativeMethod gMethods[] = {
    ......
    {"native_init",  "()V", (void *)android_media_MediaRecorder_native_init},
    {"native_setup", "(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V",
    ......
};

其中“()V”和"(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V"就是方法签名。Java中的方法是有重载的,可以定义同名的方法,但参数不同。正因为如此,在JNI中通过方法名是无法找到Java中对应的具体方法的,JNI为了解决这一问题就将参数类型和返回值类型组合在一起作为方法签名。通过方法签名和方法名就可以找到对应的Java方法。
JNI方法签名的格式为:

(参数1签名格式参数2签名格式...)返回值签名格式

native_setup函数为例,它在Java中的定义如下:

private native final void native_setup(Object mediarecorder_this,
            String clientName, String opPackageName) throws IllegalStateException;

它在JNI中的方法签名为:

(Ljava/lang/Object;Ljava/lang/String;Ljava/lang/String;)V

native_setup函数的第一个参数的签名为:"Ljava/lang/Object;",第2和第3个参数的签名为:“Ljava/lang/String;”,返回值的签名格式为:“V”。通过参数的签名格式我们可以找到java中对应的参数类型。

通过Java提供的javap命令可以自动生成方法签名。

解析JNIEnv

JNIEnvNative层Java环境的代表,通过JNIEnv *指针就可以在Native层中访问Java层中的代码。它只在创建它的线程中有效,不能跨线程传递,因此不同线程的JNIEnv是彼此独立的。
JNIEnv的主要作用:

  • 调用Java的方法。
  • 操作Java中的变量和对象等。

JNIEnv的定义如下:
libnativehelper/include/nativehelper/jni.h

#if defined(__cplusplus)
//C++中JNIEnv类型
typedef _JNIEnv JNIEnv;
typedef _JavaVM JavaVM;
#else
//C语言中JNIEnv类型
typedef const struct JNINativeInterface* JNIEnv;
typedef const struct JNIInvokeInterface* JavaVM;
#endif

这里使用预定义宏__cplusplus来区分C语言和C++两种代码,如果定义了__cplusplus(编译的是C++源文件),就使用C++代码中的定义,否则就是C语言的定义。JavaVM:它是虚拟机在JNI层的代表,在一个虚拟机进程中只有一个JavaVM,因此,该进程的所有线程都共享这个JavaVM。通过JavaVMAttachCurrentThread函数可以获取这个线程的JNIEnv,这样就可以在不同的线程中调用Java方法了。

在使用AttachCurrentThread函数的线程退出前,务必要调用DetachCurrentThread函数来释放资源。
在C++中JNIEnv的类型是_JNIEnv,下面我们看一下它是如何定义的:
libnativehelper/include/nativehelper/jni.h

struct _JNIEnv {
    const struct JNINativeInterface* functions;
    #if defined(__cplusplus)
    ......
     jclass FindClass(const char* name)
    { return functions->FindClass(this, name); }
    ......
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }
    ......
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    { return functions->GetFieldID(this, clazz, name, sig); }
    ......
}

从上面代码可以看到_JNIEnv是一个结构体,其中内部又包含了JNINativeInterface。在_JNIEnv中定义了很多的函数。这里只贴出了比较常用的3个函数,FindClass函数用来找到Java中指定名称的类,GetMethodID函数用来获取Java中的方法,GetFieldID函数用来获取Java中的成员变量。这3个函数都调用了JNINativeInterface中定义的函数,因此可以看出,无论是C语言还是C++,JNIEnv的类型都和JNINativeInterface有关系,下面看一下它的定义:
libnativehelper/include/nativehelper/jni.h

struct JNINativeInterface {
    ......
    jclass    (*FindClass)(JNIEnv*, const char*);
    ......
    jmethodID (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    ......
    jfieldID  (*GetFieldID)(JNIEnv*, jclass, const char*, const char*);
    ......
}

JNINativeInterface同样也是一个结构体,在它里面定义了很多和JNIEnv结构体对应的函数指针。通过这些函数指针的定义,就能够定义到虚拟机中的JNI函数表,从而实现了JNI层在虚拟机中的函数调用,这样JNI就可以调用Java层的方法了。

在C语法中,JNIEnv是一个结构体指针:struct JNINativeInterface* JNIEnv

jfieldID和jmethodID

_JNIEnv结构体中定义了很多的函数,这些函数都会有不同的返回值,如下所示:

struct _JNIEnv {
    const struct JNINativeInterface* functions;
    #if defined(__cplusplus)
    ......
    jmethodID GetMethodID(jclass clazz, const char* name, const char* sig)
    { return functions->GetMethodID(this, clazz, name, sig); }
    ......
    jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    { return functions->GetFieldID(this, clazz, name, sig); }
    ......
}

这个两个函数的返回值分别为:jmethodIDjfieldID,分别用来代表Java类中的方法和成员变量。jclass代表Java类,name:代表方法名或者成员变量的名字,sig:代表这个方法或者成员变量的签名。接下来我们看一下这两个函数在MediaRecorder框架中的使用:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

static void
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;
    clazz = env->FindClass("android/media/MediaRecorder");
    if (clazz == NULL) {
        return;
    }
    fields.context = env->GetFieldID(clazz, "mNativeContext", "J");
    if (fields.context == NULL) {
        return;
    }
    fields.surface = env->GetFieldID(clazz, "mSurface", "Landroid/view/Surface;");
    if (fields.surface == NULL) {
        return;
    }
    jclass surface = env->FindClass("android/view/Surface");
    if (surface == NULL) {
        return;
    }
    fields.post_event = env->GetStaticMethodID(clazz, "postEventFromNative",
                                               "(Ljava/lang/Object;IIILjava/lang/Object;)V");
    if (fields.post_event == NULL) {
        return;
    }
}

在上述函数的开始处,通过FindClass函数来找到Java层的MediaRecorder的Class对象,并赋值给jclass类型的变量clazz,所以,clazz就是Java层MediaRecorderJNI层的代表。紧接着找到Java层的MediaRecorder中名字为mNativeContextmSurface的成员变量,并分别赋值给fieldscontextsurface,最后找到名字为postEventFromNative的静态方法,并赋值给fieldspost_event。其中fields的定义如下:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

struct fields_t {
    jfieldID    context;
    jfieldID    surface;
    jmethodID   post_event;
};
static fields_t fields;

android_media_MediaRecorder_native_init函数中将Java层中的成员变量和方法赋值给了jfieldIDjmethodID保存起来,这样不用每次调用的时候都去查询。下面看一下它们是如何使用的:
frameworks/base/media/jni/android_media_MediaRecorder.cpp

void JNIMediaRecorderListener::notify(int msg, int ext1, int ext2)
{
    ALOGV("JNIMediaRecorderListener::notify");
    JNIEnv *env = AndroidRuntime::getJNIEnv();
    env->CallStaticVoidMethod(mClass, fields.post_event, mObject, msg, ext1, ext2, NULL);
}

调用CallStaticVoidMethod函数时传入的参数就包含了fields.post_event,该参数代表的是Java层MediaRecorder的静态方法postEventFromNative,下面看一下该方法的实现:
frameworks/base/media/java/android/media/MediaRecorder.java

private static void postEventFromNative(Object mediarecorder_ref,
                                        int what, int arg1, int arg2, Object obj){
    MediaRecorder mr = (MediaRecorder)((WeakReference)mediarecorder_ref).get();
    if (mr == null) {
        return;
    }
    if (mr.mEventHandler != null) {
        Message m = mr.mEventHandler.obtainMessage(what, arg1, arg2, obj);
        mr.mEventHandler.sendMessage(m);
    }
}

在该方法中创建了一个消息,然后通过mEventHandler来发送处理,这样就会切换到应用程序的主线程中。该方法是通过JNIEnvCallStaticVoidMethod函数来调用的,也就是说通过它可以访问Java层的静态方法,同理,通过CallVoidMethod函数可以访问Java层的非静态方法。

引用类型

和Java的引用类型一样,JNI也有引用类型,它们分别是本地引用(Local References)、全局引用(Global References)和弱全局引用(Weak Global References)

本地引用

JNIEnv提供的函数所返回的引用基本上都是本地引用,因此本地引用也是JNI中最常见的引用类型。本地引用的特点主要有以下几点:

  • 当native函数返回时,这个本地引用就会被自动释放。
  • 只在创建它的线程有效,不能跨线程使用。
  • 局部引用是JVM负责的引用类型,受JVM管理。
    下面通过一个示例来说明:
    frameworks/base/media/jni/android_media_MediaRecorder.cpp
android_media_MediaRecorder_native_init(JNIEnv *env)
{
    jclass clazz;
    clazz = env->FindClass("android/media/MediaRecorder");
    if (clazz == NULL) {
        return;
    }
    ......
}

FindClass函数返回的clazz就是本地引用,它会在android_media_MediaRecorder_native_init函数调用返回后自动释放,我们也可以调用JNIEnvDeleteLocalRef函数来手动删除本地引用,该函数的应用场景主要是在native函数返回之前占用了大量内存,需要手动删除本地引用。

全局引用

全局引用和本地引用几乎是相反的,它主要有以下几个特点:

  • 在native函数返回时不会被自动释放,因此全局引用需要手动进行释放,并且不会被GC回收。
  • 全局引用是可以跨线程使用的。
  • 全局引用不受JVM管理。
    JNIEnvNewGlobalRef函数用来创建全局引用,调用DeleteLocalRef函数来释放全局引用。
    下面通过一个示例来看一下全局引用的使用:
    ``
JNIMediaRecorderListener::JNIMediaRecorderListener(JNIEnv* env, jobject thiz, jobject weak_thiz)
{
    jclass clazz = env->GetObjectClass(thiz);
    if (clazz == NULL) {
        ALOGE("Can't find android/media/MediaRecorder");
        jniThrowException(env, "java/lang/Exception", NULL);
        return;
    }
    mClass = (jclass)env->NewGlobalRef(clazz);
    mObject  = env->NewGlobalRef(weak_thiz);
}

clazz是本地引用,在下面通过NewGlobalRef函数将它变成了全局引用mClass,该全局引用是在JNIMediaRecorderListener析构函数中释放,这里就不贴出源码了。

弱全局引用

弱全局引用是一种特殊的全局引用,它和全局引用的特点相似,不同的是弱全局引用是可以被GC回收的,被回收后会指向NULL。通过JNINewWeakGlobalRef函数来创建弱全局引用,调用DeleteWeakGlobalRef函数来释放弱全局引用,由于它可能被GC回收,因此在使用之前要先判断它是否被回收了,通过IsSameObject函数来判断。

Kotlin实战

Flutter实战

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