JNI 基础 C语言版

静态注册和动态注册

看下基本的jni的demo,我的java代码

package zcwfeng;
import java.*;

public class Register{
   
    static{
        System.load("/Users/zcw/dev/c_workspace/TempC/cmake-build-debug/libfirstlib.dylib");
    }
    public native String HelloWorld();

    public static void main(String[] args){

        Register r = new Register();
        String re =  r.HelloWorld();
        System.out.println(re);

    }
}

多说两句,就是可能引入jni.h 的时候不存在需要到java环境include里面jni头文件拷贝到项目,查找一下jni_md.h 也考过来,搜索一下就可以

我用的clion环境创建的工程
手动生成头文件

zcwfeng_Register.h 头文件,生成的就补贴出来了
zcwfeng_Register.c 实现,这个名字可以和头文件不保持一致,但是这样比较规范

#include "zcwfeng_Register.h"
#include <stdio.h>
JNIEXPORT jstring JNICALL Java_zcwfeng_Register_HelloWorld(JNIEnv *env, jobject jobject1){
    return (*env)->NewStringUTF(env,"Hello David");
}

静态注册

步骤:
1)编写java类,假如是JniTest.java

2)在命令行下输入 javac JniTest.java 生成JniTest.class文件

  1. 在 JniTest.class 目录下 通过 javah xxx.JniTest(全类名)生成 xxx_JniTest.h 头文件

4)编写xxx_JniTest.c 源文件,并拷贝xxx_JniTest.h 下的函数,并实现这些函数,且在其中添加jni.h头文件;

5)编写 cmakelist.txt 文件,编译生成动态/静态链接库

动态注册

在Register.java,加入动态native方法声明
-> 动态注册三个方法声明
native void dynamicFun1();
native String dynamicFun2();
native int getRandom();
-> main 方法加入调用
r.dynamicFun1();
System.out.println(r.dynamicFun2());
System.out.println(r.getRandom());

为了方便,把动态注册的代码提取出来单独的c文件实现,dyReg.c

#include "jni.h"

void func1(JNIEnv *env, jobject jobject) {
    printf("dynamicNative1 动态注册");
}


jstring func2(JNIEnv *env, jobject jobject) {
    printf("dynamicNative2 动态注册");
    return (*env)->NewStringUTF(env, "Hello dynamic native");
}

jint func3(JNIEnv *env, jobject jobject) {
    return 100;
}

static const char * mClassName = "zcwfeng/Register";
static const JNINativeMethod  mMethods[] = {
        {"dynamicFun1","()V",(void *)func1},
        {"dynamicFun2","()Ljava/lang/String;",(jstring *)func2},
        {"getRandom","()I",(jint *)func3}
};

-> 这部分方法参数和名字不用记,去jni.h 拷贝就行,也就是你java调用load 函数会自动调用这里
JNIEXPORT jint JNICALL
JNI_OnLoad(JavaVM *vm, void *reserved) {
    JNIEnv *env = NULL;
    //获得 JniEnv
    int r = (*vm)->GetEnv(vm,(void **) &env, JNI_VERSION_1_4);
    if (r != JNI_OK) {
        return -1;
    }
    jclass mainActivityCls = (*env)->FindClass(env,mClassName); // 注册 如果小于0则注册失败
    r = (*env)->RegisterNatives(env,mainActivityCls, mMethods, 3);
    if (r != JNI_OK) {
        return -1;
    }
    return JNI_VERSION_1_4;
}

完整的java调用代码

package zcwfeng;
import java.*;

public class Register{
    native void dynamicFun1();
    native String dynamicFun2();
    native int getRandom();
    -> 注意一下这里,windows 的库是dll,而mac里面是dylib
    static{
        System.load("/Users/zcw/dev/c_workspace/TempC/cmake-build-debug/libfirstlib.dylib");
    }
    public native String HelloWorld();

    public static void main(String[] args){

        Register r = new Register();
        String re =  r.HelloWorld();
        System.out.println(re);

        r.dynamicFun1();
        System.out.println(r.dynamicFun2());
        System.out.println(r.getRandom());
    }
}

CMakeList 我的配置参考

cmake_minimum_required(VERSION 3.15.3)
project(TempC)

