初識 JNI

JNI 作為 Java/Kotlin(原生端) 同 C/C++ 端交互的工具猾蒂,是學(xué)習(xí) ffmpeg 的一個前提奠货,這邊做一個學(xué)習(xí)過程中的記錄。通過 Android Studio 可以快速創(chuàng)建一個 JNI 項(xiàng)目(創(chuàng)建時候選擇 Native C++ 即可夏伊,會自動配置 CMakeList 等文件)刑峡,該文基于 AS 3.5

loadLiabry

src 文件夾下相比一般的 AS 項(xiàng)目多了 cpp 文件夾,該文件夾下有一個 .cpp 文件和 CMakeLists.txt 文件阐污,.cpp 文件用來寫 native 端實(shí)現(xiàn)的方法休涤,CMakeLists 用來做些 cpp 的配置,目前可以忽略

main
│  AndroidManifest.xml
├─cpp
│      native-lib.cpp
│      CMakeLists.txt
├─java
│
├─res

接著在 MainActivity 中有這么一行代碼

    companion object {
        init {
            System.loadLibrary("native-lib")
        }
    }

通過 loadLibrary 方法笛辟,加載編譯的鏈接 so 庫功氨,so 庫的源碼就是前面提到的 native-lib.cpp 文件了

原生調(diào)用 cpp 方法

那么在 Kotlin 中如何調(diào)用 cpp 的方法呢,可以看到 MainActivity 中有一個使用 external 修飾的方法(如果是 java 則使用 native 關(guān)鍵詞修飾)

external fun stringFromJNI(): String

通過該方法手幢,會去調(diào)用 cpp 層的 native 方法捷凄,可以看下 native-lib.cpp 文件,內(nèi)部定義了一個方法

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

可以看到該方法的命名方式為 Java_包名_類名_方法名(包名的 . 替換成 _ 即可)围来,通過這種命名方式來查找 Kotlin 層的調(diào)用方法跺涤,該方法中 extern "C" 的作用是讓 C++ 支持 C 的方法匈睁,JNIEXPORT xxx JNICALL 代表這是一個 JNI 方法,xxx 表示返回的方法類型桶错,在 JNI 中航唆,都有 Kotlin 對應(yīng)的數(shù)據(jù)類型

JNI 數(shù)據(jù)類型

JNI 對應(yīng) Java 的數(shù)據(jù)類型如下,也可以直接查看 jni.h 頭文件

JNI類型 Java類型 類型描述
jboolean boolean 無符號8位
jbyte byte 無符號8位
jchar char 無符號16位
jshort short 有符號16位
jint int 有符號32位
jlong long 有符號64位
jfloat float 有符號32位
jdouble double 有符號64位

因?yàn)?String 不屬于基本類型牛曹,所以不定義在這佛点,需要返回 jsrting 類型,只能通過 char * 進(jìn)行相應(yīng)的轉(zhuǎn)換黎比,所以上述的函數(shù)中超营,使用 env->NewStringUTF(hello.c_str()) 方法,生成 jstring 并返回阅虫,然后在 Kotlin 層通過調(diào)用 stringFromJNI 方法就可以將 native 層返回的字符串顯示出來演闭,JNI 的基本使用就這么多啦,接著通過一些使用颓帝,熟悉一些方法米碰,比如實(shí)現(xiàn)字符串的拼接

external fun stringCat(a: String, b: String): String

回到 c++ 層做具體的實(shí)現(xiàn),前面提到因?yàn)樵?C++ 中字符串拼接不能直接通過 jstring 相加實(shí)現(xiàn)购城,需要通過 char * 進(jìn)行拼接吕座,所以就需要封裝一個 jstring2Char 的方法進(jìn)行轉(zhuǎn)換

