JNI的探索

JNI的概念

  • 定义
    JNI是Java Native Interface的缩写,通过使用 Java本地接口书写程序,可以确保代码在不同的平台上方便移植
  • 原理


    Jni原理图.png

    java平台.png
  • 开发工具
    1、vs2015
    2、eclipse(或者Android studio)
    3、java环境

JNI的调用过程

  • 步骤:
    1.编写带有native声明的方法的java类
    2.编译生成class文件
    3.利用javah生成(.h)的头文件 命令:javah 类名, 注:不需要class后缀
    4.将(.h)头文件复制到vs下,创建(.cpp)或者(.c)文件实现(.h)头文件声明的方法
    5.实现完成后,编译成dll库
    6.将dll复制到java项目的根目录,调用System.loadLibrary("dll库名"); //注:不要dll后缀
    7.在代码里面调用native方法,访问native(.cpp 或者 .c)的代码

  • 栗子:
    1、java上的native定义

//定义
public native static String getStringFromCPP();
//调用
public static void main(String[] args) {
    System.out.println(getStringFromCPP());
}

2、利用javac或者编译器直接编译,生成class文件
3、利用jdk下的javah 生成(.h)的头文件


生成(.h)文件.png

4、将生成的头文件放置到vs新建的项目,如下; 还需要将JDK目录下的include 目录下的jni.h 和 jni_md.h文件copy到项目
因为生成的JniMain.h文件需要依赖到这两个文件,同时将JniMain.h中的 #include <jni.h> 改成 #include "jni.h"

JNI的vs项目结构.png

5、创建C++或者C文件实现JniMain的方法, 这边创建JniDemo.cpp, 引入头文件
具体代码:

#include "stdafx.h"
#include "JniMain.h"
#include <string.h>