set(CMAKE_CXX_STANDARD 11)

-> 编译生成库,这里注释掉
#add_executable(TempC zcwfeng_Register.c zcwfeng_Register.h dyReg.c src/main.cpp)
add_library(firstlib SHARED zcwfeng_Register.h zcwfeng_Register.c dyReg.c)

System.load 和 System.loadLibrary 的区别

System.load
System.load 参数必须为库文件的绝对路径,可以是任意路径,例如: System.load("C:\Documents and Settings\TestJNI.dll"); //Windows
System.load("/usr/lib/TestJNI.so"); //Linux
System.loadLibrary
System.loadLibrary 参数为库文件名,不包含库文件的扩展名。 System.loadLibrary ("TestJNI"); //加载Windows下的TestJNI.dll本地库 System.loadLibrary ("TestJNI"); //加载Linux下的libTestJNI.so本地库
注意:TestJNI.dll 或 libTestJNI.so 必须是在JVM属性java.library.path所指向的路径中。一般我们老程序员和喜欢是用命令的配置过java环境没问题
-------------------------------------忧伤的分割线------------------------

JNIEnv类型和jobject类型的解释

-> 「这里JNIEXPORT 与 JNICALL 都是JNI的关键字,表示此函数是要被JNI调用的,无需过多解释」

JNIEXPORT void JNICALL Java_com_jni_demo_JNIDemo_sayHello
     (JNIEnv * env, jobject obj) {
     printf(hello);
}

JNIEnv* env参数的解释

JNIEnv类型 实际上代表了Java环境,通过这个JNIEnv* 指针,就可以对Java端的代码进行操作。
例如,创建Java类中 的对象,调用Java对象的方法,获取Java对象中的属性等等。JNIEnv的指针会被JNI传入到本地方法的实现函数中来对 Java端的代码进行操作。如下代码所示

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
......
struct JNIInvokeInterface_;
......
struct JNINativeInterface_ {
    void *reserved0;
    void *reserved1;
    void *reserved2;
    void *reserved3;
    jint (JNICALL *GetVersion)(JNIEnv *env);
//全是函数指针
jclass (JNICALL *DefineClass)
      (JNIEnv *env, const char *name, jobject loader, const jbyte *buf,
       jsize len);
    jclass (JNICALL *FindClass)
      (JNIEnv *env, const char *name);
    jmethodID (JNICALL *FromReflectedMethod)
      (JNIEnv *env, jobject method);
    jfieldID (JNICALL *FromReflectedField)
      (JNIEnv *env, jobject field);
jobject (JNICALL *ToReflectedMethod)
    (JNIEnv *env, jclass cls, jmethodID methodID, jboolean isStatic);
... }

参数:jobject obj的解释

如果native方法不是static的话,这个obj就代表这个native方法的类实例。
如果native方法是static的话,这个obj就代表这个native方法的类的class对象实例(static方法不需要类实例的,所 以就代表这个类的class对象)。
java

public native void test();
public static native void testStatic();

jni代码.h

->「JNIEnv *, jobject  代表()空参数」

JNIEXPORT void JNICALL Java_Hello_test
  (JNIEnv *, jobject);
JNIEXPORT void JNICALL Java_Hello_testStatic
  (JNIEnv *, jclass);

C/C++代码调用Java代码

在JNI中还有 一个非常重要的内容,那便是在C/C++本地 代码中访问Java端的代码,一个常见的应用就是获取类的属性和调用类的方法,为了在C/C++中表示属性和方法,JNI 在jni.h头文件中定义了jfieldId,jmethodID类型来分别代表Java端的属性和方法。
我们在访问,或者设置Java属性的时候,首先就要先在本地代码取得代表该Java属性的jfieldID,然后才能在本地代码 中进行Java属性操作,同样的,我们需要呼叫Java端的方法时,也是需要取得代表该方法的jmethodID才能进行Java 方法调用。
使用JNIEnv的:GetFieldID/GetMethodID GetStaticFieldID/GetStaticMethodID 来取得相应的jfieldID和jmethodID。下面来具体看一下这几个方法:

