NDK簡單入門

前言

文章所講述主要來源于一個視頻教學,算是一個筆記。不同的是,視頻在講述NDK開發時采用了傳統的開發模式和C語言,在這里將講述使用CMake來構建項目,以及使用C++實現本地方法,內容相對簡單。

參考的資料

概念

JNI

Java平臺有一個用于和本地C/C++代碼進行交互操作的API,稱為Java本地接口(JNI)??梢园阉斫獬蓞f議,JNI指導你如何實現Java與C/C++進行交互。

NDK

NDK是谷歌為了方便在Android應用中使用C/C++而發布的一款工具庫。Android Studio2.2或更高版本對NDK提供了支持,默認的編譯工具是CMake。

Java數據類型和C數據類型

Java編程語言 C編程語言 字節
boolean jbolean 1
byte jbyte 1
char jchar 2
short jshort 2
int jint 4
long jlong 8
float jfloat 4
double jdouble 8
boolean[] jbooleanArray /
byte[] jbyteArray /
char[] jcharArray /
int[] jintArray /
short[] jshortArray /
long[] jlongArray /
float[] jfloatArray /
double[] jdoubleArray /
Object[] jobjectArray /
Class jclass /
String jstring /

環境搭建

我的環境:win10,Android Studio 3.2.0,ndk的版本r18b,CMake的版本3.6。

NDK配置

1、方法一,直接在其官網下載(NDK下載
2、方法二,通過as來下載,打開settings界面,如下圖所示:

as-ndk下載.png

3、配置環境變量(方便使用ndk命令),復制ndk目錄下的build目錄路徑:我的電腦—>屬性(右鍵)—>高級系統設置—>環境變量—>Path(系統變量)—>新建,然后粘貼保存,如下如圖所示:
環境變量.png

如果是win7、win8的記得用英文狀態下的;進行分隔。
4、打開cmd命令行窗口,輸入ndk-build,然后回車,如果出現下圖所示表示配置成功:
ndk配置成功.png

CMake配置

打開as的Settings窗口,如下圖所示:


cmake.png

CMake建議選擇3.6的,3.10不知道是什么原因,在定義好本地方法后Alt+Enter的方式幫你生成對應的C/C++代碼。而LLDB是C/C++的調試工具,選擇最新版本就好。

項目創建步驟

1、創建一個新的項目,且勾選上include C++ support選項,如下圖所示:

勾選include C++ support.png

2、步驟1后一直保持默認,一路Next下去,直到這一步為止,如下圖所示:
C++版本.png

接著Finish就好,上圖畫圈的內容可以參考這篇博客。
3、如果你是選擇從官網下載的NDK,那么項目會報一個錯誤——NDK not configured.,就是不知道NDK在那,如下圖所示:
ndk找不到.png

解決方法如圖所示:
步驟一.png

步驟二.png

然后等待項目構建完成。
4、項目構建完成后其運行的效果如下圖所示:
第一次運行效果.png

分析

下圖是項目的部分結構:

部分項目結構.png

先來看MainActivity下的代碼,如下:

public class MainActivity extends AppCompatActivity {

    // Used to load the 'native-lib' library on application startup.
    // 固定寫法,加載動態庫,要在調用本地方法之前調用,可以寫在其它類里
    static {
        // 庫的名稱是native-lib
        System.loadLibrary("native-lib");
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // Example of a call to a native method
        TextView tv = (TextView) findViewById(R.id.sample_text);
        tv.setText(stringFromJNI());
    }

    /**
     * A native method that is implemented by the 'native-lib' native library,
     * which is packaged with this application.
     */
    // 本地方法的聲明方式,該方法的主體實現在cpp目錄下的native-lib.cpp,
    // 而且是用C/C++來實現的
    // 該方法的目的是從C/C++哪里得到一個字符串
    public native String stringFromJNI();
}

在上述代碼中的靜態代碼塊中,我們怎么知道要加載的動態庫的名稱是native-lib呢,請看app目錄下的CMakeLists.txt

# For more information about using CMake with Android Studio, read the
# documentation: https://d.android.com/studio/projects/add-native-code.html

# Sets the minimum version of CMake required to build the native library.

cmake_minimum_required(VERSION 3.4.1)

# Creates and names a library, sets it as either STATIC
# or SHARED, and provides the relative paths to its source code.
# You can define multiple libraries, and CMake builds them for you.
# Gradle automatically packages shared libraries with your APK.

add_library( # Sets the name of the library.
             # 編譯后動態庫的名稱,可以改,但注意要和System.loadLibrary同步
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             # 編譯生成的動態庫所依賴的源文件,可以依賴多個源文件,也就是cpp目錄下的cpp或c文件
             # 比如cpp目下還有一個test.cpp文件,則書寫如下,此處只是舉個例子
             src/main/cpp/native-lib.cpp
             # src/main/cpp/test.cpp
            )# 注意這個括號

# Searches for a specified prebuilt library and stores the path as a
# variable. Because CMake includes system libraries in the search path by
# default, you only need to specify the name of the public NDK library
# you want to add. CMake verifies that the library exists before
# completing its build.

find_library( # Sets the name of the path variable.
              log-lib

              # Specifies the name of the NDK library that
              # you want CMake to locate.
              log )

# Specifies libraries CMake should link to your target library. You
# can link multiple libraries, such as libraries you define in this
# build script, prebuilt third-party libraries, or system libraries.

target_link_libraries( # Specifies the target library.
                       native-lib

                       # Links the target library to the log library
                       # included in the NDK.
                       ${log-lib} )

經過編譯后,會生成一個名稱為libnative-lib.so的動態庫,在名稱前面的lib是編譯后加上的,因為我們在CMakeLists.txt的配置中庫的名稱是native-lib,另外,在使用System.loadLibrary加載庫時,庫的名稱寫的時native-lib,而不是libnative-lib。其編譯后的位置如下圖所示(官網上介紹是如果未指定目標 ABI,則 CMake 默認使用 armeabi-v7a,我也不知道為啥我的demo第一次編譯運行后是x86的,這個不影響后續的介紹):

默認生成動態庫.png

最后看cpp目錄下native-lib.cpp,也就是對本地方法stringFromJNI的實現:

// jni.h這個頭文件必須要導進來
#include <jni.h>
#include <string>

// 你可以使用C++實現本地方法。然而,那樣你必須將實現本地方法的函數聲明為extern"C"
// (這可以阻止C++編譯器混編方法名)
// 方法命名的格式:
// JNIEXPORT 返回類型 JNICALL Java_+方法的全類名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    // 標準模板庫(STL)提供了一個std::string類
    std::string hello = "Hello from C++";
    // c_str(),一個將string轉換為 const* char的函數
    return env->NewStringUTF(hello.c_str());
}