/*
* Class:     JniMain
* Method:    getStringFromCPP
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP
(JNIEnv *env, jclass jclaz) {
return env->NewStringUTF("java static method call C++ back string");
}

6、打包成dll
这边vs项目创建的是win32的项目,所以需要配置成dll
在项目右键 ->属性


配置类型改为Dll.png

由于个人的环境是64位的,所以配置管理,需要修改为x64位


配置管理.png

配置管理器调整平台为x64.png

生成dll


生成dll.png

7、将dll 复制到java项目工程的根目录,并加载dll库, 运营程序

public class JniMain {

    //静态方法
    public native static String getStringFromCPP();

    static{
        System.loadLibrary("Jni");
    }

    public static void main(String[] args) {
        System.out.println(getStringFromCPP());
    }
}

结果:


结果.png

调用的分析

JNI的数据类型

  • JNI基本数据类型:
java C/C++
boolean jboolean
byte jbyte
char jchar
short jshort
int jint
long jlong
float jfloat
double jdouble
  • 引用类型:
java C/C++
String jstring
Object jobject
  • 基本数据类型数组:
    //type[] jTypeArray;
    byte[] jByteArray;

  • 引用类型数组
    Object jobjectArray;

JNI对应的java属性与方法签名

在jni调用中,返回值和参数,以及静态字段和实例字段,有对应着相应的签名,如下表格:
这些签名的时候在接下的实例讲解中会用到;
简而言之,在jni中涉及到类型的使用(包括基本类和引用类型)


Java属性与方法签名列表.png
  • 方法签名例子:
    方法1:
public string addTail(String tail, int index)

其对应的签名如下:

(Ljava/util/String;I)Ljava/util/String;

方法2:

public int addValue(int index, String value,int[] arr)

其对应的签名如下:

(ILjava/util/String;[I)I
  • javap命令查看class文件中对应jni的签名
    命令:javap -s -p class文件的路径


    javap命令.png

native修饰的静态方法

  • java代码:
public native static String getStringFromCPP();
  • C++ 代码:
/*
* Class:     JniMain
* Method:    getStringFromCPP
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP
(JNIEnv *env, jclass jclaz) {
    return env->NewStringUTF("java static method call C++ back string");
}
  • 说明:
    java中定义native方法是静态的话,生成的接口方法的参数就是(JNIEnv *env, jclass jclaz)
    JNIEnv: 是jni接口调用的api指针
    jclass: 表示的就是native修饰的java静态方法所在的类

native修饰的非静态方法

  • java代码:
public native String getStringFromCPP2();
  • C++代码:
/*
* Class:     JniMain
* Method:    getStringFromCPP2
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_getStringFromCPP2
(JNIEnv *env, jobject job) {
    return env->NewStringUTF("java non-static method call C++ back string ");
}
  • 说明:
    java中定义native方法是非静态的话,生成的接口方法的参数是(JNIEnv *env, jobject job)
    JNIEnv: 是jni接口调用的api指针
    jobject: 表示的就是native修饰的java非静态方法所在类的对象

访问java类中的成员变量

  • java代码:
public String key = "key";
public native void accessField(); //该native方法用于调用c++的接口访问java变量
  • C++代码:
/*
* Class:     JniMain
* Method:    accessField
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniMain_accessField
(JNIEnv *env, jobject job) {
    //先获取对应的java类
    jclass jclaz = env->GetObjectClass(job);

    //第二个参数对应的是java的变量名,第三个是类型的签名
    jfieldID fid = env->GetFieldID(jclaz, "key", "Ljava/lang/String;");

    char text[100] = "Jni change string field value ";

    if (fid == NULL) { // 如果字段为 NULL ,直接退出,查找失败
        return;
    }

    // 获取字段对应的值
    jstring jstr = (jstring)env->GetObjectField(job, fid); 
    const char * str = env->GetStringUTFChars(jstr, NULL);

    strcat(text, str);

    jstring key = env->NewStringUTF(text);
    //修改key的值
    env->SetObjectField(job, fid, key);

}
  • 说明:类似java的反射,步骤如下:
    1、获取 Java 对象的类
    2、获取对应字段的 id
    3、获取具体的字段值

访问java类中的静态变量

  • java代码:
public static int count = 9;
public native void accessStaticField();  //该native方法用于调用c++的接口访问java静态变量
  • C++代码:
/*
* Class:     JniMain
* Method:    accessStaticField
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniMain_accessStaticField
(JNIEnv *env, jobject job) {
    //获取类名
    jclass jclaz = env->GetObjectClass(job);

    //获取静态字段  第一个参数是类,第二个参数对应的是java的变量名,第三个是类型的签名
    jfieldID fid = env->GetStaticFieldID(jclaz, "count", "I");

    if (fid == NULL) {
        return;
    }

    // 获取字段对应的值
    jint count = env->GetStaticIntField(jclaz, fid);

    count = 20;
    // 修改字段的值
    env->SetStaticIntField(jclaz, fid, count);
}
  • 说明:静态字段的访问类似实例字段的访问,步骤相同

jni中调用java某个对象的方法

  • java代码:
public class Animal {

    protected String name;
    public static int num = 0;
    public Animal(String name) {
        this.name = name;
    }
    //jni访问的非静态方法
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
    public int getNum() {
        return num;
    }

    public static String getUID(String id) {
        return "10001"+id;
    }
}
//另一个类的native方法
public native void callInstanceMethod(Animal animal);
  • C++代码:
/*
* Class:     JniMain
* Method:    callInstanceMethod
* Signature: (LAnimal;)V
*/
JNIEXPORT void JNICALL Java_JniMain_callInstanceMethod
(JNIEnv *env, jobject instance, jobject animal) {

    // 获得具体的类
    jclass cls = env->GetObjectClass(animal);
    // 获得具体的方法 id
    jmethodID mid = env->GetMethodID(cls, "setName", "(Ljava/lang/String;)V");

    if (mid == NULL) {
        return;
    }
    //设置要穿的参数
    jstring name = env->NewStringUTF("baozi");
    //调用java的方法
    env->CallVoidMethod(animal, mid, name);
}
  • 说明:
    与访问字段不同的是,GetFieldID 方法换成了 GetMethodID 方法,另外由 CallVoidMethod 函数来调用具体的方法,前面两个参数是获得的类和方法 id,最后的参数是具体调用方法的参数(签名)。方法签名 = (参数类型额签名) + 返回值类型的签名
    GetMethodID 方法的第一个参数就是具体的 Java 类型,第二个参数是该 Java 类的对应实例方法的名称,第三个参数就是该方法对应的返回类型和参数签名转换成 Native 对应的描述。
    对于不需要返回值的函数,调用 CallVoidMethod 即可,对于返回值为引用类型的,调用 CallObjectMethod 方法,对于返回基础类型的方法,则有各自对应的方法调用,比如:CallBooleanMethod、CallShortMethod、CallDoubleMethod 等等

jni中调用java类的某个静态方法

  • java代码:
public class Animal {

    protected String name;
    public static int num = 0;
    public Animal(String name) {
        this.name = name;
    }
    //jni访问的非静态方法
    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
    public int getNum() {
        return num;
    }
    //jni访问的静态方法
    public static String getUID(String id) {
        return "10001"+id;
    }
}
//另一个类的native方法
public native String callStaticMethod(Animal animal);
  • C++代码:
