Bitmap創建過程

前言

在安卓中我們圖片相關的操作一定離不開Bitmap位圖,可見Bitmap在圖像顯示中的位置是多么的重要,而且Bitmap操作不當即會引發OOM,因此我詳細復習了一下Bitmap相關的知識點在此記錄一下。

Bitmap介紹

位圖(Bitmap),又稱柵格圖(英語:Raster graphics)或點陣圖,是使用像素陣列(Pixel-array/Dot-matrix點陣)來表示的圖像。自2005年Skia被Google收購后,一直相當神秘低調,直到2007年初,Skia GL相關的源代碼才被揭露,作為Google Android平臺的圖形引擎,稍后的Google Chrome瀏覽器也采用Skia引擎。隨著Android與Chrome (開放版本稱為"Chromium")兩大專案公布源代碼后,Skia也一并公開原始源代碼,以Apache License v2發布(注意,這意味著與GPLv2授權不相容) ,而Android與Chrome的源代碼庫中都有一份[Skia]的復制,因需求不同,做了部份的修改,比方說Chrome專案底下的 [chrome/trunk/src/skia],需要注意的是,Skia本身是不涉及底層環境,如Linux Framebuffer或Gtk+銜接的處理,這也是何以Android (透過Linux Framebuffer)與Chrome (開發中的Linux版本使用Gtk+)需要提供一份修改,以便系統接軌。 [1]

Bitmap創建過程 (Android N)

Bitmap創建

創建bitmap有很多的api,將這些api歸類下,大致分文三種創建形式。

  • 根據現有的Bitmap創建新的Bitmap
/**
* 通過矩陣的方式,返回原始 Bitmap 中的一個不可變子集。新 Bitmap 可能返回的就是原始的 Bitmap,也可能還是復制出來的。
* 新 Bitmap 與原始 Bitmap 具有相同的密度(density)和顏色空間;
*
* @param source   原始 Bitmap
* @param x        在原始 Bitmap 中 x方向的其起始坐標(你可能只需要原始 Bitmap x方向上的一部分)
* @param y        在原始 Bitmap 中 y方向的其起始坐標(你可能只需要原始 Bitmap y方向上的一部分)
* @param width    需要返回 Bitmap 的寬度(px)(如果超過原始Bitmap寬度會報錯)
* @param height   需要返回 Bitmap 的高度(px)(如果超過原始Bitmap高度會報錯)
* @param m        Matrix類型,表示需要做的變換操作
* @param filter   是否需要過濾,只有 matrix 變換不只有平移操作才有效
*/
public static Bitmap createBitmap(@NonNull Bitmap source, int x, int y, int width, int height,
            @Nullable Matrix m, boolean filter) 
  • 根據顏色像素數組創建空的Bitmap
/**
 *
 * 返回具有指定寬度和高度的不可變位圖,每個像素值設置為colors數組中的對應值。
 * 其初始密度由給定的確定DisplayMetrics。新創建的位圖位于sRGB 顏色空間中。
 * @param display  顯示將顯示此位圖的顯示的度量標準
 * @param colors   用于初始化像素的sRGB數組
 * @param offset   顏色數組中第一個顏色之前要跳過的值的數量
 * @param stride   行之間數組中的顏色數(必須> = width或<= -width)
 * @param width    位圖的寬度
 * @param height   位圖的高度
 * @param config   要創建的位圖配置。如果配置不支持每像素alpha(例如RGB_565),
 * 那么colors []中的alpha字節將被忽略(假設為FF)
 */
public static Bitmap createBitmap(@NonNull DisplayMetrics display,
        @NonNull @ColorInt int[] colors, int offset, int stride,
        int width, int height, @NonNull Config config)
  • 對現有Bitmap進行縮放處理
/**
* 對Bitmap進行縮放,縮放成寬 dstWidth、高 dstHeight 的新Bitmap
*/
public static Bitmap createScaledBitmap(@NonNull Bitmap src, int dstWidth, int dstHeight,boolean filter)

至此我們將這15種創建函數進行了行為分類成這三種模式。經過一輪追溯,我們看下這三個創建方式最終會調用到Bitmap#createBitmap()的方法之中。

