Android JNI學(xué)習(xí)

本篇總結(jié)Android JNI如何通過兩種不同的構(gòu)建方式與C/C++進(jìn)行交互,話不多少先上兩張效果圖曲楚,注意:本項(xiàng)目是基于Android Studio3.0開發(fā)的。


圖1.png

圖2.png

以上就是與C/C++語言交互的內(nèi)容部分分褥符,涵蓋了開發(fā)基本上的需求龙誊,讓我進(jìn)入正題

兩種不同的構(gòu)建方式

以下介紹一種基于CMake的方式來構(gòu)建,一種是基于比較傳統(tǒng)的mk文件構(gòu)建方式喷楣,雖然基于CMake的方式更加的只能趟大,但是也是基于傳統(tǒng)的方式來的,這種方式只能適用于Android Studio 2.2以上的版本铣焊。無論基于什么構(gòu)建逊朽,首先曲伊,你必須要下載Android NDK開發(fā)包叽讳。點(diǎn)我下載

基于CMake方式

基于CMake方式構(gòu)建的項(xiàng)目,在寫C++和C代碼的時(shí)候可以與寫Java代碼一樣的有提示熊昌,而且還可以直接的debug绽榛。
打開一個(gè)項(xiàng)目,從菜單欄中選擇 Tools > Android > SDK Manager婿屹。
點(diǎn)擊 SDK Tools 選項(xiàng)卡灭美。
勾選 LLDB,CMake 和 NDK昂利。如圖:


setting.png
  • The Android Native Development Kit (NDK): 讓你能在 Android 上面使用 C 和 C++ 代碼的工具集届腐。
  • CMake: 外部構(gòu)建工具。如果你準(zhǔn)備只使用 ndk-build 的話蜂奸,可以不使用它犁苏。
  • LLDB: Android Studio 上面調(diào)試本地代碼的工具。
創(chuàng)建支持C/C++的項(xiàng)目

點(diǎn)擊新建項(xiàng)目扩所,勾選以下選項(xiàng)以支持C/C++


圖3.png
  • C++ Standard:點(diǎn)擊下拉框围详,可以選擇標(biāo)準(zhǔn) C++,或者選擇默認(rèn) CMake 設(shè)置的 Toolchain Default 選項(xiàng)祖屏。
  • Exceptions Support:如果你想使用有關(guān) C++ 異常處理的支持助赞,就勾選它。勾選之后袁勺,Android Studio 會(huì)在 module 層的 build.gradle 文件中的 cppFlags 中添加 -fexcetions 標(biāo)志雹食。
  • Runtime Type Information Support:如果你想支持 RTTI,那么就勾選它期丰。勾選之后群叶,Android Studio 會(huì)在 module 層的 build.gradle 文件中的 cppFlags 中添加 -frtti 標(biāo)志吃挑。


    圖4.png

    最后點(diǎn)擊Finish完成工程的創(chuàng)建。


    圖5.png

    與之前普通項(xiàng)目就多了這三個(gè)文件街立。
  • .externalNativeBuild:這個(gè)文件夾與下面的CMakeLists.txt文件是對(duì)應(yīng)的舶衬,他主要由CMakeLists.txt文件來構(gòu)建so文件庫的文件夾。
  • cpp:這個(gè)是存放C/C++源代碼的文件夾几晤。
  • CMakeLists.txt:這個(gè)文件主要是構(gòu)建整個(gè)C++/C的腳本文件约炎。里面需要編寫構(gòu)建的代碼,有點(diǎn)類似Android.mk文件蟹瘾。
    到這里,基于CMake的構(gòu)建就完成了掠手,點(diǎn)擊運(yùn)行可以直接看到來自C++的字符憾朴。
    在這里,主要講將CMakeLists.txt的用法喷鸽,如何用了構(gòu)建:
    先看如下代碼:
cmake_minimum_required(VERSION 3.4.1)