char *jstring2Char(JNIEnv *env, jstring jstr) {
    char *rtn = nullptr;

    jclass clazz = env->FindClass("java/lang/String");
    jstring strenCode = env->NewStringUTF("UTF-8");
    jmethodID mid = env->GetMethodID(clazz, "getBytes", "(Ljava/lang/String;)[B");

    auto barr = (jbyteArray) (env->CallObjectMethod(jstr, mid, strenCode));
    jsize alen = env->GetArrayLength(barr);
    jbyte *ba = env->GetByteArrayElements(barr, JNI_FALSE);
    
    if (alen > 0) {
        // malloc(bytes) 方法分配 bytes 字節(jié),并返回這塊內(nèi)存的指針瘪板,
        // malloc 分配的內(nèi)存記得使用 free 進(jìn)行釋放吴趴,否則會內(nèi)存泄漏
        rtn = static_cast<char *>(malloc(static_cast<size_t>(alen + 1)));
        // memcpy(void*dest, const void *src, size_t n)
        // 由 src 指向地址為起始地址的連續(xù) n 個字節(jié)的數(shù)據(jù)復(fù)制到以 destin 指向地址為起始地址的空間內(nèi)
        memcpy(rtn, ba, static_cast<size_t>(alen));
        rtn[alen] = 0;
    }

    env->ReleaseByteArrayElements(barr, ba, 0);
    return rtn;
}

定義完轉(zhuǎn)換方法,直接調(diào)用即可侮攀,記得釋放內(nèi)存

extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_stringCat(JNIEnv *env, jobject, jstring a, jstring b){
    char *first = jstring2Char(env, a);
    char *second = jstring2Char(env, b);
    std::strcat(first, second);
    free(first);
    free(second);
    return env->NewStringUTF(first);
}

靜態(tài) JNI 方法

在很多情況下锣枝,都不會將 JNI 方法直接定義在 Activity,而是封裝到公共方法中兰英,方便調(diào)用撇叁,那么在公共方法類調(diào)用除了通過該類的實(shí)例,調(diào)用相應(yīng)方法畦贸,還有就是設(shè)置該方法為靜態(tài)方法陨闹,那么這種情況和上述有啥區(qū)別呢,其實(shí)區(qū)別不是很大薄坏,只需要將 native 端的方法中的參數(shù) jobject 替換成 jclass 即可正林,但是在 Kotlin 端,除了在半生對象中聲明該 native 方法颤殴,還需要增加 JvmStatic 注解才行觅廓,例如有如下的一個方法

class JniUtils {
    companion object {
        @JvmStatic
        external fun plus(a: Int, b: Int): Int
    }
}

那么在 native 端生成 JNI 方法和前面提到的類似,只需替換參數(shù)類型即可

extern "C" JNIEXPORT jint JNICALL
Java_com_xxx_JniUtils_plus(JNIEnv *env, jclass, jint , jint b){
    return a + b;
}

C++ 調(diào)用 Kotlin 方法

前面介紹了如何在 Kotlin 中調(diào)用 native 方法涵但,當(dāng)然杈绸,在 c++ 層也可以調(diào)用 Kotlin 層的方法帖蔓。假設(shè)在 MainActivity 中有一個 callMe(message: String)call(message:String) 方法,在調(diào)用 call 的時候瞳脓,同時內(nèi)部調(diào)用 callMe 方法塑娇,當(dāng)然直接調(diào)用很簡單,這邊通過 JNI 來實(shí)現(xiàn)

fun callMe(message: String){
    Log.e(TAG, message) // 只做簡單的打印
}

external fun call(message: String)

native 實(shí)現(xiàn) call 方法上面已經(jīng)介紹了劫侧,接下來介紹在 JNI 內(nèi)部調(diào)用 callMe 方法

extern "C" JNIEXPORT void JNICALL
Java_com_xxx_MainActivity_call(JNIEnv *env, jobject instance, jstring msg){
    const char *methodName = "callMe"; // 指定需要調(diào)用的方法名
    jclass clazz = env->FindClass("com.xxx.MainActivity"); //查找對應(yīng)的類埋酬,指定對應(yīng)的包名和類
    // 根據(jù)所在類和方法名查找方法的 ID,最后一個參數(shù)為方法的簽名烧栋,稍后做解釋
    jmethodID mid = env->GetMethodId(clazz, methodName, "(Ljava/lang/String;)V"); 
    env->CallVoidMethod(instance, mid, msg); // 根據(jù)返回的類型写妥,調(diào)用方法,傳入相應(yīng)參數(shù)
}

當(dāng) Kotlin 層調(diào)用 call 方法的時候审姓,就會通過 JNI 調(diào)用 callMe 方法珍特,執(zhí)行 callMe 的內(nèi)部邏輯。在上面提到了「簽名」這個東西魔吐,這邊列出簽名的表示方法