public static Bitmap createBitmap(@Nullable DisplayMetrics display, int width, int height,
            @NonNull Config config, boolean hasAlpha, @NonNull ColorSpace colorSpace) {
        // 省略邏輯異常語句...
        // 最終會調用到這個JNI方法中
        Bitmap bm = nativeCreate(null, 0, width, width, height, config.nativeInt, true,
                colorSpace == null ? 0 : colorSpace.getNativeInstance());

        if (display != null) {
            bm.mDensity = display.densityDpi;
        }
        bm.setHasAlpha(hasAlpha);
        if ((config == Config.ARGB_8888 || config == Config.RGBA_F16) && !hasAlpha) {
            nativeErase(bm.mNativePtr, 0xff000000);
        }
        return bm;
    }

到這里我們發現最終的Bitmap的創建是交由JNI調用Native方法進行時機的處理。ok 我們分析下Native的部分。我們找下JNI引用橋代碼

static const JNINativeMethod gBitmapMethods[] = {
    {   "nativeCreate",             "([IIIIIIZJ)Landroid/graphics/Bitmap;",
        (void*)Bitmap_creator },
    {   "nativeCopy",               "(JIZ)Landroid/graphics/Bitmap;",
        (void*)Bitmap_copy },
    {   "nativeCopyAshmem",         "(J)Landroid/graphics/Bitmap;",
        (void*)Bitmap_copyAshmem },
    {   "nativeCopyAshmemConfig",   "(JI)Landroid/graphics/Bitmap;",
        (void*)Bitmap_copyAshmemConfig },
    {   "nativeGetNativeFinalizer", "()J", (void*)Bitmap_getNativeFinalizer },
    {   "nativeRecycle",            "(J)V", (void*)Bitmap_recycle },
    {   "nativeReconfigure",        "(JIIIZ)V", (void*)Bitmap_reconfigure },
    {   "nativeCompress",           "(JIILjava/io/OutputStream;[B)Z",
        (void*)Bitmap_compress },
    {   "nativeErase",              "(JI)V", (void*)Bitmap_erase },
    {   "nativeErase",              "(JJJ)V", (void*)Bitmap_eraseLong },
    {   "nativeRowBytes",           "(J)I", (void*)Bitmap_rowBytes },
    {   "nativeConfig",             "(J)I", (void*)Bitmap_config },
    {   "nativeHasAlpha",           "(J)Z", (void*)Bitmap_hasAlpha },
    {   "nativeIsPremultiplied",    "(J)Z", (void*)Bitmap_isPremultiplied},
    {   "nativeSetHasAlpha",        "(JZZ)V", (void*)Bitmap_setHasAlpha},
    {   "nativeSetPremultiplied",   "(JZ)V", (void*)Bitmap_setPremultiplied},
    {   "nativeHasMipMap",          "(J)Z", (void*)Bitmap_hasMipMap },
    {   "nativeSetHasMipMap",       "(JZ)V", (void*)Bitmap_setHasMipMap },
    {   "nativeCreateFromParcel",
        "(Landroid/os/Parcel;)Landroid/graphics/Bitmap;",
        (void*)Bitmap_createFromParcel },
    {   "nativeWriteToParcel",      "(JILandroid/os/Parcel;)Z",
        (void*)Bitmap_writeToParcel },
    {   "nativeExtractAlpha",       "(JJ[I)Landroid/graphics/Bitmap;",
        (void*)Bitmap_extractAlpha },
    {   "nativeGenerationId",       "(J)I", (void*)Bitmap_getGenerationId },
    {   "nativeGetPixel",           "(JII)I", (void*)Bitmap_getPixel },
    {   "nativeGetColor",           "(JII)J", (void*)Bitmap_getColor },
    {   "nativeGetPixels",          "(J[IIIIIII)V", (void*)Bitmap_getPixels },
    {   "nativeSetPixel",           "(JIII)V", (void*)Bitmap_setPixel },
    {   "nativeSetPixels",          "(J[IIIIIII)V", (void*)Bitmap_setPixels },
    {   "nativeCopyPixelsToBuffer", "(JLjava/nio/Buffer;)V",
                                            (void*)Bitmap_copyPixelsToBuffer },
    {   "nativeCopyPixelsFromBuffer", "(JLjava/nio/Buffer;)V",
                                            (void*)Bitmap_copyPixelsFromBuffer },
    {   "nativeSameAs",             "(JJ)Z", (void*)Bitmap_sameAs },
    {   "nativePrepareToDraw",      "(J)V", (void*)Bitmap_prepareToDraw },
    {   "nativeGetAllocationByteCount", "(J)I", (void*)Bitmap_getAllocationByteCount },
    {   "nativeCopyPreserveInternalConfig", "(J)Landroid/graphics/Bitmap;",
        (void*)Bitmap_copyPreserveInternalConfig },
    {   "nativeWrapHardwareBufferBitmap", "(Landroid/hardware/HardwareBuffer;J)Landroid/graphics/Bitmap;",
        (void*) Bitmap_wrapHardwareBufferBitmap },
    {   "nativeGetHardwareBuffer", "(J)Landroid/hardware/HardwareBuffer;",
        (void*) Bitmap_getHardwareBuffer },
    {   "nativeComputeColorSpace",  "(J)Landroid/graphics/ColorSpace;", (void*)Bitmap_computeColorSpace },
    {   "nativeSetColorSpace",      "(JJ)V", (void*)Bitmap_setColorSpace },
    {   "nativeIsSRGB",             "(J)Z", (void*)Bitmap_isSRGB },
    {   "nativeIsSRGBLinear",       "(J)Z", (void*)Bitmap_isSRGBLinear},
    {   "nativeSetImmutable",       "(J)V", (void*)Bitmap_setImmutable},

    // ------------ @CriticalNative ----------------
    {   "nativeIsImmutable",        "(J)Z", (void*)Bitmap_isImmutable}
};
Bitmap.cpp # Bitmap_creator()
static jobject Bitmap_creator(JNIEnv* env, jobject, jintArray jColors,
                              jint offset, jint stride, jint width, jint height,
                              jint configHandle, jboolean isMutable,
                              jlong colorSpacePtr) {
    // 1
    SkColorType colorType = GraphicsJNI::legacyBitmapConfigToColorType(configHandle);
    if (NULL != jColors) {
        size_t n = env->GetArrayLength(jColors);
        if (n < SkAbs32(stride) * (size_t)height) {
            doThrowAIOOBE(env);
            return NULL;
        }
    }
    // ARGB_4444 is a deprecated format, convert automatically to 8888
    if (colorType == kARGB_4444_SkColorType) {
        colorType = kN32_SkColorType;
    }
    sk_sp<SkColorSpace> colorSpace;
    if (colorType == kAlpha_8_SkColorType) {
        colorSpace = nullptr;
    } else {
        colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr);
    }
    SkBitmap bitmap;
    // 2
    bitmap.setInfo(SkImageInfo::Make(width, height, colorType, kPremul_SkAlphaType,
                colorSpace));
    // 3 
    Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef(env, &bitmap, NULL);
    if (!nativeBitmap) {
        ALOGE("OOM allocating Bitmap with dimensions %i x %i", width, height);
        doThrowOOME(env);
        return NULL;
    }
    if (jColors != NULL) {
        GraphicsJNI::SetPixels(env, jColors, offset, stride, 0, 0, width, height, &bitmap);
    }
    // 4
    return GraphicsJNI::createBitmap(env, nativeBitmap,getPremulBitmapCreateFlags(isMutable));