include_directories(src/main/cpp/include/)

add_library( # Sets the name of the library.
             native-lib

             # Sets the library as a shared library.
             SHARED

             # Provides a relative path to your source file(s).
             src/main/cpp/native-lib.cpp )

為了讓 CMake 將源代碼(native source code)編譯成 native library众雷。需要在編譯文件中添加 cmake_minimum_required() 和 add_library() 命令。
看下圖:

圖6.png

include_directories(src/main/cpp/include/)這行代碼的意思就是當(dāng)你使用 add_library()做祝,將一個(gè)源文件(source file)或庫添加到你的 CMake 構(gòu)建腳本砾省,同步你的項(xiàng)目,然后你會(huì)發(fā)現(xiàn) Android studio 將關(guān)聯(lián)的頭文件也顯示了處理混槐。然而编兄,為了讓 CMake 在編譯時(shí)期能定位到你的頭文件,你需要在 CMake 構(gòu)建腳本中添加 include_directories() 命令声登,并指定頭文件路徑狠鸳。

target_link_libraries( # Specifies the target library.
                       native-lib

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

這段代碼就是如果你的native-lib代碼中有引用別的頭文件,在這里就需要把頭文件鏈接到這里悯嗓,這個(gè)與add_library的順序是一致的件舵。
最后在Java代碼中l(wèi)oad的so庫是與上面native-lib是一致的。

  static {
        System.loadLibrary("native-lib");
    }

至此脯厨,一個(gè)完整的jni項(xiàng)目就集成完畢铅祸,至于如何引用第三方C/C++的庫到CMake中,后期會(huì)介紹將librtmp合武,F(xiàn)Fmpeg集成到Android中來講解临梗。

傳統(tǒng)的構(gòu)建JNI項(xiàng)目的方式

這個(gè)方式比較古老了,相信從eclipse轉(zhuǎn)過來的童鞋都知道眯杏,這里我就不詳細(xì)講述這種方式的集成夜焦,這里推薦大家看這位大神的一篇博客:看不到我,看不到我
我這里就詳細(xì)總結(jié)一下Android.mk與Application.mk文件的編寫岂贩。

Android.mk

Android.mk是Android提供的一種makefile文件茫经,用來指定諸如編譯生成so庫名巷波、引用的頭文件目錄、需要編譯的.c/.cpp文件和.a靜態(tài)庫文件

最基本的格式應(yīng)該是這樣:

LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

LOCAL_xxx       := xxx  
LOCAL_MODULE := NdkTools
LOCAL_SRC_FILES := NdkTools.cpp
LOCAL_xxx       := xxx  

include $(BUILD_SHARED_LIBRARY)
  • LOCAL_PATH:變量制定了該.mk的路徑卸伞,$(call my-dir)調(diào)用NDK內(nèi)部的函數(shù)獲得當(dāng)前.mk文件的路徑抹镊,每個(gè)Android.mk文件必須以定義LOCAL_PATH為開始。它用于在開發(fā)tree中查找源文件荤傲。宏my-dir 則由Build System提供垮耳。返回包含Android.mk的目錄路徑。
  • include $(CLEAR_VARS):清空了除了LOCAL_PATH之外的所有LOCAL_xxx變量的值遂黍,CLEAR_VARS 變量由Build System提供终佛。并指向一個(gè)指定的GNU Makefile,由它負(fù)責(zé)清理很多LOCAL_xxx.
    例如:LOCAL_MODULE, LOCAL_SRC_FILES, LOCAL_STATIC_LIBRARIES等等雾家。但不清理LOCAL_PATH.
    這個(gè)清理動(dòng)作是必須的铃彰,因?yàn)樗械木幾g控制文件由同一個(gè)GNU Make解析和執(zhí)行,其變量是全局的芯咧。所以清理后才能避免相互影響牙捉。
  • 中間就是對(duì)于模塊參數(shù)的設(shè)置,主要包括:模塊名字敬飒、模塊源文件邪铲、模塊類型、編譯好的模塊存放位置无拗、以及編譯的平臺(tái)等
  • include $(BUILD_xxx_xxx): 執(zhí)行NDK的默認(rèn)腳本带到,它會(huì)收集include $(CLEAR_VARS)腳本后所有定義的LOCAL_xxx變量,然后根據(jù)它們來生成模塊蓝纲。BUILD_SHARED_LIBRARY:是Build System提供的一個(gè)變量阴孟,指向一個(gè)GNU Makefile Script。
    它負(fù)責(zé)收集自從上次調(diào)用 include $(CLEAR_VARS) 后的所有LOCAL_XXX信息税迷。并決定編譯為什么永丝。
    BUILD_STATIC_LIBRARY :編譯為靜態(tài)庫。
    BUILD_SHARED_LIBRARY :編譯為動(dòng)態(tài)庫
    BUILD_EXECUTABLE :編譯為Native C可執(zhí)行程序
    BUILD_PREBUILT :該模塊已經(jīng)預(yù)先編譯