類型 簽名
boolean Z
byte B
char C
short S
int I
long J
float F
double D
void V
數(shù)組 [
String/Object Ljava/lang/String; Ljava/lang/Object;
普通類(com.example.className) Lcom/example/className;
嵌套類(com.example.className.Inner) Lcom/example/className$Inner;

所以方法的簽名的規(guī)則就是根據(jù)傳入的參數(shù)類型和返回的類型扎筒,替換成相應(yīng)的簽名即可,例如:call(Student s, int a): String 方法的簽名為 (Lcom/xxx/Student;I)Ljava/lang/String; 如果是內(nèi)部類則使用 $ 表示嵌套

C++ 獲取 Kotlin 的內(nèi)部參數(shù)

假設(shè)我們在 MainActivity 有個私有參數(shù) name酬姆,如果外部有個類需要獲取這個參數(shù)嗜桌,可以通過 MainActivty 內(nèi)部的共有方法來獲取,假如沒有這個共有方法該咋辦呢辞色,當(dāng)然我們可以通過 JNI 來獲取

extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_getField(JNIEnv *env, jobjcet instance){
    jclass clazz = env->FindClass("com.xxx.MainActivity"); // 根據(jù)類的包名來查找相應(yīng)的類
    // 根據(jù)類和參數(shù)名來獲取該參數(shù)骨宠,第三個參數(shù)為參數(shù)的簽名,即類型在 JNI 對應(yīng)的簽名
    jfieldID fid = env->GetFieldID(clazz, "name", "Ljava/lang/String;");
    // 因?yàn)?String 不是基本類型淫僻,所以只能通過 GetObjectField 進(jìn)行獲取诱篷,然后進(jìn)行強(qiáng)轉(zhuǎn)
    // 如果是 int 等基本類型壶唤,提供了 GetIntField 等獲取方法雳灵,auto 為可自行根據(jù)結(jié)果判斷類型
    auto name = (jstring)(env->GetObjectField(instance, fid));
    return name;
}

當(dāng)在外部通過 getField 方法即可獲取到該私有屬性,這個例子僅為例子而已...

C++ 獲取普通類的參數(shù)信息

假設(shè)我們有一個類闸盔,例如 Student 里面有一些名字悯辙,年齡等屬性,然后通過 JNI 將這些屬性轉(zhuǎn)成 String 返回迎吵,那么就需要涉及到獲取參數(shù)的字段信息了

// 定義一個普通類 Student
data class Student(val firstName: String, val lastName: String, val age: Int)

// 在 MAinActivity 定義一個轉(zhuǎn)換的方法
external fun printStudent(Student student): String

那么在 C++ 層就需要將 student 內(nèi)部的信息都獲取出來躲撰,并拼接到字符串,然后返回

extern "C" JNIEXPORT jstring JNICALL
Java_com_xxx_MainActivity_printStudent(JNIEnv *env, jobject, jobject student){
    jcalss clazz = env->GetObjectClass(student); // 獲取傳入?yún)?shù)對應(yīng)的類
    // 通過參數(shù)名和簽名击费,去對應(yīng)的 class 獲取相應(yīng)的 FieldID拢蛋,
    // 然后根據(jù) FiedlID 通過 GetObjectField 方法獲取對應(yīng)的屬性
    auto firstName = (jstring)(env->GetObjectField(student, env->GetFieldID(clazz, "firstName", "Ljava/lang/String;")));
    auto lastName = (jstring)(env->GetObjectField(student, env->GetFieldID(clazz, "lastName", "Ljava/lang/String;")));
    // int 為基本類型,可直接通過獲取對應(yīng)類型屬性的方法獲取
    auto age = env->GetIntField(student, env->GetFieldID(clazz, "age", "I"));
    
    char *cFirstName = jstring2Char(firstName);
    char *cLastName = jstring2Char(lastName);
    std::string cAge = std::to_string(age);
    
    strcat(cFirstName, " ");
    strcat(cFirstName, cLastName);
    strcat(cFirstName,  " is ");
    strcat(cFirstName, cAge.c_str());
    strcat(cFirstName, " years old");
    
    free(cFirstName);
    free(cLastName);
    
    return env->NewStringUTF(cFirstName);
}

當(dāng)外部調(diào)用 printStudent 方法的時候就會將 student 的屬性打印出來

動態(tài)注冊

在前面的 JNI 方法中蔫巩,每個方法都需要寫很長的一段類名谆棱,非常容易出錯快压,那么能不能省略包名呢,當(dāng)然是可以垃瞧,通過動態(tài)注冊就可以讓這個麻煩的方法名變得簡略

動態(tài)注冊蔫劣,需要指定一個方法列表,用來存放同個包名下的方法个从,存放的方式如下:

{ Kotlin 層方法名脉幢, 方法前面, JNI 函數(shù)指針} // 函數(shù)指針固定為 ```(void *) JNI 方法名```

例如我們前面提到的方法嗦锐,放到一個列表中

static JNINativeMethod jniMethods[] = {
    {"stringFromJNI", "()Ljava/lang/String;", (void *) stringFromJNI},
    {"stringCat", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;", (void *) stringCat},
    {"call", "(Ljava/lang/String;)V", (void *) call},
    {"getField", "()Ljava/lang/String;", (void *) getField},
    {"printStudent", "(Lcom/xxx/Student;)Ljava/lang/String;", (void *) printStudent},
};

接著就是需要注冊這些方法了嫌松,封裝一個通用的方法,注冊成功返回 JNI_TRUE 否則 JNI_FALSE

static int registerNativeMethods(JNIEnv *env, const char *className, 
                                 JNINativeMethod *getMethods, int sumNum){
    jclass clazz = env->FindClass(className); // 根據(jù)類名去查找相應(yīng)類意推,包含 JNINativeMethod 列表所有方法

    if (clazz == nullptr) return JNI_FALSE; // 未找到 class 則認(rèn)為注冊失敗

    // 根據(jù)所有的方法名和數(shù)量進(jìn)行注冊豆瘫,如果結(jié)果返回小于 0 則認(rèn)為注冊失敗
    if (env->RegisterNatives(clazz, getMethods, methodSum) < 0) return JNI_FALSE;

    return JNI_TRUE;
}

接著就需要實(shí)現(xiàn) JNI_OnLoad 方法(定義在 jni.h 頭文件中),對上述的方法進(jìn)行注冊菊值,該方法會返回一個版本號

JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *reversed) {
    JNIEnv *env = nullptr;

    // 檢測環(huán)境失敗返回 -1
    if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) {
        return -1;
    }

    assert(env != nullptr);

    // 注冊失敗返回 -1
    if (!registerNativeMethods(
            env, jniClazz, jniMethods, sizeof(jniMethods) / sizeof(jniMethods[0]))) {
        return -1;
    }

    return JNI_VERSION_1_6;
}

