JNI 和 JNA 使用

Java Native Interface (JNI)是一個本地編程接口,可以讓Java代碼使用以其他語言(C/C++) 編寫的代碼和代碼庫。

編寫Java代碼

package myjni;

public class HelloJNI {
   static {
      System.loadLibrary("hello"); // Load native library at runtime
                                   // hello.dll (Windows) or libhello.so (Unixes)
   }
 
   // Declare a native method sayHello() that receives nothing and returns void
   private native void sayHello();
 
   // Test Driver
   public static void main(String[] args) {
      new HelloJNI().sayHello();  // invoke the native method  sayHello()
   }
}
  • 靜態初始化塊加載本地庫,這個庫應該被包含在Java庫路徑中(java.library.path 變量),否則會產生UnsatisfiedLinkError
    通過System.getProperty("java.library.path")可以查詢對應的值,在執行程序的時候可以通過VM參數-Djava.library.path=path_to_lib顯示指定路徑
  • native 關鍵字聲明方法,表明使用另外一種語言實現的

在實際使用中,.so文件會被放入到resource目錄下,通過Class.getResource或者ClassLoader.getResource獲取資源,存儲到臨時文件中,再加載該臨時文件,而不用通過Djava.library.path顯示指定路徑


編譯java代碼

成字節碼 "HelloJNI.java" -> "HelloJNI.class"

> javac  myjni\HelloJNI.java
or
> javac -d.  myjni\HelloJNI.java   //-d 指定放置生成類文件的位置,必要時創建包名對應的目錄

創建 C/C++ 頭文件

創建一個定義native函數簽名的C/ C++頭文件
通過使用JDK附帶javah工具創建一個頭文件,它可以生成包含class文件中native方法函數聲明的頭文件

// 如果使用的是IDEA 先cd  project/build/class/main
javah -d <dir>   myjni.HelloJNICpp // 生成  HelloJNICpp.h 文件

Java10中該工具不再提供,改為使用

javac -h <output_dir> myjni\HelloJNI.java

生成樣式如下:

/* DO NOT EDIT THIS FILE - it is machine generated */
#include <jni.h>
/* Header for class HelloJNI */
 
#ifndef _Included_HelloJNI
#define _Included_HelloJNI
#ifdef __cplusplus
extern "C" {
#endif
/*
 * Class:     HelloJNI
 * Method:    sayHello
 * Signature: ()V
 */
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *, jobject);
 
#ifdef __cplusplus
}
#endif
#endif

頭文件聲明了一個C函數

JNIEXPORT void JNICALL Java_HelloJNI_sayHello(JNIEnv *, jobject);

C函數的命名慣例是
Java_{package_and_classname}_{function_name}(JNI arguments),包名稱中的點用下劃線替代,函數默認先包含兩個參數:

  • JNIEnv* : JNI environment 的引用(jni.h中定義),相當于一個函數表指針,用來訪問所有的JNI函數
  • jobject : Java 該對象引用,相當于“this”

宏定義JNIEXPORT 和 JNICALL 是對編譯器說明信息,用來做出口函數
extern "C" 只會被C++編譯器識別,表明使用C的函數命名協議


C/C++代碼

C代碼如下:

#include <jni.h>
#include <stdio.h>
#include "HelloJNI.h"
 
JNIEXPORT void JNICALL Java_myjni_HelloJNI_sayHello(JNIEnv *env, jobject thisObj) {
   printf("Hello World!\n");
   return;
}

Ubuntu下的編譯C代碼,如果是C++需要使用g++編譯

gcc -fPIC -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -shared -o libhello.so HelloJNI.c
  • -fPIC則表明使用地址無關代碼,PIC:Position Independent Code
  • -I 指定頭文件,例如jni.h

運行Java程序

顯示指定.so文件

> java -Djava.library.path=. myjni.HelloJNI

JNI基礎

  • Java基本類型:jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean 分別對應
    int, byte, short, long, float, double, char, boolean
  • Java引用類型:jobject 應用對應于 java.lang.Object,它同樣定義了眾多子類型
typedef struct _jobject *jobject;

typedef jobject jclass;
typedef jobject jthrowable;
typedef jobject jstring;
typedef jobject jarray;
typedef jarray jbooleanArray;
typedef jarray jbyteArray;
typedef jarray jcharArray;
typedef jarray jshortArray;
typedef jarray jintArray;
typedef jarray jlongArray;
typedef jarray jfloatArray;
typedef jarray jdoubleArray;
typedef jarray jobjectArray;

