Android NDK 開發(fā)之JNI常規(guī)操作

image.png

1. JNI函數(shù)詳解

java中native方法在C++代碼中一般如下:

extern "C" JNIEXPORT jstring JNICALL
Java_com_lucky_jnidemo_MainActivity_stringFromJNI(
        JNIEnv* env,
        jobject /* this */) {
    std::string hello = "Hello from C++";
    return env->NewStringUTF(hello.c_str());
}

1. extern "C" 說明

表示下面的代碼雏掠,都采用C的編譯方式蔚出。

之所以用C的編譯方式杀赢,是因?yàn)?JNIEnv 是C語言代碼寫的旦万,避免一些函數(shù)重載等C語言中不支持的

查看JNIEnv源碼,進(jìn)入到jni.h萎坷,可以看到:

#if defined(__cplusplus)
typedef _JNIEnv JNIEnv; // 如果是C++代碼范抓,用這個(gè)宏定義
typedef _JavaVM JavaVM;
#else
typedef const struct JNINativeInterface* JNIEnv;  //如果是C代碼,用這個(gè)宏定義
typedef const struct JNIInvokeInterface* JavaVM;
#endif

繼續(xù)跟蹤 _JNIEnv食铐,可以看到:

struct _JNIEnv {
    /* do not rename this; it does not seem to be entirely opaque */
    const struct JNINativeInterface* functions;

因此,env的調(diào)用方式如下:

// C++的情況如下: 
Java_com_lucky_jnidemo_MainActivity_stringFromJNI(JNIEnv * env, ...) 
{
   env->NewStringUTF();  //詳細(xì)看源碼中的結(jié)構(gòu)體定義
}

C的情況如下: 
Java_com_lucky_jnidemo_MainActivity_stringFromJNI(JNIEnv * env, ...) 
{ 
  (*env)->NewStringUTF(); //二級(jí)指針
}

2. JNIEXPORT 宏定義說明

針對Linux平臺(tái):該聲明的作用是保證在本動(dòng)態(tài)庫中聲明的方法 , 能夠在其他項(xiàng)目中可以被調(diào)用 ;

3. JNICALL 宏定義說明

此宏定義為空僧鲁,用來表示函數(shù)的調(diào)用規(guī)范虐呻。

4. jobjectjclass

jobject // java傳遞下來的對象象泵,就是本項(xiàng)目中 MainActivity 對象 
jclass // java傳遞下來的 class 對象,就是本項(xiàng)目中的 MainActivity class

2. JNI中函數(shù)操作

1. 在C層中修改Java/Kotlin層String變量(引用類型遍歷)

Kotlin代碼:

//即將要修改的代碼熟悉
private var mName: String = "Lucky" 

//native 修改String name
external fun changeStringName()

//點(diǎn)擊按鈕進(jìn)行修改
binding.btn1.setOnClickListener {
    Log.i("MainActivity", "修改前:$mName")
    changeStringName()
    Log.i("MainActivity", "修改后:$mName")
}

C層中的代碼

extern "C"
JNIEXPORT void JNICALL
Java_com_lucky_jnidemo_MainActivity_changeStringName(JNIEnv *env, jobject thiz) {

    //獲取Class方式一
    jclass mainActivityCls = env->FindClass("com/lucky/jnidemo/MainActivity");

    //獲取Class方式二
    //jclass mainActivityCls = env->GetObjectClass(thiz);

    // jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    jfieldID nameFid = env->GetFieldID(mainActivityCls, "mName", "Ljava/lang/String;");

    // void SetObjectField(jobject obj, jfieldID fieldID, jobject value)
    jstring value = env->NewStringUTF("Update Lucky");

    //void SetObjectField(jobject obj, jfieldID fieldID, jobject value)
    env->SetObjectField(thiz, nameFid, value); //修改值
}

打印結(jié)果:

com.lucky.jnidemo I/MainActivity: 修改前:Lucky
com.lucky.jnidemo I/MainActivity: 修改后:Update Lucky

2. 在C層中修改Kotlin層靜態(tài)Int變量

通過apk查看編譯后的MainActivity 類斟叼,可以看到靜態(tài)mAege是在MainActivity 中

image.png

因此偶惠,這里跟修改MainActivity中的變量一樣的操作

Kotlin代碼:

companion object {
    private var mAge: Int = 666
    init {
        System.loadLibrary("jnidemo")
    }
    external fun changeIntAge() //修改Int age
}


binding.btn2.setOnClickListener {
    Log.i("MainActivity", "修改前:${mAge}")
    changeIntAge()
    Log.i("MainActivity", "修改后:${mAge}")
}

C層中的代碼

extern "C"
JNIEXPORT void JNICALL
Java_com_lucky_jnidemo_MainActivity_00024Companion_changeIntAge(JNIEnv *env, jobject thiz) {

    // 這里要注意用FindClass方式獲取,不然拿到的jcalss會(huì)是 "com/lucky/jnidemo/MainActivity$Companion"
    jclass mainActivityCls = env->FindClass("com/lucky/jnidemo/MainActivity");

    //jfieldID GetStaticFieldID(jclass clazz, const char* name, const char* sig)
    jfieldID ageFid = env->GetStaticFieldID(mainActivityCls, "mAge", "I");
    if (ageFid == NULL) {
        LOGI("%s", "no field  mAge ");
        return;
    }
    // jint 背后就是int,所以可以直接用朗涩,  但是String 必須用 jstring
    int age = env->GetStaticIntField(mainActivityCls, ageFid); // 獲取之前的age
    //  void SetStaticIntField(jclass clazz, jfieldID fieldID, jint value)
    env->SetStaticIntField(mainActivityCls, ageFid, age + 10); //修改值+10

}

打印結(jié)果:

com.lucky.jnidemo I/MainActivity: 修改前:666
com.lucky.jnidemo I/MainActivity: 修改后:676

3. 在C層中修改Kotlin層double變量

kotlin層代碼:

private var mHeight: Double = 175.0

binding.btn3.setOnClickListener {
    Log.i("MainActivity", "修改前:${mHeight}")
    changeDoubleHeight()
    Log.i("MainActivity", "修改后:${mHeight}")
}

external fun changeDoubleHeight() //修改Double height

C層中的代碼

extern "C"
JNIEXPORT void JNICALL
Java_com_lucky_jnidemo_MainActivity_changeDoubleHeight(JNIEnv *env, jobject thiz) {

    jclass mainActivityCls = env->GetObjectClass(thiz);

    // jfieldID GetFieldID(jclass clazz, const char* name, const char* sig)
    jfieldID numberFid  = env->GetFieldID(mainActivityCls, "mHeight", "D");

    // void SetDoubleField(jobject obj, jfieldID fieldID, jdouble value)
    env->SetDoubleField(thiz, numberFid, 178.55);

}

打印結(jié)果:

com.lucky.jnidemo I/MainActivity: 修改前:175.0
com.lucky.jnidemo I/MainActivity: 修改后:178.55

4. 在C層中調(diào)用Kotlin層方法

kotlin層代碼:

/***
 * "(II)I"
 * 被C層調(diào)用的方法
 */
fun add(number1: Int, number2: Int): Int {
    return number1 + number2
}

/***
 * 被C調(diào)用的方法 (Ljava/lang/String;I) Ljava/lang/String;
 */
fun showString(str: String, num: Int): String {
    Log.i("MainActivity", "Value from C++ str: $str  num: $num")
    return "【$str】"
}

external fun callKotlinMethod() //在C層中調(diào)用Kotlin代碼

binding.btn4.setOnClickListener {
    callKotlinMethod() //點(diǎn)擊按鈕
}

C層中的代碼

extern "C"
JNIEXPORT void JNICALL
Java_com_lucky_jnidemo_MainActivity_callKotlinMethod(JNIEnv *env, jobject thiz) {

    jclass mainActivitCls = env->GetObjectClass(thiz);

    // jmethodID   (*GetMethodID)(JNIEnv*, jclass, const char*, const char*);
    jmethodID addMid = env->GetMethodID(mainActivitCls, "add", "(II)I"); //kotlin層 fun add(number1: Int, number2: Int): Int

    // jint CallIntMethod(jobject obj, jmethodID methodID, ...)
    int result = env->CallIntMethod(thiz, addMid, 10, 10); //調(diào)用方法
    LOGD("result:%d\n", result);

    // ++++++++++++++++++++++ C調(diào)用 fun showString(str: String, num: Int): String 函數(shù)
    jmethodID showStringMid = env->GetMethodID(mainActivitCls, "showString", "(Ljava/lang/String;I)Ljava/lang/String;");

    // jobject     (*CallObjectMethod)(jobject, jmethodID, ...);
    jstring value = env->NewStringUTF("C語言調(diào)用Kotlin");
    jstring resultStrJ = (jstring) env->CallObjectMethod(thiz, showStringMid, value, 10086);
    const char *resultStr = env->GetStringUTFChars(resultStrJ, NULL);
    LOGD("result2:%s\n", resultStr);
}

打印結(jié)果:

//C中的打印
com.lucky.jnidemo D/Lucky: result:20
com.lucky.jnidemo D/Lucky: result2:【C語言調(diào)用Kotlin】

//Kotlin中的打印
com.lucky.jnidemo I/MainActivity: Value from C++ str: C語言調(diào)用Kotlin  num: 10086

5. 在C層中修改Kotlin層數(shù)組

kotlin層代碼:

binding.btn5.setOnClickListener {
    val ints = intArrayOf(1, 2, 3, 4, 5, 6)
    val strs = arrayOf("李小龍", "李連杰", "李元霸")
    testArrayAction(99, "你好", ints, strs)

    for (anInt in ints) {
        Log.d("MainActivity", "(Kotlin)修改后: ints:$anInt")
    }

    for (str in strs) {
        Log.e("MainActivity", "(Kotlin)修改后: strs:$str")
    }
}

//在C層中修改數(shù)組
external fun testArrayAction(count: Int, textInfo: String, ints: IntArray, strs: Array<String>) 

C層中的代碼

extern "C"
JNIEXPORT void JNICALL
Java_com_lucky_jnidemo_MainActivity_testArrayAction(JNIEnv *env, jobject thiz, jint count, jstring text_info, jintArray ints, jobjectArray strs) {

    //++++++++++++++++++++++++++ 操作Int數(shù)組
    LOGD("count:%d\n", count);
    const char *_textInfoStr = env->GetStringUTFChars(text_info, NULL);
    LOGD("text_info:%s\n", _textInfoStr);
    env->ReleaseStringUTFChars(text_info, _textInfoStr); //釋放

    int intsLen = env->GetArrayLength(ints); //獲取數(shù)組個(gè)數(shù)

    for (int i = 0; i < intsLen; ++i) {
        jint *_ints = env->GetIntArrayElements(ints, NULL);
        *(_ints + i) = (i + 1000001); //修改數(shù)組的值
        LOGD("C++ _ints item:%d\n", *(_ints + i));
        // JNI_OK 0 == 代表 先用操縱桿刷新到JVM忽孽,JVM會(huì)更新上層代碼。再釋放C++層數(shù)組
        // JNI_COMMIT 1 == 代表 用操縱桿刷新到JVM谢床,JVM會(huì)更新上層代碼
        // JNI_ABORT 2 == 代表 釋放C++層數(shù)組
        env->ReleaseIntArrayElements(ints, _ints, JNI_OK); //在數(shù)組循環(huán)中兄一,一定要記得釋放
    }

    //++++++++++++++++++++++++++ 操作String數(shù)組
    int strsLen = env->GetArrayLength(strs);
    for (int i = 0; i < strsLen; ++i) {
        jobject item = env->GetObjectArrayElement(strs, i); //獲取每一個(gè)值
        jstring itemStr = (jstring) item;
        const char *itemStr_1 = env->GetStringUTFChars(itemStr, NULL);
        LOGI("C++ 修改前itemStr_:%s\n", itemStr_1);
        env->ReleaseStringUTFChars(itemStr, itemStr_1);

        jstring value = env->NewStringUTF("AAAAAAA");
        env->SetObjectArrayElement(strs, i, value); //修改數(shù)組值
        jobject item2 = env->GetObjectArrayElement(strs, i);
        jstring itemStr2 = (jstring) item2;
        const char *itemStr_2 = env->GetStringUTFChars(itemStr2, NULL);
        LOGI("C++ 修改后itemStr_2:%s\n", itemStr_2);
        env->ReleaseStringUTFChars(itemStr2, itemStr_2);
    }
}

打印日志:

com.lucky.jnidemo D/Lucky: count:99
com.lucky.jnidemo D/Lucky: text_info:你好
com.lucky.jnidemo D/Lucky: C++ _ints item:1000001
com.lucky.jnidemo D/Lucky: C++ _ints item:1000002
com.lucky.jnidemo D/Lucky: C++ _ints item:1000003
com.lucky.jnidemo D/Lucky: C++ _ints item:1000004
com.lucky.jnidemo D/Lucky: C++ _ints item:1000005
com.lucky.jnidemo D/Lucky: C++ _ints item:1000006
com.lucky.jnidemo I/Lucky: C++ 修改前itemStr_:李小龍
com.lucky.jnidemo I/Lucky: C++ 修改后itemStr_2:AAAAAAA
com.lucky.jnidemo I/Lucky: C++ 修改前itemStr_:李連杰
com.lucky.jnidemo I/Lucky: C++ 修改后itemStr_2:AAAAAAA
com.lucky.jnidemo I/Lucky: C++ 修改前itemStr_:李元霸
com.lucky.jnidemo I/Lucky: C++ 修改后itemStr_2:AAAAAAA
com.lucky.jnidemo D/MainActivity: (Kotlin)修改后: ints:1000001
com.lucky.jnidemo D/MainActivity: (Kotlin)修改后: ints:1000002
com.lucky.jnidemo D/MainActivity: (Kotlin)修改后: ints:1000003
com.lucky.jnidemo D/MainActivity: (Kotlin)修改后: ints:1000004
com.lucky.jnidemo D/MainActivity: (Kotlin)修改后: ints:1000005
com.lucky.jnidemo D/MainActivity: (Kotlin)修改后: ints:1000006
com.lucky.jnidemo E/MainActivity: (Kotlin)修改后: strs:AAAAAAA
com.lucky.jnidemo E/MainActivity: (Kotlin)修改后: strs:AAAAAAA

6. 在C層中修改Kotlin層類

修改實(shí)體類,我們這里用Java類识腿,這樣修改int 變量不需要kotlin中這么麻煩出革,需要轉(zhuǎn)換Integer

kotlin層代碼:

Person類

public class Person {

    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
binding.btn6.setOnClickListener {
    val person = Person()
    person.name = "張三"
    person.age = 18
    putObject(person, "Kotlin文本")
    Log.i("MainActivity", "(Kotlin)修改后, person:$person")
}

// 只玩Student對象里面的成員
external fun putObject(person: Person, str: String) // 傳遞引用類型,傳遞對象

C層中的代碼

extern "C"
JNIEXPORT void JNICALL
Java_com_lucky_jnidemo_MainActivity_putObject(JNIEnv *env, jobject thiz, jobject person, jstring str) {

    const char *_str = env->GetStringUTFChars(str, NULL);
    LOGD("_str:%s\n", _str);
    env->ReleaseStringUTFChars(str, _str);
    jclass mStudentClass = env->GetObjectClass(person);
    // toString
    jmethodID toStringMethod = env->GetMethodID(mStudentClass, "toString", "()Ljava/lang/String;");
    jstring results = (jstring) env->CallObjectMethod(person, toStringMethod);
    const char *result = env->GetStringUTFChars(results, NULL);
    LOGD("C++ toString:%s\n", result);
    env->ReleaseStringUTFChars(results, result);

    // setName
    jmethodID setNameMethod = env->GetMethodID(mStudentClass, "setName", "(Ljava/lang/String;)V");
    jstring nameS = env->NewStringUTF("李四");
    env->CallVoidMethod(person, setNameMethod, nameS);

    // getName
    jmethodID getNameMethod = env->GetMethodID(mStudentClass, "getName", "()Ljava/lang/String;");
    jstring nameS2 = (jstring) env->CallObjectMethod(person, getNameMethod);
    const char *nameS3 = env->GetStringUTFChars(nameS2, NULL);
    LOGD("C++ getName:%s\n", nameS3);
    env->ReleaseStringUTFChars(nameS2, nameS3);

    // setAge
    jmethodID setAgeMethod = env->GetMethodID(mStudentClass, "setAge", "(I)V");
    env->CallVoidMethod(person, setAgeMethod, 99);

    // getAge
    jmethodID getAgeMethod = env->GetMethodID(mStudentClass, "getAge", "()I");
    int age = env->CallIntMethod(person, getAgeMethod);
    LOGD("C++ getAge:%d\n", age);

    env->DeleteLocalRef(mStudentClass);
}

7. 在C層中創(chuàng)建類

Student類

public class Student {
    private String name;
    private int age;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Person 類

public class Person {