/*
* Class:     JniMain
* Method:    callStaticMethod
* Signature: (LAnimal;)Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_callStaticMethod
(JNIEnv *env, jobject instance, jobject animal) {

    //获取具体的类
    jclass jclz = env->FindClass("Animal");//参数也累的路径名

    // 获取具体的静态方法的 id
    jmethodID mid = env->GetStaticMethodID(jclz, "getUID", "(Ljava/lang/String;)Ljava/lang/String;");
    if (mid == NULL) {
        return env->NewStringUTF("method no found!");
    }
    jstring id = env->NewStringUTF("xxxxxx");
    jstring result = (jstring)env->CallStaticObjectMethod(jclz, mid, id);
    return result;
}
  • 说明:
    与访问非静态不同的是,GetMethodID 方法换成了 GetStaticMethodID 方法,另外由 CallStaticObjectMethod 函数来调用具体的方法,前面两个参数是获得的类和方法 id,最后的参数是具体调用方法的参数(签名)。
    GetStaticMethodID 方法的第一个参数就是具体的 Java 类型,第二个参数是该 Java 类的对应实例方法的名称,第三个参数就是该方法对应的返回类型和参数签名转换成 Native 对应的描述。对于不需要返回值的函数,调用 CallStaticVoidMethod 即可,对于返回值为引用类型的,调用 CallStaticObjectMethod 方法,对于返回基础类型的方法,则有各自对应的方法调用,比如:CallStaticBooleanMethod、CallStaticShortMethod、CallStaticDoubleMethod 等等

调用java类的构造方法

  • java代码:
//访问构造方法
public native Date acceessConstructor();
  • C++ 代码:
/*
* Class:     JniMain
* Method:    acceessConstructor
* Signature: (LAnimal;)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL Java_JniMain_acceessConstructor
(JNIEnv *env, jobject job) {
    //通过类的路径来从JVM里面找到对应的类
    jclass jclz = env->FindClass("java/util/Date");
    //jmethodId  构造方法
    jmethodID jmid = env->GetMethodID(jclz, "<init>","()V");

    if (jmid == NULL) {
        return NULL;
    }

    // 调用newObject 实例化Date 对象,返回值是一个jobject
    jobject date_obj = env->NewObject(jclz, jmid);

    // 得到对应的对象方法,前提是,我们访问了相关对象的构造函数创建了这个对象
    jmethodID time_mid = env->GetMethodID(jclz, "getTime","()J");
    jlong time = env->CallLongMethod(date_obj, time_mid);

    printf("time: %lld \n", time);
    return date_obj;
}
  • 说明
    调用java类的构造方法,等于在C++里面创建一个java对象,然后进行调用;同样也是采用GetMethodID的方法进行获取构造函数的id,然后由NewObject 进行对象的创建

JNI数组的使用

  • java代码:
//整型数据在C++中进行排序
public native void giveArray(int[] inArray);
public native int[][] initInt2DArray(int size);
public native String[] initStringArray(int size);
//调用
int[] array = {3,9,2,50,6,13};
jniMain.giveArray(array);
for(int i=0; i< array.length; i++) {
    System.out.println(array[i]);
}

String[] strArr = jniMain.initStringArray(5);
for (int i = 0; i < strArr.length; i++) {
    System.out.println("strArr["+i+"] = "+strArr[i]);
}

int[][] intArr = jniMain.initInt2DArray(4);
for(int i =0; i < 4; i++) {
    for(int j = 0; j < 3; j ++) {
        System.out.println("arr["+i+"]["+j+"] = "+intArr[i][j]);
    }
}
  • C++代码:
/*
* Class:     JniMain
* Method:    giveArray
* Signature: ([I)V
*/
JNIEXPORT void JNICALL Java_JniMain_giveArray
(JNIEnv *env, jobject job, jintArray array) {
    //jintArray -> jint *
    jint *elemts = env->GetIntArrayElements(array, NULL);
    if (elemts == NULL)
    {
        return;
    }
    //数组长度
    int len = env->GetArrayLength(array);
    qsort(elemts, len, sizeof(jint), compare);
    //释放可能的内存
    //将JNI  修改的数据重新写回原来的内存
    env->ReleaseIntArrayElements(array, elemts, JNI_COMMIT);
}