下面詳細(xì)說說中間比較常見的東西吧:

  • LOCAL_MODULE := NdkTools:LOCAL_MODULE模塊必須定義箭养,以表示Android.mk中的每一個(gè)模塊慕嚷。名字必須唯一且不包含空格。Build System會(huì)自動(dòng)添加適當(dāng)?shù)那熬Y和后綴毕泌。例如喝检,NdkTools,要產(chǎn)生動(dòng)態(tài)庫撼泛,則生成libNdkTools.so. 但請(qǐng)注意:如果模塊名被定為:libNdkTools挠说,則生成libfoo.so. 不再加前綴
  • LOCAL_MODULE_PATH :=$(TARGET_ROOT_OUT) 指定最后生成的模塊的目標(biāo)地址,TARGET_ROOT_OUT:根文件系統(tǒng)愿题,路徑為out/target/product/generic/root
    TARGET_OUT:system文件系統(tǒng)损俭,路徑為out/target/product/generic/system
    TARGET_OUT_DATA:data文件系統(tǒng)蛙奖,路徑為out/target/product/generic/data
    除了上面的這些,NDK還提供了很多其他的TARGET_XXX_XXX變量杆兵,用于將生成的模塊拷貝到輸出目錄的不同路徑雁仲,默認(rèn)是TARGET_OUT
  • LOCAL_SRC_FILES := NdkTools.cpp:LOCAL_SRC_FILES變量必須包含將要打包如模塊的C/C++ 源碼。不必列出頭文件琐脏,build System 會(huì)自動(dòng)幫我們找出依賴文件攒砖。缺省的C++源碼的擴(kuò)展名為.cpp. 也可以修改,通過LOCAL_CPP_EXTENSION

可能以后還會(huì)經(jīng)常用到一些以“LOCAL_”開頭的編譯變量日裙,這些變量是啥意思吹艇,可以參考這篇文章:看不到我,看不到我

使用C代碼通過JNI與Java代碼溝通

好了阅签,前面鋪墊完了掐暮,接下才是編寫代碼中經(jīng)常需要用到的地方

  • 傳遞Boolean類型
JNIEXPORT jboolean

JNICALL Java_com_dramascript_ndktest_NdkTools_getBoolean
        (JNIEnv *evn, jobject obj, jboolean b) {
    unsigned char bo = b;
    bo = 1;
    return (jboolean)bo;
}
  • 傳遞byte類型
JNIEXPORT jbyte

JNICALL Java_com_dramascript_ndktest_NdkTools_getByte
        (JNIEnv *evn, jobject obj, jbyte b) {
    int i = b;
    i = 15;
    return (jbyte)
    i;
}
  • 傳遞char類型
JNIEXPORT jchar