C/C++方法收到的是JNI類型,返回值也是JNI類型(例如jstring, jintArray),但是程序內部使用的是C/C++類型,所以中間需要JNI類型與C/C++類型的相互轉換

C/C++程序步驟:

  • 接收JNI類型的參數(由Java程序傳遞來)
  • 對于引用類型(jObject),將參數轉換或復制為本地類型,例如將jstring轉換為C字符串,將jintArray轉換為C的int []等;對于原始JNI類型,如jint和jdouble不需要轉換,可以直接操作
  • 以本地類型執行操作
  • 創建JNI類型的返回對象,并將結果復制到返回對象中

JNI編程中關鍵點是JNI引用類型(如jstring,jobject,jintArray,jobjectArray)和本機類型(C-string,int [])之間的轉換。 JNI接口提供了許多函數來進行這樣的轉換

JNI是一個C接口,它不是面向對象的,所以沒有真正傳遞對象


JNIEnv函數調用方法

通過JNIEnv可以調用眾多函數,內部實際上都是JNINativeInterface_結構體內部的函數指針

struct JNINativeInterface_;

struct JNIEnv_;

#ifdef __cplusplus
typedef JNIEnv_ JNIEnv;
#else
typedef const struct JNINativeInterface_ *JNIEnv;
#endif
類型 語法
C (*env)->FunctionName(env, parameter)
C++ env->FunctionName(parameter) //使用了內聯函數

下面的例子中多使用C語言的調用方式


傳遞原始類型

可以直接使用強制類型轉換()

typedef unsigned char   jboolean;       /* unsigned 8 bits */
typedef signed char     jbyte;          /* signed 8 bits */
typedef unsigned short  jchar;          /* unsigned 16 bits */
typedef short           jshort;         /* signed 16 bits */
typedef int             jint;           /* signed 32 bits */
typedef long long       jlong;          /* signed 64 bits */
typedef float           jfloat;         /* 32-bit IEEE 754 */
typedef double          jdouble;        /* 64-bit IEEE 754 */

傳遞字符串

Java的字符串是16位Unicode字符序列,C的字符串是char型數組,以空字符結尾
JNIEnv提供了轉換的函數

UTF-8(1到3個字節) <--> Unicode(2個字節)

//If isCopy is not NULL, then *isCopy is set to JNI_TRUE if a copy is made; or it is set to JNI_FALSE if no copy is made.
const char * GetStringUTFChars(JNIEnv *env, jstring string, jboolean *isCopy);
//將jstring轉換成為Unicode格式的char*
const jchar * GetStringChars(JNIEnv *env, jstring string, jboolean *isCopy);


//告訴VM native 代碼不在訪問utf
void ReleaseStringUTFChars(JNIEnv *env, jstring string, const char *utf);
//釋放指向Unicode格式的char*的指針
void ReleaseStringChars(JNIEnv *env, jstring string, const jchar *chars);


//創建一個java.lang.String 對象
jstring NewStringUTF(JNIEnv *env, const char *bytes);
//創建一個Unicode格式的String對象,從Unicode字符
jstring NewString(JNIEnv *env, const jchar *unicodeChars, jsize length);


//獲取UTF-8形式的串長度
jsize GetStringUTFLength(JNIEnv *env, jstring string);
//獲取Unicode格式的的長度
jsize GetStringLength(JNIEnv *env, jstring string);



//把一段區域中的String,轉為UTF-8,放入buf
void GetStringUTFRegion(JNIEnv *env, jstring str, jsize start, jsize length, char *buf);
//復制一段區域中的String,放入buf
void GetStringRegion(JNIEnv *env, jstring str, jsize start, jsize length, jchar *buf);

GetStringChars : jstring -> char* 如果內存分配失敗,返回NULL,第三個參數如果不為NULL,當返回字符串是原始string的復制時,被置為JNI_TRUE;
當直接指向初始的String類型時,對應為JNI_FALSE ,jni在運行時會盡可能執行這種情況,這時本地程序如果修改字符數組的內容,對應Java程序的字符串會發生改變


傳遞原始類型的數組

// ArrayType: jintArray, jbyteArray, jshortArray, jlongArray, jfloatArray, jdoubleArray, jcharArray, jbooleanArray
// PrimitiveType: int, byte, short, long, float, double, char, boolean
// NativeType: jint, jbyte, jshort, jlong, jfloat, jdouble, jchar, jboolean