GetFieldID(jclass clazz,const char* name,const char* sign) 方法的参数说明:
clazz:这个简单就是这个方法依赖的类对象的class对象 name:这个是这个字段的名称 sign:这个是这个字段的签名(我们知道每个变量,每个方法都是有签名的)

怎么查看类中的字段和方法的签名

使用javap命令:

javap -s -p JniTes.class

java 字段的签名

几个demo里的方法签名,也是常用的
int->()I
String->Ljava/lang/String;
void->()V
int [ ] ->[I
float [ ]-> [F
String [ ] -> [Ljava/lang/String;
int [ ][ ] -> [[I
float[ ][ ] -> [[F

Java层方法                                               JNI函数签名

String test ( )                                              Ljava/lang/String;
int f (int i, Object object)                            (ILjava/lang/Object;)I
void set (byte[ ] bytes)                                ([B)V


对应于Java端的数据类型,我们也可以看一下下面的表: 
Java 类型 类型签名
boolean Z
byte    B
char    C
short   S
int     I
long    L
float   F
double  D
类   L全限定名;,比如String, 其签名为Ljava/lang/util/String;
数组  [类型签名, 比如 [B

JNI 访问字符串

  • java内部使用的是utf-16 16bit 的编码方式
  • jni 里面使用的utf-8 unicode编码方式 英文是1个字节,中文 3个字节
  • C/C++ 使用 ascii编码 ,中文的编码方式 GB2312编码 中文 2个字节
 public native static String sayHello(String text);

->JNI

JNIEXPORT jstring JNICALL Java_JString_sayHello
        (JNIEnv * env, jclass jclaz, jstring jstr) {
    const char * c_str = NULL;
    char buf[128] = {0};
    jboolean  iscopy;
    c_str = (*env)->GetStringUTFChars(env, jstr, &iscopy);
    printf("isCopy:%d\n", iscopy);
    if(c_str == NULL) {
        return NULL;
}
printf("C_str: %s \n", c_str); sprintf(buf,"Hello, 你好 %s", c_str); printf("C_str: %s \n", buf); (*env)->ReleaseStringUTFChars(env, jstr, c_str); return (*env)->NewStringUTF(env,buf);
}

sayHello函数接收一个jstring类型的参数text,但jstring类型是指向JVM内部的一个字符串,和C风格的字符串类型 char*不同,所以在JNI中不能通把jstring当作普通C字符串一样来使用,必须使用合适的JNI函数来访问JVM内部的字 符串数据结构。

异常处理

if(c_str == NULL) {
        return NULL;
}
以上代码是很关键的,调用完GetStringUTFChars之后不要忘记安全检查,因为JVM需要为新诞生的字符串分配内存空
间,
当内存空间不够分配的时候,会导致调用失败,失败后GetStringUTFChars会返回NULL,并抛出一个 OutOfMemoryError异常。

JNI的异常和Java中的异常处理流程是不一样的,
Java遇到异常如果没有捕获,程序会立即停止 运行。
而JNI遇到未决的异常不会改变程序的运行流程,也就是程序会继续往下走,这样后面针对这个字符串的所有操作都是非 常危险的,

因此,我们需要用return语句跳过后面的代码,并立即结束当前方法。

释放字符串

在调用GetStringUTFChars函数从JVM内部获取一个字符串之后,JVM内部会分配一块新的内存,用于存储源字符串 的拷贝,以便本地代码访问和修改。即然有内存分配,用完之后马上释放是一个编程的好习惯。通过调用 ReleaseStringUTFChars函数通知JVM这块内存已经不使用了,你可以清除了。注意:这两个函数是配对使用的,用 了GetXXX就必须调用ReleaseXXX,而且这两个函数的命名也有规律,除了前面的Get和Release之外,后面的都一 样。

访问Java 成员变量

非静态变量

java代码

 public int property;

Jni代码

JNIEXPORT void JNICALL Java_Hello_testField(JNIEnv *env, jobject jobj) {
->「首先调用GetObjectClass函数获取ClassField的Class引用」
    jclass  claz = (*env)->GetObjectClass(env,jobj);
-> 「然后调用GetFieldID函数从Class引用中获取字段的ID(property是字段名,I是字段的签名)」
    jfieldID  jfid = (*env)->GetFieldID(env, claz, "property","I");
-> 「最后调用GetIntField函数,传入实例对象和字段ID,获取属性的值」
    jint va = (*env)->GetIntField(env,jobj, jfid);
    printf("va: %d", va);
-> 「最后调用GetIntField函数,传入实例对象和字段ID,获取属性的值」
    (*env)->SetIntField(env, jobj, jfid, va + 10086);
}

访问静态变量

访问静态变量和实例变量不同的是,获取字段ID使用GetStaticFieldID,获取和修改字段的值使用
Get/SetStaticXXXField系列函数,比如获取和修改静态变量num:

num = (*env)->GetStaticIntField(env,clazz,fid); 
// 4.修改静态变量num的值
 (*env)->SetStaticIntField(env, clazz, fid, 80);

总结下

1、由于JNI函数是直接操作JVM中的数据结构,不受Java访问修饰符的限制。即,在本地代码中可以调用JNI函数可以
访问Java对象中的非public属性和方法

2、访问和修改实例变量操作步聚:
1>、调用GetObjectClass函数获取实例对象的Class引用 
2>、调用GetFieldID函数获取Class引用中某个实例变量的ID 
3>、调用GetXXXField函数获取变量的值,需要传入实例变量所属对象和变量ID 
4>、调用SetXXXField函数修改变量的值,需要传入实例变量所属对象、变量ID和变量的值 

3、访问和修改静态变量操作步聚: 
1>、调用FindClass函数获取类的Class引用 
2>、调用GetStaticFieldID函数获取Class引用中某个静态变量ID
3>、调用GetStaticXXXField函数获取静态变量的值,需要传入变量所属Class的引用和变量ID 
4>、调用SetStaticXXXField函数设置静态变量的值,需要传入变量所属Class的引用、变量ID和变量的值

访问Java中的函数

Java成员函数一般有两类:静态和非静态。所以在JNI中对这两种不同的类型就有了两种不太相同的调用方法,这两 种不同类型虽然他们的调用方式有些许不同,但是,他们的实质上是一样的。只是调用的接口的名字有区别,而对于 流程是没有区别的。

private static void callStaticMethod(String str, int i) {
        System.out.format("ClassMethod::callStaticMethod called!-->str=%s," 
+" i=%d\n", str, i);

JNI

JNIEXPORT void JNICALL Java_JniTest_callJavaStaticMethod(JNIEnv *env, jobject jobje){
->「1:从classpath路径下搜索ClassMethod这个类,并返回该类的Class对象」
    jclass  clz = (*env)->FindClass(env,"ClassMethod");
    if(clz == NULL) {
        printf("clz is null");
        return; 
  }
->「2:从clazz类中查找callStaticMethod方法」
    jmethodID  jmeid = (*env)->GetStaticMethodID(env, clz, "callStaticMethod", "
(Ljava/lang/String;I)V");
    if(jmeid == NULL) {
        printf("jmeid is null");
        return;
}
jstring arg = (*env)->NewStringUTF(env, "我是静态类");
->「2:从clazz类中查找callStaticMethod方法」
 (*env)->CallStaticVoidMethod(env,clz,jmeid,arg,100);
 (*env)->DeleteLocalRef(env,clz); 
(*env)->DeleteLocalRef(env,arg);
}

JNI引用

在 JNI 规范中定义了三种引用:局部引用(Local Reference)、全局引用(Global Reference)、弱全局引用
(Weak Global Reference)。

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

jclass cls_string = (*env)->FindClass(env, "java/lang/String");
jcharArray charArr = (*env)->NewCharArray(env, len);
jstring str_obj = (*env)->NewObject(env, cls_string, cid_string, elemArray);
jstring str_obj_local_ref = (*env)->NewLocalRef(env,str_obj); // 通过NewLocalRef函数创建 ...

释放一个局部引用有两种方式:
1、本地方法执行完毕后VM自动释放;
2、通过DeleteLocalRef手动释放; VM会自动释放局部引用,
为什么还需要手动释放呢?
因为局部引用会阻止它所引用的对象被GC回收。

全局引用
调用NewGlobalRef基于局部引用创建,会阻GC回收所引用的对象。可以跨方法、跨线程使用。JVM不会自动释放,
必须调用DeleteGlobalRef手动释放 (*env)->DeleteGlobalRef(env,g_cls_string);

tic jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewGlobalRef(env,cls_string);
}

弱全局引用
调用NewWeakGlobalRef基于局部引用或全局引用创建,不会阻止GC回收所引用的对象,可以跨方法、跨线程使 用。引用不会自动释放,在JVM认为应该回收它的时候(比如内存紧张的时候)进行回收而被释放。或调用 DeleteWeakGlobalRef手动释放。(*env)->DeleteWeakGlobalRef(env,g_cls_string)

static jclass g_cls_string;
void TestFunc(JNIEnv* env, jobject obj) {
    jclass cls_string = (*env)->FindClass(env, "java/lang/String");
    g_cls_string = (*env)->NewWeakGlobalRef(env,cls_string);
}

拼接字符串例子

->java
        public static native String sayHello(String ret);

->jni
JNIEXPORT jstring JNICALL Java_zcwfeng_Register_sayHello(JNIEnv *env,jobject obj,jstring str){
->「将jstring转换成c可以用的char *」
    const char * c_str = NULL;
    jboolean isCopy;
    c_str = (*env)->GetStringUTFChars(env,str,&isCopy);
    if(c_str == NULL)
        return NULL;

-> 1. malloc 2.初始化 3.释放----buf[128] = {0} 相当于1,2
    char buf[128] = {0};
-> c里面的拼接,std::copy函数c++
    sprintf(buf,"Hello,你好:%s",c_str);
-> 注意这里千万别忘记
    (*env) -> ReleaseStringUTFChars(env,str,c_str);
    return (*env)->NewStringUTF(env, buf);
}

获取Field实例

package zcwfeng;
public class FieldTest{
    static{
            System.load("/Users/zcw/dev/c_workspace/TempC/cmake-build-debug/libfirstlib.dylib");
    }

    public int property = 1;
    public static int statProperty = 8;
    public native void testField();
    public native void testStaticField();

    public static void main(String[] args){

            FieldTest t = new FieldTest();
            t.testField();
            t.testStaticField();
            System.out.println(t.property);
            System.out.println(t.statProperty);

        }

}

-> JNI
JNIEXPORT void JNICALL Java_zcwfeng_FieldTest_testField(JNIEnv *env,jobject obj){
//    (*env)->FindClass(env,)
    jclass clazz = (*env)->GetObjectClass(env,obj);
    jfieldID  jfid = (*env)->GetFieldID(env,clazz,"property","I");
    jint value = (*env)->GetIntField(env,obj,jfid);
    printf("java 原始值:%d",value);
    (*env)->SetIntField(env,obj,jfid,value + 10000);

}


JNIEXPORT void JNICALL Java_zcwfeng_FieldTest_testStaticField(JNIEnv *env,jobject obj){
//    (*env)->FindClass(env,)
    jclass clazz = (*env)->GetObjectClass(env,obj);
    jfieldID  jfid = (*env)->GetStaticFieldID(env,clazz,"statProperty","I");
    jint value = (*env)->GetStaticIntField(env,obj,jfid);
    printf("java 原始值:%d",value);
    (*env)->SetStaticIntField(env,obj,jfid,value + 10000);

}

获取方法实例

-> java 静态方法和非静态方法
package zcwfeng;
public class ClassMethod{
    private static void callStaticMethod(String str,int i){
        System.out.format("ClassMethod::callStaticMethod called!-str=%s, " + "i=%d\n",str,i);
    }

    private void callInstanceMethod(String str,int i){
        System.out.format("ClassMethod::callInstanceMethod called!-str=%s, " + "i=%d\n",str,i);
    }
}

-> JNI

// 调用java构造方法
JNIEXPORT void JNICALL Java_zcwfeng_Register_callJavaStaticMethod(JNIEnv *env,jobject obj){
    jclass jclz = (*env) -> FindClass(env,"zcwfeng/ClassMethod");
    if(jclz == NULL){
        printf("jclz is null");
        return;
    }
    jmethodID jconstructorId = (*env) -> GetMethodID(env,jclz,"<init>","()V");
    if(jconstructorId == NULL){
        printf("jconstructorId is null");
        return;
    }

    jobject jobj = (*env)->NewObject(env,jclz,jconstructorId);
    jmethodID jmid = (*env) -> GetStaticMethodID(env,jclz,"callStaticMethod","(Ljava/lang/String;I)V");


    jstring jstr = (*env)->NewStringUTF(env,"I am from JNI zcwfeng");

    -> 「注意一下这里CallStaticVoidMethod,参数是class而不是jobject」
    (*env)->CallStaticVoidMethod(env,jclz,jmid,jstr,1008600);



    (*env)->DeleteLocalRef(env,jclz);
    (*env)->DeleteLocalRef(env,jobj);
    (*env)->DeleteLocalRef(env,jstr);


}

// 调用java构造方法
JNIEXPORT void JNICALL Java_zcwfeng_Register_callJavaInstanceMethod(JNIEnv *env,jobject obj){
    jclass jclz = (*env) -> FindClass(env,"zcwfeng/ClassMethod");
    if(jclz == NULL){
        printf("jclz is null");
    return;
    }
    jmethodID jconstructorId = (*env) -> GetMethodID(env,jclz,"<init>","()V");
    if(jconstructorId == NULL){
        printf("jconstructorId is null");
        return;
    }

    jobject jobj = (*env)->NewObject(env,jclz,jconstructorId);
    jmethodID jmid = (*env) -> GetMethodID(env,jclz,"callInstanceMethod","(Ljava/lang/String;I)V");

    jstring jstr = (*env)->NewStringUTF(env,"I am from JNI David\n");
    (*env)->CallVoidMethod(env,jobj,jmid,jstr,10086);



    (*env)->DeleteLocalRef(env,jclz);
    (*env)->DeleteLocalRef(env,jobj);
    (*env)->DeleteLocalRef(env,jstr);


}


野指针实例

特别注意局部引用static的情况释放
DeleteLocalRef
并将对应的 引用智控
区别什么是引用,GetMethodID方法不是

-> java 本地方法

        public native String JCallC();

        public native String newString(int len);
        。。。
        Register r = new Register();
        r.JCallC();
        r.JCallC();   
        
-> 逻辑是 JCallC()调用一个jni方法,jni内部调用newString方法        

-> JNI 

JNIEXPORT jstring JNICALL Java_zcwfeng_Register_newString(JNIEnv *env, jobject obj, jint len) {

    jcharArray elementArray;
    jstring j_str = NULL;
    static jclass cls_string = NULL;
    static jmethodID jmetid = NULL;
    if (cls_string == NULL) {
        printf("cls_string is null \n");
        cls_string = (*env)->FindClass(env, "java/lang/String");
        if (cls_string == NULL) {
            return NULL;
        }
    }
    if (jmetid == NULL) {
        printf("jmetid is null \n");
        jmetid = (*env)->GetMethodID(env, cls_string, "<init>", "([C)V");
        if (jmetid == NULL) {
            return NULL;
        }
    }

    printf("this is a line ----------------------- \n");
    elementArray = (*env)->NewCharArray(env, len);
    printf("this is a line2 ----------------------- \n");
    printf("this is a %d,%d,%d\n", cls_string, jmetid, elementArray);
    j_str = (*env)->NewObject(env, cls_string, jmetid, elementArray);
    printf("this is a line3 ----------------------- \n");
    (*env)->DeleteLocalRef(env, elementArray);
    elementArray = NULL;

    (*env)->DeleteLocalRef(env, cls_string);
    -> 「static静态引用,注释掉这句,下次在调用JCallC,就会导致野指针问题,因为开始判断为NULL,创建,现在没有=NULL,就会导致:指针指向的内存块内容为空」
    cls_string = NULL;
   -> 「这句是错的因为jmetid---GetMethodID 不是LocakRef 他不是引用类型」
////////    (*env)->DeleteLocalRef(env,jmetid);
    jmetid = NULL;
    printf("end of function\n");
    return j_str;

}


JNIEXPORT jstring JNICALL Java_zcwfeng_Register_JCallC(JNIEnv *env, jobject obj) {
    return Java_zcwfeng_Register_newString(env, obj, 1024);
}

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