代碼塊中的1處 將位圖格式轉換 RGB_565 ARGB_8888 等轉換為skia域的顏色類型kBGRA_8888_SkColorType,而ARGB_4444由于顯示質量原因被標記過時,在進行Skia顏色轉換的時候被強制轉換為kBGRA_8888_SkColorType。

enum SkColorType {
    kUnknown_SkColorType,      //!< uninitialized
    kAlpha_8_SkColorType,      //!< pixel with alpha in 8-bit byte
    kRGB_565_SkColorType,      //!< pixel with 5 bits red, 6 bits green, 5 bits blue, in 16-bit word
    kARGB_4444_SkColorType,    //!< pixel with 4 bits for alpha, red, green, blue; in 16-bit word
    kRGBA_8888_SkColorType,    //!< pixel with 8 bits for red, green, blue, alpha; in 32-bit word
    kRGB_888x_SkColorType,     //!< pixel with 8 bits each for red, green, blue; in 32-bit word
    kBGRA_8888_SkColorType,    //!< pixel with 8 bits for blue, green, red, alpha; in 32-bit word
    kRGBA_1010102_SkColorType, //!< 10 bits for red, green, blue; 2 bits for alpha; in 32-bit word
    kRGB_101010x_SkColorType,  //!< pixel with 10 bits each for red, green, blue; in 32-bit word
    kGray_8_SkColorType,       //!< pixel with grayscale level in 8-bit byte
    kRGBA_F16_SkColorType,   //!< pixel with half floats for red, green, blue, alpha; in 64-bit word
    kRGBA_F32_SkColorType,     //!< pixel using C float for red, green, blue, alpha; in 128-bit word
    kLastEnum_SkColorType     = kRGBA_F32_SkColorType,//!< last valid value
#if SK_PMCOLOR_BYTE_ORDER(B,G,R,A)
    kN32_SkColorType          = kBGRA_8888_SkColorType,//!< native ARGB 32-bit encoding
#elif SK_PMCOLOR_BYTE_ORDER(R,G,B,A)
    kN32_SkColorType          = kRGBA_8888_SkColorType,//!< native ARGB 32-bit encoding
#else
    #error "SK_*32_SHIFT values must correspond to BGRA or RGBA byte order"
#endif
};