NativeType * Get<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, jboolean *isCopy);
void Release<PrimitiveType>ArrayElements(JNIEnv *env, ArrayType array, NativeType *elems, jint mode);

void Get<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, NativeType *buffer);
void Set<PrimitiveType>ArrayRegion(JNIEnv *env, ArrayType array, jsize start, jsize length, const NativeType *buffer);

ArrayType New<PrimitiveType>Array(JNIEnv *env, jsize length);

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);
void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

jsize GetArrayLength(JNIEnv *env, jarray array);//獲取數組的長度

訪問對象的變量和方法

訪問實例對象的變量和類的靜態變量

jclass GetObjectClass(JNIEnv *env, jobject obj);
   // Returns the class of an object.
   
jfieldID GetFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
  // Returns the field ID for an instance variable of a class.
jfieldID GetStaticFieldID(JNIEnv *env, jclass cls, const char *name, const char *sig);
  // Returns the field ID for a static variable of a class.
 
// 通過jfieldID讀寫字段值
  // <type>包括8種原始類型和object
NativeType Get<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID);
void Set<type>Field(JNIEnv *env, jobject obj, jfieldID fieldID, NativeType value);

NativeType GetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID);
void SetStatic<type>Field(JNIEnv *env, jclass clazz, jfieldID fieldID, NativeType value);

GetFieldID中sig表示簽名,編碼形式如下

類型 sig
boolen Z
byte B
char C
short S
int I
long J
float F
double D
void V
class L<fully-qualified-name>;//引用類型 以 L 開頭 ; 結尾

方法簽名主要用在重載方法的說明,簽名的形式是“(parameters)return-type
javap -s -p 類名稱 查看簽名
javap 是java類文件的反編譯器
-s :輸出內部類型簽名
-p:顯示私有成員,默認只打印public的簽名信息

例子:設定一個long變量

static jlong setField_long(JNIEnv *env, jobject java_obj, const char *field_name, jlong val) {

    jclass clazz = env->GetObjectClass(java_obj);
    jfieldID field = env->GetFieldID(clazz, field_name, "J");
    if (field)
        env->SetLongField(java_obj, field, val);
    else {
        LOGE("__setField_long:field '%s' not found", field_name);
    }
    return val;
}

調用實例對象的方法和靜態方法

  1. 獲取這個類對象的引用 GetObjectClass()
  2. 獲取方法ID GetMethodID(JNIEnv *env, jclass clazz, const char *name, const char *sig)
    需要提供方法名和對應的簽名
  3. 基于方法ID,Call<Primitive-type>Method()CallVoidMethod()CallObjectMethod(),對應<Primitive-type>指的是返回類型,如果有參數,就在后邊加上參數

JNI中對應的函數:

jmethodID GetMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   // Returns the method ID for an instance method of a class or interface.
   
NativeType Call<type>Method(JNIEnv *env, jobject obj, jmethodID methodID, ...);
NativeType Call<type>MethodA(JNIEnv *env, jobject obj, jmethodID methodID, const jvalue *args);
NativeType Call<type>MethodV(JNIEnv *env, jobject obj, jmethodID methodID, va_list args);
   // Invoke an instance method of the object.
   // The <type> includes each of the eight primitive and Object.
   
jmethodID GetStaticMethodID(JNIEnv *env, jclass cls, const char *name, const char *sig);
   // Returns the method ID for an instance method of a class or interface.
   
NativeType CallStatic<type>Method(JNIEnv *env, jclass clazz, jmethodID methodID, ...);
NativeType CallStatic<type>MethodA(JNIEnv *env, jclass clazz, jmethodID methodID, const jvalue *args);
NativeType CallStatic<type>MethodV(JNIEnv *env, jclass clazz, jmethodID methodID, va_list args);
   // Invoke an instance method of the object.
   // The <type> includes each of the eight primitive and Object.


//調用超類的方法
NativeType CallNonvirtual<type>Method(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, ...);
NativeType CallNonvirtual<type>MethodA(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, const jvalue *args);
NativeType CallNonvirtual<type>MethodV(JNIEnv *env, jobject obj, jclass cls, jmethodID methodID, va_list args);

創建對象和對象數組

在native代碼中構建jobject,jobjectArray

創建對象

獲得構造方法的ID,傳遞函數名為“<init>”,“V”作為返回類型