JNICALL Java_com_dramascript_ndktest_NdkTools_getChar
        (JNIEnv *evn, jobject obj, jchar c) {
    char ch = c;
    ch = 'S';
    return (jchar)
    ch;
}
  • 傳遞short類型
JNIEXPORT jshort

JNICALL Java_com_dramascript_ndktest_NdkTools_getShort
        (JNIEnv *evn, jobject obj, jshort s) {
    short st = s;
    st = 100;
    return (jshort)
    st;
}
  • 傳遞int類型
JNIEXPORT jint

JNICALL Java_com_dramascript_ndktest_NdkTools_getInt
        (JNIEnv *evn, jobject obj, jint i) {
    int a = i;
    a = 1024;
    return (jint)
    a;
}
  • 傳遞long類型
JNIEXPORT jlong

JNICALL Java_com_dramascript_ndktest_NdkTools_getLong
        (JNIEnv *evn, jobject obj, jlong l) {
    long lg = l;
    lg = 1024L;
    return (jlong)
    lg;
}
  • 傳遞float類型
JNIEXPORT jfloat

JNICALL Java_com_dramascript_ndktest_NdkTools_getFloat
        (JNIEnv *evn, jobject obj, jfloat f) {
    float ft = f;
    ft = 23;
    return (jfloat)
    ft;
}
  • 傳遞double類型
JNIEXPORT jdouble

JNICALL Java_com_dramascript_ndktest_NdkTools_getDouble
        (JNIEnv *evn, jobject obj, jdouble d) {
    double dl = d;
    dl = 2048.12;
    return (jdouble)
    dl;
}
  • 傳遞string類型
JNIEXPORT jstring

JNICALL Java_com_dramascript_ndktest_NdkTools_getString
        (JNIEnv *env, jobject obj, jstring s) {

    char *st = (char *) env->GetStringUTFChars(s, 0);
    char *str = "我很好!";
    jstring rtn;
    rtn = env->NewStringUTF(str);
    return rtn;
}
  • 傳遞數(shù)組類型
JNIEXPORT jbyteArray

JNICALL Java_com_dramascript_ndktest_NdkTools_getByteArray
        (JNIEnv *evn, jobject obj, jbyteArray ba) {

    //獲得byte數(shù)組
    jbyte * bytes = evn->GetByteArrayElements(ba, 0);
    int chars_len = evn->GetArrayLength(ba);
    //返回新的byte數(shù)組
    jbyteArray arr = evn->NewByteArray(6);
    jbyte * by = evn->GetByteArrayElements(arr, 0);
    char ch[10] = "abcd";
    for (int i = 0; i < 4; ++i) {
        by[i] = ch[i];
    }
    evn->SetByteArrayRegion(arr, 0, 6, by);
    return arr;

}
  • 傳遞對(duì)象類型
JNIEXPORT jobject

JNICALL Java_com_dramascript_ndktest_NdkTools_getObject
        (JNIEnv *env, jobject obj, jobject paramIn) {

    //獲取
    jclass paramInClass = env->GetObjectClass(paramIn);
    if (paramInClass == NULL) {
        return NULL;
    }
    if (env->IsInstanceOf(obj, paramInClass))//判斷jobject是否是某個(gè)jclass類型政钟。
    {
        jboolean iscopy;
        jfieldID intId = env->GetFieldID(paramInClass, "age", "I");
        jint num = (int) env->GetIntField(paramIn, intId);

        jfieldID strId = env->GetFieldID(paramInClass, "name", "Ljava/lang/String;");
        jstring str = (jstring)(env)->GetObjectField(paramIn, strId);
        const char *locstr = env->GetStringUTFChars(str, &iscopy);

        env->ReleaseStringUTFChars(str, locstr);
    }

    //返回
    jclass cls = env->FindClass("com/dramascript/ndktest/User");
    jmethodID id = env->GetMethodID(cls, "<init>", "()V");

    jobject paramOut = env->NewObjectA(cls, id, 0);

    jfieldID intId = env->GetFieldID(cls, "age", "I");
    env->SetIntField(paramOut, intId, 23);

    jfieldID strId = env->GetFieldID(cls, "name", "Ljava/lang/String;");
    env->SetObjectField(paramOut, strId, (jstring)(env)->NewStringUTF("林俊杰"));

    return paramOut;
}

  • 傳遞數(shù)組對(duì)象類型