關于字符串(摘自《Java核心技術 卷II》),Java編程語言中的字符串是UTF-16編碼點的序列,而C的字符串則是以null結尾的字節序列,所以在這兩種語言中的的字符串是很不一樣的。Java本地接口有兩組操作字符串的函數,一組把Java字符串轉換成“改良的UTF-8”字節序列,另一組將它們轉換成UTF-16數值的數組,也就是說轉換成jchar數組。

如果你的C代碼已經使用了Unicode,你可以使用第二組轉換函數。另一方面,如果你的字符串都僅限于使用ASCII字符,你可以使用“改良的UTF-8”轉換函數。

NewStringUTF函數可以用來構造一個新的jstring,而讀取現有jstring對象的內容,需要使用GetStringUTFChars函數。

接著是JNIEnv* env,可以通過ctrl+鼠標左鍵的方式,點擊JNIEnv*,定位到其具體的實現,可以參考這篇文章??梢缘弥?,env是一個二級指針,它是指向函數指針表的指針,封裝了JNI開發所需的函數,如NewStringUTF

在C中,必須在每個JNI調用前面加上(*env)->,以便實際上解析對函數指針的引用。在C++中,JNIEnv類的C++版本有一個內聯成員函數,它負責幫你查找函數指針,所以你可以這樣使用:jstr = env->NewStringUTF(greeting)。

jobjectthis引用等價。靜態方法得到的是類的引用,而非靜態方法得到的是對隱式的this參數對象的引用。

那么,接下來的內容就是以jni為主了。

Java調C++

新建類JavaCallC,如下:

public class JavaCallC {

    // 讓c代碼做加法運算,把結果返回
    public native int add(int x, int y);

    // 從Java傳入字符,c代碼進行拼接
    public native String stringConcatenation(String s);

    // 讓c代碼給每個元素都加上10
    public native int[] increaseArrayElements(int[] intArry);

}