/*
* Class:     JniMain
* Method:    initInt2DArray
* Signature: (I)[[I
*/
JNIEXPORT jobjectArray JNICALL Java_JniMain_initStringArray
(JNIEnv *env, jobject job, jint size) {
    //创建jobjectArray对象
    jobjectArray result;
    jclass jclz;
    int i;
    jclz = env->FindClass("java/lang/String");
    if (jclz == NULL) {
        return NULL;
    }
    result = env->NewObjectArray(size,jclz, job);
    if (result == NULL) {
    return NULL;
    }
    //赋值
    for ( i = 0; i < size; i++)
    {
        char * c_str = (char *)malloc(256);
        memset(c_str, 0, 256);
        //将 int 转换成为 char
        sprintf(c_str, "hello num: %d\n",i);
        // C -> jstring
        jstring str = env->NewStringUTF(c_str);
        if (str == NULL) {
            return NULL;
        }

        // 将jstring 赋值给数组
        env->SetObjectArrayElement(result, i, str);
        free(c_str);
        c_str = NULL;
    }

    return result;
}

/*
* Class:     JniMain
* Method:    initInt2DArray
* Signature: (I)[[I
*/
JNIEXPORT jobjectArray JNICALL Java_JniMain_initInt2DArray
(JNIEnv *env, jobject job, jint size) {
    //返回对象,是个二维数组
    jobjectArray ret;
    int i = 0;
    int j = 0;
    jclass intArrayClz = env->FindClass("[I");
    if (intArrayClz == NULL) {
       return NULL;
    }

    ret = env->NewObjectArray(size * 3, intArrayClz, NULL);

    jint tmp[3];//固定数组
    for ( i = 0; i < size; i++)
    {
        jintArray intArr = env->NewIntArray(3);
        for ( j = 0; j < 3; j++)
        {
            tmp[j] = i + j;
        }

        env->SetIntArrayRegion(intArr, 0, 3, tmp);
        //将一维数组的值复制到
        env->SetObjectArrayElement(ret, i, intArr);
        env->DeleteLocalRef(intArr);
    }
    return ret;
}
  • 说明:
    jobjectArray 表示二维数组
    env->SetObjectArrayElement(ret, i, intArr); 二维数组的赋值
    env->NewObjectArray(size * 3, intArrayClz, NULL); 二维数组的创建

处理中文字符串的乱码问题

由于java的字符串编码,和C或者C++的字符串编码不一样,所以在java传中文到C/C++会出现乱码的现象


字符编码切换.png

解决方法,就是在C/C++直接调用java的String来处理

  • java代码:
//定义
public native String chineseChars2(String str);

//调用
System.out.println(jniMain.chineseChars2("宝宝22"));
  • C++代码:

/*
* Class:     JniMain
* Method:    chineseChars2
* Signature: (LAnimal;)Ljava/lang/Object;
*/
JNIEXPORT jobject JNICALL Java_JniMain_chineseChars2
(JNIEnv *env, jobject job, jstring in) {

     char * c_str = "马蓉与宝宝";
    //创建String的类
    jclass jclz = env->FindClass("java/lang/String");
    //获取构造函数的mid   这边使用的是java的String(byte[], string)的构造函数
    jmethodID mid = env->GetMethodID(jclz, "<init>", "([BLjava/lang/String;)V");

    //创建参数 jstring -> jbyteArray
    jbyteArray bytes = env->NewByteArray(strlen(c_str));

    // char * 赋值到byte数组中
    env->SetByteArrayRegion(bytes, 0, strlen(c_str), (jbyte*)c_str);
    // 设置编码
    jstring charsetName = env->NewStringUTF("GB2312");

    return env->NewObject(jclz, mid, bytes, charsetName);
}

JNI中 局部引用

  • java代码:
public native void localRef();
  • C++代码:
/*
* Class:     JniMain
* Method:    localRef   局部引用
* Signature: ()V
*/
JNIEXPORT void JNICALL Java_JniMain_localRef
(JNIEnv *env, jobject job) {
    int i;
    for ( i = 0; i < 5; i++)
    {
        jclass clz = env->FindClass("java/util/Date");
        jmethodID mid = env->GetMethodID(clz, "<init>", "()V");
        //创建对象
        jobject date_obj = env->NewObject(clz, mid);
        //使用这个引用
        jmethodID time_mid = env->GetMethodID(clz, "getTime", "()J");
        jlong time = env->CallLongMethod(date_obj, time_mid);

        printf("local reference time: %lld \n", time);

        //释放引用
        env->DeleteLocalRef(clz);
        env->DeleteLocalRef(date_obj);
    }
}
  • 说明:
    局部引用通过NewLocalRef和各种JNI接口创建(FindClass、NewObject、GetObjectClass和NewCharArray等)。会阻止GC回收所引用的对象,不在本地函数中跨函数使用,不能跨线前使用。函数返回后局部引用所引用的对象会被JVM自动释放,或调用DeleteLocalRef释放