根據以上的枚舉類,我們知道所有的Skia圖片顯示格式的種類以及對應的字節大小。這里將在后續計算Bitmap所占內存大小的計算起到重中之重的角色。之后回到代碼2處,SkImageInfo::Make()入參中的width、height、colorType為后續計算bitmap大小起到提供數據的作用,make()函數創建出來了SkImageInfo,這個對象存入了SkBitmap中。fWidth的賦值是一個關鍵點,后面Java層通過getAllocationByteCount獲取Bitmap內存占用中會用到它計算一行像素占用空間,再用一行的所占用的控件乘以高度(行數)就是對應的總量。代碼3處為SkBitmap bitmap;變量進行分配指定的地址空間,代碼4 調用JNI方法,并創建處Bitmap對象。我們代碼走一編3、4步驟。


Graphics.cpp
int register_android_graphics_Graphics(JNIEnv* env)
{
    jmethodID m;
    jclass c;
    gVMRuntime_class = MakeGlobalRefOrDie(env, FindClassOrDie(env, "dalvik/system/VMRuntime"));
    m = env->GetStaticMethodID(gVMRuntime_class, "getRuntime", "()Ldalvik/system/VMRuntime;");
    gVMRuntime = env->NewGlobalRef(env->CallStaticObjectMethod(gVMRuntime_class, m));
    gVMRuntime_newNonMovableArray = GetMethodIDOrDie(env, gVMRuntime_class, "newNonMovableArray","(Ljava/lang/Class;I)Ljava/lang/Object;");
    gVMRuntime_addressOf = GetMethodIDOrDie(env, gVMRuntime_class, "addressOf", "(Ljava/lang/Object;)J");
    return 0;
}

第一個紅框調用了VMRuntime的newNonMovableArray方法,拿到虛擬機分配Heap對象,再調用VMRuntime的addressOf方法拿到其對應的內存地址,這回再調用Native方法的Bitmap.cpp構造方法,創建出Bitmap對象。而后又調用了getSkitmap(SkBitmap* outBitmap)方法。

Bitmap.cpp

空間分配以后,進行調用Native層的Bitmap構造方法,new android::Bitmap(env, arrayObj, (void*) addr, info, rowBytes, ctable),這里能夠看到mPixelStorage保存之前分配后的Heap對象的弱引用,jstrongRef在構造方法中先被初始化為null。

Bitmap.cpp

之后調用getSkBitmap,setPixelRef()方法中對強指針進行了賦值


Bitmap.cpp
Bitmap.cpp