jclass FindClass(JNIEnv *env, const char *name);
 
jobject NewObject(JNIEnv *env, jclass cls, jmethodID methodID, ...);
jobject NewObjectA(JNIEnv *env, jclass cls, jmethodID methodID, const jvalue *args);
jobject NewObjectV(JNIEnv *env, jclass cls, jmethodID methodID, va_list args);
   // Constructs a new Java object. The method ID indicates which constructor method to invoke
 
jobject AllocObject(JNIEnv *env, jclass cls);
  // Allocates a new Java object without invoking any of the constructors for the object.

對象數組

  • FindClass(env, "java/lang/Double"); 找到對應的類
  • NewObjectArray(env, 2, classDouble, NULL); 創建對應的數組
  • GetMethodID(env, classDouble, "<init>", "(D)V"); 找到類的構造方法
  • NewObject(env, classDouble, midDoubleInit, average); 創建該類
  • SetObjectArrayElement(env, outJNIArray, 0, objSum); 存到數組對應的位置上
jobjectArray NewObjectArray(JNIEnv *env, jsize length, jclass elementClass, jobject initialElement);
   // Constructs a new array holding objects in class elementClass.
   // All elements are initially set to initialElement.
 
jobject GetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index);
   // Returns an element of an Object array.
 
void SetObjectArrayElement(JNIEnv *env, jobjectArray array, jsize index, jobject value);
   // Sets an element of an Object array.

本地和全局引用

JNI將native代碼中的對象引用分為兩類

  • 本地引用 local reference :native方法中創建的都是本地引用,方法結束后就回收,可以通過DeleteLocalRef()顯式的使本地引用無效,使得JVM盡快進行GC,同時傳遞到native方法的參數對象,jni方法返回的Java對象都是本地引用
  • 全局引用global reference:通過jobject NewGlobalRef() (JNIEnv *env, jobject lobj);獲取,然后通過void DeleteGlobalRef(JNIEnv *env, jobject gref);顯示的釋放

所以對全局變量賦值時,先要將本地引用轉為全局引用,賦值后再釋放本地引用

實際上本地引用并不是native方法里的局部變量,局部變量存放在堆棧中,而Local Reference存放在Local Reference Table中,而該Table的容量是有限的,雖然在native Method結束時,JVM會自動釋放Local Reference,但在使用中,要及時使用DeleteLocalRef()刪除不必要的Local Reference,避免Local Reference Table溢出


動態加載

VM虛擬機會在native庫加載的時候調用JNI_OnLoad,例如通過System.loadLibrary加載的時候

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reserved);

靜態注冊本地方法的弊端

  1. 需要編譯所有聲明了native方法的Java類,每個所生成的class文件都得用javah生成一個頭文件
  2. javah生成的JNI層函數名特別長,書寫起來很不方便。
  3. 初次調用native函數時要根據函數名字搜索對應用JNI層函數來建立關聯關系,這樣會影響運行效率。

動態注冊核心是JNINativeMethod,把Java中的方法和本地的方法關聯起來

typedef struct {
    char *name; //Java中原生方法的名稱
    char *signature;//方法簽名,是參數類型和返回值類型的組合
    void *fnPtr;//函數指針,
} JNINativeMethod;

看了其他的教程,發現都是固定的套路,C++代碼如下:


static JNINativeMethod gMethods[] = {  
    {"name",  "signature", fnptr},  
};  


JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *jvm, void *reserved) {
    JNIEnv *env = NULL;
    jint result = JNI_FALSE;

    //獲取env指針
    if (jvm->GetEnv(reinterpret_cast<void **> (&env), JNI_VERSION_1_6.) != JNI_OK) {
        return result;
    }
    if (env == NULL) {
        return result;
    }


    //獲取類引用,寫類的全路徑(包名+類名
    jclass clazz = env->FindClass("***/***/JNIDynamicUtils");
    if (clazz == NULL) {
        return result;
    }
    //注冊方法
    if (env->RegisterNatives(clazz, gMethods, sizeof(gMethods) / sizeof(gMethods[0])) < 0) {
        return result;
    }
    //成功
    result = JNI_VERSION_1_6;
    return result;
}

在JNI_OnLoad中,可以保存JavaVM的指針,這是跨線程的,持久有效的變量


多線程

JNIEnv只在當前線程有效,如果C/C++中創建的新線程需要訪問Java VM,先調用AttachCurrentThread方法把自己附著到VM并且獲得env