這樣幾步就完成了 JNI 方法的動態(tài)注冊外驱,只需要全局定義 className 即可,不需要每次都在方法聲明完整包路徑

內(nèi)存釋放

C++ 中腻窒,非常重要的一步就是內(nèi)存釋放昵宇,否則就會造成內(nèi)存泄漏,分分鐘給你炸開

哪些需要手動釋放
  • 不需要手動釋放(基本類型):jint儿子,jlong 等等
  • 需要手動釋放(引用類型瓦哎,數(shù)組家族):jstring,jobject 柔逼,jobjectArray蒋譬,jintArray ,jclass 愉适,jmethodID
釋放方法(該部分參考自《JNI手動釋放內(nèi)存》)
  • jstring & char *
    // 創(chuàng)建 jstring 和 char*
    jstring jstr = (jstring)(jniEnv->CallObjectMethod(jniEnv, mPerson, getName));
    char* cstr = (char*) jniEnv->GetStringUTFChars(jniEnv,jstr, 0);
     
    // 釋放
    jniEnv->ReleaseStringUTFChars(jniEnv, jstr, cstr);
    jniEnv->DeleteLocalRef(jniEnv, jstr);jbyteArray audioArray = jnienv->NewByteArray(frameSize);
     
    jnienv->DeleteLocalRef(audioArray)
    
  • jobject犯助,jobjectArray,jclass 维咸,jmethodID 等引用類型
    jniEnv->DeleteLocalRef(jniEnv, XXX);
    
  • jbyteArray
    jbyteArray arr = jnienv->NewByteArray(frameSize);
    jnienv->DeleteLocalRef(arr);
    
  • GetByteArrayElements
    jbyte* array= jniEnv->GetByteArrayElements(env,jarray,&isCopy);
    jniEnv->ReleaseByteArrayElements(env,jarray,array,0);
    
  • NewGlobalRef
    jobject ref= env->NewGlobalRef(customObj);
    env->DeleteGlobalRef(customObj);
    