強指針被指向這個Heap對象。總結一下,native層的Bitmap構造函數,mPixelStorage保存前面創建Heap對象的弱引用,mPixelRef指向WrappedPixelRef。outBitmap拿到mPixelRef強引用對象,這里理解為拿到SkBitmap對象。Bitmap* nativeBitmap = GraphicsJNI::allocateJavaPixelRef完成Bitmap Heap分配,創建native層Bitmap,SkBitmap對象,最后自然是創建Java層Bitmap對象,把該包的包上。native層是通過JNI方法,在Java層創建一個數組對象的,這個數組是對應在Java層的Bitmap對象的buffer數組,所以pixels還是保存在Java堆。而在native層這里它是通過weak指針來引用的,在需要的時候會轉換為strong指針,用完之后又去掉strong指針,這樣這個數組對象還是能夠被Java堆自動回收。里面jstrongRef一開始是賦值為null的,但是在bitmap的getSkBitmap方法會使用weakRef給他賦值。
因此證明了,Android N 版本Bitmap對象是分配在Dalvik堆上。

jobject GraphicsJNI::createBitmap(JNIEnv* env, android::Bitmap* bitmap,
        int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets,
        int density) {
    bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable;
    bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied;
    // The caller needs to have already set the alpha type properly, so the
    // native SkBitmap stays in sync with the Java Bitmap.
    assert_premultiplied(bitmap->info(), isPremultiplied);

    jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID,
            reinterpret_cast<jlong>(bitmap), bitmap->javaByteArray(),
            bitmap->width(), bitmap->height(), density, isMutable, isPremultiplied,
            ninePatchChunk, ninePatchInsets);
    hasException(env); // For the side effect of logging.
    return obj;
}

之后將創建好的Bitmap對象返回。

Bitmap創建過程 (Android O)

流程和N差不多

Bitmap.cpp

sk_sp<Bitmap> Bitmap::allocateHeapBitmap(SkBitmap* bitmap) {
   return allocateBitmap(bitmap, &android::allocateHeapBitmap);
}

static sk_sp<Bitmap> allocateBitmap(SkBitmap* bitmap, AllocPixelRef alloc) {
            const SkImageInfo& info = bitmap->info();
            if (info.colorType() == kUnknown_SkColorType) {
                LOG_ALWAYS_FATAL("unknown bitmap configuration");
                return nullptr;
            }
        
            size_t size;
        
            // we must respect the rowBytes value already set on the bitmap instead of
            // attempting to compute our own.
            const size_t rowBytes = bitmap->rowBytes();
            if (!computeAllocationSize(rowBytes, bitmap->height(), &size)) {
                return nullptr;
            }
        
            // 進行分配內存
            auto wrapper = alloc(size, info, rowBytes);
            if (wrapper) {
                wrapper->getSkBitmap(bitmap);
            }
            return wrapper;
        }

這里調用alloc(size, info, rowBytes)來進行內存分配。alloc是通過參數傳遞進來的,其實它是一個函數指針,我們來看它的定義。

typedef sk_sp<Bitmap> (*AllocPixelRef)(size_t allocSize, const SkImageInfo& info, size_t rowBytes);

static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) {
    // 真正的在Native層進行內存分配
    void* addr = calloc(size, 1);
    if (!addr) {
        return nullptr;
    }
    return sk_sp<Bitmap>(new Bitmap(addr, size, info, rowBytes));
}

之后再回溯到主流程,之后正常調用createBitmap方法,那么就與Android N 版本后半部分流程一致了。


Bitmap.cpp

到這里我們就可以區分出來了Bitmap在不同Android版本下,對內存分配的差異做了比較。這里我們再提一下,在C語言中的內存分配函數比較。

C中分配內存的函數主要有兩個,malloc()和calloc()。
  • 參數區別
void *__cdecl calloc(size_t _NumOfElements,size_t _SizeOfElements);
void *__cdecl malloc(size_t _Size);

malloc函數:malloc(size_t size)函數有一個參數,即要度分配的內存空間的大小。
calloc函數:calloc(size_t numElements,size_t sizeOfElement)有兩個參數,分別為元素的數目和每個元素的大小,這兩個參數的乘積就是要分配的內存空間的大小。

  • 初始化內存空間上的區問別
    malloc函數:不能初始化所分配的內存空間,在動態分配完內存后,里邊答數據是隨機的垃圾數據。
    calloc函數:能初始化所分配的內存空間,在動態分配完內存后,自動初始化該內存空間為零。

  • 函數返回值上的區別
    malloc函數:函數返回值是一個對象。
    calloc函數:函數返回值是一個數組。

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

推薦閱讀更多精彩內容