NDK简单入门

前言

文章所讲述主要来源于一个视频教学,算是一个笔记。不同的是,视频在讲述NDK开发时采用了传统的开发模式和C语言,在这里将讲述使用CMake来构建项目,以及使用C++实现本地方法,内容相对简单。

参考的资料

概念

JNI

Java平台有一个用于和本地C/C++代码进行交互操作的API,称为Java本地接口(JNI)。可以把它理解成协议,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阅读 219,366评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,521评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,689评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,925评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,942评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,727评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,447评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,349评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,820评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,990评论 3 337
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,127评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,812评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,471评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,017评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,142评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,388评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,066评论 2 355