JavaVM *vm;
JNIEnv *env;
vm->AttachCurrentThread(&env, NULL);
vm->DetachCurrentThread();//卸載

多Java線程加載同一個lib,或者單一線程加載多次,實際上都不會進行處理,因為ClassLoader內部靜態字段會記錄進程加載的所有共享文件,首先會檢查該ClassLoad是否加載過,不會進行重復加載,如果其他ClassLoad加載過,也不會進行加載,多個線程調用同一個so文件的不同函數,共享一套全局變量,因為共享庫在進程中只有一套數據

ClassLoad中關于加載native library的核心字段:

    // 所有加載的native library名稱
    private static Vector<String> loadedLibraryNames = new Vector<>();

    // Native libraries belonging to system classes.
    private static Vector<NativeLibrary> systemNativeLibraries
        = new Vector<>();

    //當前class loader對應的加載的所有Native libraries
    private Vector<NativeLibrary> nativeLibraries = new Vector<>();

    // The paths searched for libraries
    private static String usr_paths[];
    private static String sys_paths[];

底層具體通過dlopen庫函數加載:
jdk/src/share/native/java/lang/ClassLoader.c#Java_java_lang_ClassLoader_00024NativeLibrary_load
src/share/vm/prims/jvm.cpp#JVM_LoadLibrary
/src/os/linux/vm/os_linux.cpp#dll_load#os::Linux::dlopen_helper#dlopen(filename, RTLD_LAZY)


JNA

JNA(Java Native Access)是一個開源的Java框架,是Sun公司推出的一種調用本地方法的技術,建立在JNI基上的一個框架。JNA簡化了調用本地方法的過程,可以直接調用本地共享庫中的函數,不需要寫Java代碼之外的程序

需要添加依賴的jar包jna.jar到CLASSPATH,也可以再添加jna-platform.jar包,內部包含一些平臺常用的庫映射

        <dependency>
            <groupId>net.java.dev.jna</groupId>
            <artifactId>jna</artifactId>
        </dependency>

使用方法:

package com.sun.jna.examples;

import com.sun.jna.Library;
import com.sun.jna.Native;
import com.sun.jna.Platform;

/** Simple example of JNA interface mapping and usage. */
public class HelloWorld {

    // This is the standard, stable way of mapping, which supports extensive
    // customization and mapping of Java to native types.

    public interface CLibrary extends Library {
        CLibrary INSTANCE = (CLibrary)
            Native.load((Platform.isWindows() ? "msvcrt" : "c"),
                                CLibrary.class);

        void printf(String format, Object... args);
    }

    public static void main(String[] args) {
        CLibrary.INSTANCE.printf("Hello, World\n");
        for (int i=0;i < args.length;i++) {
            CLibrary.INSTANCE.printf("Argument %d: %s\n", i, args[i]);
        }
    }
}
  • 一個接口類映射一個要加載的庫,該類需要擴展自Library,接口內定義需要使用的本地庫方法
  • 接口內部需要一個公共靜態常量:INSTANCE,通過這個常量,就可以獲得這個接口的實例,從而使用接口的方法,JNA內部通過代理模式,先對數據類型進行轉換,調用外部dll/so的函數
  • 可以將本地庫路徑設定到jna.library.path

核心是不同平臺數據類型的轉變:

同JNI一樣,Java基本數據類型可以直接映射


Default Type Mappings

數組類型:

// Original C declarations
void fill_buffer(int *buf, int len);
void fill_buffer(int buf[], int len); // same thing with array syntax

// Equivalent JNA mapping
void fill_buffer(int[] buf, int len);

結構體對應:定義一個繼承Structurepublic static類,用來作為參數或返回值類型,類中的公共字段的順序,必須與C 語言中的結構的順序一致,同時定義getFieldOrder()方法,返回有序的字段名稱

Java 調用動態鏈接庫中的C 函數,實際上就是一段內存作為函數的參數傳遞給C函數。動態鏈接庫以為這個參數就是C 語言傳過來的參數。同時,C 語言的結構體是一個嚴格的規范,它定義了內存的次序。因此,JNA 中模擬的結構體的變量順序絕對不能錯

JNA是不能完全替代JNI的,因為NA只能實現Java訪問C函數,使用JNI技術,不僅可以實現Java訪問C函數,也可以實現C語言調用Java代碼


Reference

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容