舉個例子

Android 中剂买,經(jīng)常需要用到 Context 獲取一些相關(guān)的信息,這邊舉個獲取屏幕信息的例子

#include <jni.h>
#include <string>
#include <iostream>
#include <android/log.h>

#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, "JNI", __VA_ARGS__)

// 獲取當(dāng)前的 Context
jobject getAndroidApplication(JNIEnv *env) {
    jclass activityThreadClazz = env->FindClass("android/app/ActivityThread");

    jmethodID jCurrentActivityThread =
            env->GetStaticMethodID(activityThreadClazz,
                                   "currentActivityThread", "()Landroid/app/ActivityThread;");

    jobject currentActivityThread =
            env->CallStaticObjectMethod(activityThreadClazz, jCurrentActivityThread);

    jmethodID jGetApplication =
            env->GetMethodID(activityThreadClazz, "getApplication", "()Landroid/app/Application;");

    return env->CallObjectMethod(currentActivityThread, jGetApplication);
}

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

    jobject instance = getAndroidApplication(env);
    jclass contextClazz = env->GetObjectClass(instance);
    // 獲取 `getResources` 方法
    jmethodID getResources = env->GetMethodID(contextClazz, "getResources",
                                              "()Landroid/content/res/Resources;");

    jobject resourceInstance = env->CallObjectMethod(instance, getResources);
    jclass resourceClazz = env->GetObjectClass(resourceInstance);
    // 獲取 Resources 下的 `getDisplayMetrics` 方法
    jmethodID getDisplayMetrics = env->GetMethodID(resourceClazz, "getDisplayMetrics",
                                                   "()Landroid/util/DisplayMetrics;");

    jobject metricsInstance = env->CallObjectMethod(resourceInstance, getDisplayMetrics);
    jclass metricsClazz = env->GetObjectClass(metricsInstance);

    // 獲取 DisplayMetrics 下的一些參數(shù)
    jfieldID densityId = env->GetFieldID(metricsClazz, "density", "F");
    jfloat density = env->GetFloatField(metricsInstance, densityId);

    jfieldID widthId = env->GetFieldID(metricsClazz, "widthPixels", "I");
    jint width = env->GetIntField(metricsInstance, widthId);

    jfieldID heightId = env->GetFieldID(metricsClazz, "heightPixels", "I");
    jint height = env->GetIntField(metricsInstance, heightId);

    LOGE("get density: %f, width: %d, height: %d", density, width, height);
}

目前使用到的就那么多啦癌蓖,后面有更多的方法涉及到瞬哼,再進(jìn)行添加,Enjoy it ~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末租副,一起剝皮案震驚了整個濱河市坐慰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌用僧,老刑警劉巖结胀,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件两残,死亡現(xiàn)場離奇詭異,居然都是意外死亡把跨,警方通過查閱死者的電腦和手機(jī)人弓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來着逐,“玉大人崔赌,你說我怎么就攤上這事∷时穑” “怎么了健芭?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長秀姐。 經(jīng)常有香客問我慈迈,道長,這世上最難降的妖魔是什么省有? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任痒留,我火速辦了婚禮,結(jié)果婚禮上蠢沿,老公的妹妹穿的比我還像新娘伸头。我一直安慰自己,他們只是感情好舷蟀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布恤磷。 她就那樣靜靜地躺著,像睡著了一般野宜。 火紅的嫁衣襯著肌膚如雪扫步。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天匈子,我揣著相機(jī)與錄音河胎,去河邊找鬼。 笑死旬牲,一個胖子當(dāng)著我的面吹牛仿粹,可吹牛的內(nèi)容都是我干的搁吓。 我是一名探鬼主播原茅,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼堕仔!你這毒婦竟也來了擂橘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤摩骨,失蹤者是張志新(化名)和其女友劉穎通贞,沒想到半個月后朗若,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡昌罩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年哭懈,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片茎用。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡遣总,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出轨功,到底是詐尸還是另有隱情旭斥,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布古涧,位于F島的核電站垂券,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏羡滑。R本人自食惡果不足惜菇爪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柒昏。 院中可真熱鬧娄帖,春花似錦、人聲如沸昙楚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽堪旧。三九已至削葱,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間淳梦,已是汗流浹背析砸。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留爆袍,地道東北人首繁。 一個月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像陨囊,于是被迫代替她去往敵國和親弦疮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

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