    private String name;
    private int age;
    private static Student student;

    public static void putStudent(Student student) {
        Person.student = student;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Student getStudent() {
        return student;
    }

    public void setStudent(Student student) {
        Person.student = student;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", student=" + student +
                '}';
    }
}

kotlin層中的代碼

binding.btn7.setOnClickListener {
    insertObject()
}
external fun insertObject()

C層中的代碼

extern "C"
JNIEXPORT void JNICALL
Java_com_lucky_jnidemo_MainActivity_insertObject(JNIEnv *env, jobject thiz) {

    // @Student對象
    jclass studentClass = env->FindClass("com/lucky/jnidemo/Student");
    jobject student = env->AllocObject(studentClass);

    // setName
    jmethodID setNameMethod = env->GetMethodID(studentClass, "setName", "(Ljava/lang/String;)V");
    jstring value1 = env->NewStringUTF("張三");
    env->CallVoidMethod(student, setNameMethod, value1);

    // setAge
    jmethodID setAgeMethod = env->GetMethodID(studentClass, "setAge", "(I)V");
    env->CallVoidMethod(student, setAgeMethod, 99);

    // @Person對象
    jclass personClass = env->FindClass("com/lucky/jnidemo/Person");
    jobject person = env->AllocObject(personClass); // C++ 分配一個(gè)對象出來渡讼,不會(huì)調(diào)用此對象的構(gòu)造函數(shù)
    // env->NewObject();  // C++ 實(shí)例化一個(gè)對象出來骂束,會(huì)調(diào)用此對象的構(gòu)造函數(shù),相當(dāng)于: new XXX();

    // void CallVoidMethod(person obj, jmethodID methodID, ...)
    jmethodID setStudent = env->GetMethodID(personClass, "setStudent", "(Lcom/lucky/jnidemo/Student;)V");
    env->CallVoidMethod(person, setStudent, student);

    // static void putStudent
    jmethodID putStudent = env->GetStaticMethodID(personClass, "putStudent", "(Lcom/lucky/jnidemo/Student;)V");
    env->CallStaticVoidMethod(personClass, putStudent, student);

    // TODO 釋放工作
    // 釋放方式一
    env->DeleteLocalRef(personClass);
    env->DeleteLocalRef(student);
    env->DeleteLocalRef(value1);
    env->DeleteLocalRef(personClass);
    env->DeleteLocalRef(person);
    // 釋放方式二
    /*env->GetStringUTFChars()
    env->ReleaseStringUTFChars()*/

    // 釋放方式三
    /*new StudentCPP對象
    delete StudentCPP對象*/
}

GitHub源碼:MyJniDemo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末成箫,一起剝皮案震驚了整個(gè)濱河市展箱,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌蹬昌,老刑警劉巖混驰,帶你破解...
    沈念sama閱讀 222,590評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異凳厢,居然都是意外死亡账胧,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評論 3 399
  • 文/潘曉璐 我一進(jìn)店門先紫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來治泥,“玉大人,你說我怎么就攤上這事遮精【蛹校” “怎么了?”我有些...
    開封第一講書人閱讀 169,301評論 0 362
  • 文/不壞的土叔 我叫張陵本冲,是天一觀的道長准脂。 經(jīng)常有香客問我,道長檬洞,這世上最難降的妖魔是什么狸膏? 我笑而不...
    開封第一講書人閱讀 60,078評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮添怔,結(jié)果婚禮上湾戳,老公的妹妹穿的比我還像新娘贤旷。我一直安慰自己,他們只是感情好砾脑,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評論 6 398
  • 文/花漫 我一把揭開白布幼驶。 她就那樣靜靜地躺著,像睡著了一般韧衣。 火紅的嫁衣襯著肌膚如雪盅藻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,682評論 1 312
  • 那天畅铭,我揣著相機(jī)與錄音氏淑,去河邊找鬼。 笑死顶瞒,一個(gè)胖子當(dāng)著我的面吹牛夸政,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播榴徐,決...
    沈念sama閱讀 41,155評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼守问,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坑资?” 一聲冷哼從身側(cè)響起耗帕,我...
    開封第一講書人閱讀 40,098評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎袱贮,沒想到半個(gè)月后仿便,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡攒巍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評論 3 342
  • 正文 我和宋清朗相戀三年嗽仪,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片柒莉。...
    茶點(diǎn)故事閱讀 40,852評論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡闻坚,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出兢孝,到底是詐尸還是另有隱情窿凤,我是刑警寧澤,帶...
    沈念sama閱讀 36,520評論 5 351
  • 正文 年R本政府宣布跨蟹,位于F島的核電站雳殊,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏窗轩。R本人自食惡果不足惜夯秃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧寝并,春花似錦箫措、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽植酥。三九已至镀岛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間友驮,已是汗流浹背漂羊。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留卸留,地道東北人走越。 一個(gè)月前我還...
    沈念sama閱讀 49,279評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像耻瑟,于是被迫代替她去往敵國和親旨指。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評論 2 361

推薦閱讀更多精彩內(nèi)容