JNIEXPORT jobjectArray

JNICALL Java_com_dramascript_ndktest_NdkTools_getObjectArray
        (JNIEnv *env, jobject _obj, jobjectArray objarr) {

    //獲取
    //JNI提供了兩個(gè)函數(shù)來訪問對(duì)象數(shù)組弛针,GetObjectArrayElement返回?cái)?shù)組中指定位置的元素摧找,
    // SetObjectArrayElement修改數(shù)組中指定位置的元素。與基本類型不同的是枕荞,我們不能一次得到數(shù)據(jù)
    // 中的所有對(duì)象元素或者一次復(fù)制多個(gè)對(duì)象元素到緩沖區(qū)瓢宦。
    jobject objects = env->GetObjectArrayElement(objarr, 0);//返回第0個(gè)對(duì)象
    jsize length = (env)->GetArrayLength(objarr);

    //返回

    //申明一個(gè)object數(shù)組
    jobjectArray args = 0;
    //獲取object所屬類,一般為ava/lang/Object就可以了
    jclass objClass = (env)->FindClass("java/lang/Object");
    //新建object數(shù)組
    args = (env)->NewObjectArray(5, objClass, 0);


    //給每一個(gè)實(shí)例的變量付值碎连,并且將實(shí)例作為一個(gè)object,添加到objcet數(shù)組中
    for (int i = 0; i < 5; i++) {
        jclass cls = env->FindClass("com/dramascript/ndktest/User");
        jmethodID id = env->GetMethodID(cls, "<init>", "()V");

        jobject paramOut = env->NewObjectA(cls, id, 0);

        jfieldID intId = env->GetFieldID(cls, "age", "I");
        env->SetIntField(paramOut, intId, 23);

        jfieldID strId = env->GetFieldID(cls, "name", "Ljava/lang/String;");
        env->SetObjectField(paramOut, strId, (jstring)(env)->NewStringUTF("書戲"));

        //添加到objcet數(shù)組中
        (env)->SetObjectArrayElement(args, i, paramOut);
    }
    return args;
}

JNIEXPORT jobject

JNICALL Java_com_dramascript_ndktest_NdkTools_getObjectList
        (JNIEnv *env, jobject obj) {

    jclass list_cls = env->FindClass("Ljava/util/ArrayList;");//獲得ArrayList類引用
    jmethodID list_costruct = env->GetMethodID(list_cls, "<init>", "()V"); //獲得得構(gòu)造函數(shù)Id
    jobject list_obj = env->NewObject(list_cls, list_costruct); //創(chuàng)建一個(gè)Arraylist集合對(duì)象
    //或得Arraylist類中的 add()方法ID驮履,其方法原型為: boolean add(Object object) ;
    jmethodID list_add = env->GetMethodID(list_cls, "add", "(Ljava/lang/Object;)Z");

    jclass cls = env->FindClass("com/dramascript/ndktest/User");
    jmethodID id = env->GetMethodID(cls, "<init>", "()V");

    for (int i = 0; i < 3; i++) {
        jobject paramOut = env->NewObject(cls, id);
        env->CallBooleanMethod(list_obj, list_add, paramOut); //執(zhí)行Arraylist類實(shí)例的add方法鱼辙,添加一個(gè)stu對(duì)象
    }
    return list_obj;
}
  • C層代碼調(diào)用Java代碼的某個(gè)方法