JNI中 全局引用

  • java代码:
public native void createGlobalRef();
public native String getglobalRef();
public native void delGlobalRef();
  • C++代码:
/*
* Class:     JniMain
* Method:    createGlobalRef  创建全局引用
* Signature: ()V
*/
jstring global_str;
JNIEXPORT void JNICALL Java_JniMain_createGlobalRef
(JNIEnv *env, jobject job) {
    jstring str = env->NewStringUTF("JNI is intersting");
    global_str = (jstring)env->NewGlobalRef(str);
}
//全局引用
//跨线程,跨方法使用
// NewGlobalRef 是创建全局引用的唯一方法

/*
* Class:     JniMain
* Method:    getglobalRef
* Signature: ()Ljava/lang/String;
*/
JNIEXPORT jstring JNICALL Java_JniMain_getglobalRef
(JNIEnv *env, jobject job) {
    return global_str;
}

/*
* Class:     JniMain
* Method:    delGlobalRef  移除全局引用
* Signature: ()V;
*/
JNIEXPORT void JNICALL Java_JniMain_delGlobalRef
(JNIEnv *env, jobject job) {
    env->DeleteGlobalRef(global_str);
}
  • 说明
    全局引用,变量是定义在方法外,调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,必须调用DeleteGlobalRef手动释放env->DeleteGlobalRef(g_cls_string);

JNI中 弱全局引用

  • java代码:
public native String createWeakRef();
  • C++代码:
/*
* Class:     JniMain
* Method:    createWeakRef
* Signature: ()Ljava/lang/String;
*/
jstring g_weak_cls;
JNIEXPORT jstring JNICALL Java_JniMain_createWeakRef
(JNIEnv *env, jobject job) {
    jclass cls_string = env->FindClass("java/lang/String");
    g_weak_cls = (jstring)env->NewWeakGlobalRef(cls_string);
    g_weak_cls = env->NewStringUTF("Jni weak reference");
    printf("weak ref = %s \n ",g_weak_cls);
    return g_weak_cls;
}
  • 说明:
    弱全局引用:调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用DeleteWeakGlobalRef手动释放。env->DeleteWeakGlobalRef(g_cls_string)

JNI中 异常处理

  • java代码:
public native void exception();
  • C++代码:
/*
* Class:     JniMain
* Method:    exception   异常处理
* Signature: ()V;
*/
JNIEXPORT void JNICALL Java_JniMain_exception
(JNIEnv *env, jobject job) {
    jclass cls = env->GetObjectClass(job);
    jfieldID fid = env->GetFieldID(cls, "key11", "Ljava/lang/String;");

    //检查是否发送异常
    jthrowable ex = env->ExceptionOccurred();
    // 判断异常是否发送
    if (ex != NULL) {
        jclass newExc;
        //清空JNI 产生的异常
        env->ExceptionClear();
        //NullPointerException
        newExc = env->FindClass("java/lang/IllegalArgumentException");
    if (newExc == NULL)
    {
        printf("exception\n");
        return;
    }
    env->ThrowNew(newExc, "Throw exception from JNI: GetFieldID faild ");
    }
}
  • 说明:
    native调用java中的方法,java中的方法抛出异常,我们在native中检测异常,检测到后抛出native中的异常,并清理异常。
    函数介绍:
    1> ExceptionCheck:检查是否发生了异常,若有异常返回JNI_TRUE,否则返回JNI_FALSE
    2> ExceptionOccurred:检查是否发生了异常,若用异常返回该异常的引用,否则返回NULL
    3> ExceptionDescribe:打印异常的堆栈信息
    4> ExceptionClear:清除异常堆栈信息
    5> ThrowNew:在当前线程触发一个异常,并自定义输出异常信息
    6> Throw:丢弃一个现有的异常对象,在当前线程触发一个新的异常
    7> FatalError:致命异常,用于输出一个异常信息,并终止当前VM实例(即退出程序)

jni的静态注册和动态注册

参考:https://blog.csdn.net/qq_20404903/article/details/80662316

结语

以上就是个人对jni的认知和总结,如有错误的地方,欢迎大家指出,该篇文件比较适合于对JNI的入门的同学
项目代码:https://github.com/jasonkevin88/JniDemo

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