此時,as會提示這些方法有錯,那是因為在native-lib.cpp并沒有創建與之對應的函數,通過alter+enter可以快速創建。C++的實現如下:

// jni.h這個頭文件必須要導進來
#include <jni.h>
#include <string>

// 你可以使用C++實現本地方法。然而,那樣你必須將實現本地方法的函數聲明為extern"C"
// (這可以阻止C++編譯器混編方法名)
// 方法命名的格式:
// JNIEXPORT 返回類型 JNICALL Java_+方法的全類名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    // 標準模板庫(STL)提供了一個std::string類
    std::string hello = "Hello from C++";
    // c_str(),一個將string轉換為 const* char的函數
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_add(JNIEnv *env, jobject instance, jint x, jint y) {
    int result = x + y;
    return result;
}

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_increaseArrayElements(JNIEnv *env, jobject instance,
                                                          jintArray intArry_) {
    jint *intArry = env->GetIntArrayElements(intArry_, NULL); // 訪問數組元素
    jsize len = env->GetArrayLength(intArry_); // 數組長度

            // 遍歷數組,給每個元素加10
    for (int i = 0; i < len; ++i) {
        *(intArry + i) += 10;
    }

    env->ReleaseIntArrayElements(intArry_, intArry, 0);

    return intArry_; // 注意這里返回的是intArry_,而不是intArry,原因在于指針
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_stringConcatenation(JNIEnv *env, jobject instance, jstring s_) {
    const char *s = env->GetStringUTFChars(s_, 0); // 得到java傳遞的string,固定寫法

    // 使用C語言的方式來拼接字符串
    char *fromC = ",I am C++.";
    int len = strlen(s) + strlen(fromC);
    char returnValue[len];
    strcpy(returnValue, s); // 把s復制到returnValue
    strcat(returnValue, fromC); // 把fromC所指向的字符串追加到returnValue所指向的字符串的結尾

    // 虛擬機必須知道你何時使用完字符串,這樣它就能進行垃圾回收(垃圾回收器是在一個獨立線程中運行的,它能夠中
    // 斷本地方法的執行),基于這個原因,你必須調用ReleaseStringUTFChars函數
    env->ReleaseStringUTFChars(s_, s);

    return env->NewStringUTF(returnValue); // 生成字符串并返回
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_greeting(JNIEnv *env, jclass type) { // 注意第二個參數
    std::string returnValue = "Hello,I am static method.";

    return env->NewStringUTF(returnValue.c_str());
}

C++調用Java

其大概流程如下:
1、在Java端定義一個native方法并調用它
2、在C++端實現該native方法,并實現C++調用Java的功能

編碼簽名

為了訪問實例域和調用用Java編程語言定義的方法,你必須學習將數據類型的名稱和方法名進行“混編”的規則(方法簽名描述了參數和該方法返回值的類型)。下面混編方案:

代表 類型
B byte
C char
D double
F float
I int
J long
Lclassname 類的類型
S short
V void
Z boolean

為了描述數組類型,要使用[。例如,一個字符串數組如下:

[Ljava/lang/String;

一個float[][]可以描述為:

[[F

注意:L表達式結尾處的分號是類型表達式的終止符,而不是參數之間的分隔符。另外,在這個編碼方案中,必須用/代替.分隔包和類名。

要建立一個方法的完整簽名,需要把括號內的參數類型都列出來,然后列出返回值類型。例如,一個接收兩個整形參數并返回一個整數的方法編碼為:

(II)I

查看方法簽名

方法一

先編譯項目,然后在下圖的所示的classes的目錄右鍵,選擇Show in Explorer

方法簽名一.png

classess目錄下.png

注意是classes目錄下!打開cmd命令行窗口,cd到此目錄下,執行如下命令:

javap -s 全類名

// 如果要查看private的域
javap -s private 全類名

如下圖所示:


方法簽名一效果.png

方法二

假設,桌面上有一個Test.java,內容如下:

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Calendar;

public class Test
{

    private int age;

    public static void main(String[] args) {
        Calendar calendar = Calendar.getInstance();
        int currentYear = calendar.get(Calendar.YEAR);
        int currentMonth = calendar.get(Calendar.MONTH);
        int currentDay = calendar.get(Calendar.DAY_OF_MONTH);
        System.out.println("currentDay:" + currentDay);
    }
}

查看其方法簽名的大致步驟如下:
1、打開cmd命令行窗口,cdTest.java文件所在額目錄
2、編譯Test.java——javac Test.java
3、執行命令——javap -s Test
效果如下圖所示:

方法簽名二效果.png

方法三

通過生成的頭文件來查看。舉個例子,如下圖所示:

生成頭文件.png

至于哪些報錯,只要不影響頭文件的生成就好。然后將生成的頭文件剪切粘貼到cpp目錄下,打開就可以查看方法簽名了。

注意:我們在前面的步驟中生成了頭文件且剪切粘貼到cpp目錄下(如果目錄下有其它的cpp或c文件,as自動生成的代碼時也不一定生成在你想要放的文件里),新建的native方法通過alt+enter的方式生成的對應函數不一定生成在你想要的文件里,比如native-lib.cpp(什么原因造成還不知道)。你可以手動將自動生成的代碼剪切粘貼到你想放的地方,可以先生成對應的函數再生成頭文件,或者在cpp目錄下新建一個includes目錄專門存放頭文件。所以,還是手動將它粘貼過來吧。

新建類CCallJava.java,代碼如下:

public class CCallJava {

    /**
     * 當執行這個方法的時候,讓C代碼調用
     * public int add(int x, int y)
     */
    public native void callbackAdd();

    /**
     * 當執行這個方法的時候,讓C代碼調用
     * public void helloFromJava()
     */
    public native void callbackHelloFromJava();


    /**
     * 當執行這個方法的時候,讓C代碼調用void printString(String s)
     */
    public native void callbackPrintString();

    /**
     * 當執行這個方法的時候,讓C代碼靜態方法 static void sayHello(String s)
     */
    public native void callbackSayHello();



    public int add(int x, int y) {
        Log.e("TAG", "add() x=" + x + " y=" + y);
        return x + y;
    }

    public void helloFromJava() {
        Log.e("TAG", "helloFromJava()");
    }

    public void printString(String s) {
        Log.e("TAG","C中輸入的:" + s);
    }

    public static void sayHello(String s){
        Log.e("TAG",  "我是java代碼中的JNI."
                + "java中的sayHello(String s)靜態方法,我被C調用了:"+ s);
    }

}

native-lib.cpp更改如下:

// jni.h這個頭文件必須要導進來
#include <jni.h>
#include <string>

// 你可以使用C++實現本地方法。然而,那樣你必須將實現本地方法的函數聲明為extern"C"
// (這可以阻止C++編譯器混編方法名)
// 方法命名的格式:
// JNIEXPORT 返回類型 JNICALL Java_+方法的全類名(.用_替代)
extern "C" JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_MainActivity_stringFromJNI(
        JNIEnv *env,
        jobject /* this */) {
    // 標準模板庫(STL)提供了一個std::string類
    std::string hello = "Hello from C++";
    // c_str(),一個將string轉換為 const* char的函數
    return env->NewStringUTF(hello.c_str());
}

extern "C"
JNIEXPORT jint JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_add(JNIEnv *env, jobject instance, jint x, jint y) {
    int result = x + y;
    return result;
}

extern "C"
JNIEXPORT jintArray JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_increaseArrayElements(JNIEnv *env, jobject instance,
                                                          jintArray intArry_) {
    jint *intArry = env->GetIntArrayElements(intArry_, NULL); // 訪問數組元素
    jsize len = env->GetArrayLength(intArry_); // 數組長度

    // 遍歷數組,給每個元素加10
    for (int i = 0; i < len; ++i) {
        *(intArry + i) += 10;
    }

    env->ReleaseIntArrayElements(intArry_, intArry, 0);

    return intArry_; // 注意這里返回的是intArry_,而不是intArry,原因在于指針
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_stringConcatenation(JNIEnv *env, jobject instance, jstring s_) {
    const char *s = env->GetStringUTFChars(s_, 0); // 得到java傳遞的string,固定寫法

    // 使用C語言的方式來拼接字符串
    char *fromC = ",I am C++.";
    int len = strlen(s) + strlen(fromC);
    char returnValue[len];
    strcpy(returnValue, s); // 把s復制到returnValue
    strcat(returnValue, fromC); // 把fromC所指向的字符串追加到returnValue所指向的字符串的結尾

    // 虛擬機必須知道你何時使用完字符串,這樣它就能進行垃圾回收(垃圾回收器是在一個獨立線程中運行的,它能夠中
    // 斷本地方法的執行),基于這個原因,你必須調用ReleaseStringUTFChars函數
    env->ReleaseStringUTFChars(s_, s);

    return env->NewStringUTF(returnValue); // 生成字符串并返回
}

extern "C"
JNIEXPORT jstring JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_greeting(JNIEnv *env, jclass type) { // 注意第二個參數
    std::string returnValue = "Hello,I am static method.";

    return env->NewStringUTF(returnValue.c_str());
}

extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackAdd(JNIEnv *env, jobject instance) {

    // 得到字節碼,使用/代替.
    jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");

    // 得到方法,最后一個參數的方法簽名
    jmethodID jmethodIDs = env->GetMethodID(jclazz, "add", "(II)I");

    // 實例化該類
    jobject jobject1 = env->AllocObject(jclazz);

    // 調用方法,方法的返回值是int
    jint value = env->CallIntMethod(jobject1, jmethodIDs, 99, 1);

}

extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackHelloFromJava(JNIEnv *env, jobject instance) {

    // 得到字節碼,使用/代替.
    jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");

    // 得到方法,最后一個參數的方法簽名
    jmethodID jmethodIDs = env->GetMethodID(jclazz, "helloFromJava", "()V");

    // 實例化該類
    jobject jobject1 = env->AllocObject(jclazz);

    // 調用方法,方法的返回值是void
    env->CallVoidMethod(jobject1, jmethodIDs);

}

extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackPrintString(JNIEnv *env, jobject instance) {

    // 得到字節碼,使用/代替.
    jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");

    // 得到方法,最后一個參數的方法簽名
    jmethodID jmethodIDs = env->GetMethodID(jclazz, "printString", "(Ljava/lang/String;)V");

    // 實例化該類
    jobject jobject1 = env->AllocObject(jclazz);

    // 調用方法
    jstring jst = env->NewStringUTF("I am afu!!!(*env)->");
    // 方法的返回值是void
    env->CallVoidMethod(jobject1, jmethodIDs, jst);

}

extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_CCallJava_callbackSayHello(JNIEnv *env, jobject instance) {

    // 得到字節碼,使用/代替.
    jclass jclazz = env->FindClass("com/jwstudio/ndkdemo/CCallJava");

    // 得到方法,最后一個參數的方法簽名
    jmethodID jmethodIDs = env->GetStaticMethodID(jclazz, "sayHello", "(Ljava/lang/String;)V");

    // 實例化該類
    jstring jst = env->NewStringUTF("I am 123456android");
    // 注意這個調用的是static方法
    env->CallStaticVoidMethod(jclazz, jmethodIDs, jst);

}

打印日志

C++里打印Log日志。
app目錄下的build.gradle文件添加如下代碼:

        ndk {
            ldLibs 'log'
        }

具體位置:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "com.jwstudio.ndkdemo"
        minSdkVersion 21
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
        externalNativeBuild {
            cmake {
                cppFlags ""
                // 生成.so庫的目標平臺,如果未指定目標 ABI,則 CMake 默認使用 armeabi-v7a,其配置如下
                // abiFilters 'armeabi', 'armeabi-v7a'
            }
        }

        ndk {
            ldLibs 'log'
        }
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
    externalNativeBuild {
        cmake {
            path "CMakeLists.txt"
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}

在類JavaCallC.java中添加一個新的本地方法:

// 通過C++來打日志
    public native void myLog();

native-lib.cpp中的頂部添加如下代碼:

#include <android/log.h>
// 定義Log的tag
#define LOG_TAG "System.out"
// 不同等級的Log
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)

myLog()方法在native-lib.cpp的實現如下:

extern "C"
JNIEXPORT void JNICALL
Java_com_jwstudio_ndkdemo_JavaCallC_myLog(JNIEnv *env, jobject instance) {

    // 打印不同等級的log
    LOGD("result=%s", "result1");
    LOGI("result=%s", "result2");
    LOGE("result=%s", "result2");

}

總結

NDK開發的入門內容大致就這些,更詳細的說明還請查看推薦的參考資料。除了一些必要的配置外,其實大部分內容都是與JNI相關,另外,可能還需要補一補C/C++的知識。

源碼地址

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