JNIEXPORT void JNICALL Java_com_dramascript_ndktest_NdkTools_callSetData
(JNIEnv *env, jobject obj){

    jclass clz = env->FindClass("com/dramascript/ndktest/NdkTools");
    jmethodID mid = env->GetMethodID(clz, "setData", "(Ljava/lang/String;I)V");
    env->CallVoidMethod(obj, mid, env->NewStringUTF("haha in C ."),20);
    

}

注意:在C++中調(diào)用JNI頭文件的方法和在C代碼中調(diào)用的方法傳的參數(shù)是不一樣的,如下:

//在C++可以少env這個(gè)參數(shù)
 jfieldID intId = env->GetFieldID(paramInClass, "age", "I");

//在C里面是不能缺少的
 jfieldID intId = (*env)->GetFieldID(*env,paramInClass, "age", "I");

至此玫镐,結(jié)束倒戏,代碼在:Demo代碼,如果覺得不錯(cuò)恐似,給點(diǎn)打賞唄杜跷。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市矫夷,隨后出現(xiàn)的幾起案子葛闷,更是在濱河造成了極大的恐慌,老刑警劉巖双藕,帶你破解...
    沈念sama閱讀 212,816評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件淑趾,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡忧陪,警方通過查閱死者的電腦和手機(jī)扣泊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,729評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門近范,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人旷赖,你說我怎么就攤上這事顺又。” “怎么了等孵?”我有些...
    開封第一講書人閱讀 158,300評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵稚照,是天一觀的道長。 經(jīng)常有香客問我俯萌,道長果录,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,780評(píng)論 1 285
  • 正文 為了忘掉前任咐熙,我火速辦了婚禮弱恒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棋恼。我一直安慰自己返弹,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,890評(píng)論 6 385
  • 文/花漫 我一把揭開白布爪飘。 她就那樣靜靜地躺著义起,像睡著了一般。 火紅的嫁衣襯著肌膚如雪师崎。 梳的紋絲不亂的頭發(fā)上默终,一...
    開封第一講書人閱讀 50,084評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音犁罩,去河邊找鬼齐蔽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛床估,可吹牛的內(nèi)容都是我干的含滴。 我是一名探鬼主播,決...
    沈念sama閱讀 39,151評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼顷窒,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼蛙吏!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起鞋吉,我...
    開封第一講書人閱讀 37,912評(píng)論 0 268
  • 序言:老撾萬榮一對(duì)情侶失蹤鸦做,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后谓着,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體泼诱,經(jīng)...
    沈念sama閱讀 44,355評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,666評(píng)論 2 327
  • 正文 我和宋清朗相戀三年赊锚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了治筒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片屉栓。...
    茶點(diǎn)故事閱讀 38,809評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖耸袜,靈堂內(nèi)的尸體忽然破棺而出友多,到底是詐尸還是另有隱情,我是刑警寧澤堤框,帶...
    沈念sama閱讀 34,504評(píng)論 4 334
  • 正文 年R本政府宣布域滥,位于F島的核電站,受9級(jí)特大地震影響蜈抓,放射性物質(zhì)發(fā)生泄漏启绰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,150評(píng)論 3 317
  • 文/蒙蒙 一沟使、第九天 我趴在偏房一處隱蔽的房頂上張望委可。 院中可真熱鬧,春花似錦腊嗡、人聲如沸着倾。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,882評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽屈呕。三九已至,卻和暖如春棺亭,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背蟋软。 一陣腳步聲響...
    開封第一講書人閱讀 32,121評(píng)論 1 267
  • 我被黑心中介騙來泰國打工镶摘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人岳守。 一個(gè)月前我還...
    沈念sama閱讀 46,628評(píng)論 2 362
  • 正文 我出身青樓凄敢,卻偏偏與公主長得像,于是被迫代替她去往敵國和親湿痢。 傳聞我的和親對(duì)象是個(gè)殘疾皇子涝缝,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,724評(píng)論 2 351

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