iOS-底層原理 04:內(nèi)存對齊

計(jì)算內(nèi)存方法

首先我們要知道計(jì)算內(nèi)存大小的三種方式:

  • sizeof
  • class_getInstanceSize
  • malloc_size缀拭。

接下來我們定義一個LGPerson類智厌,分析這三種方法盲赊。代碼如下:

LGPerson * p = [LGPerson alloc];
LGPerson * q;
NSLog(@"對象類型占用內(nèi)存大小=%lu",sizeof(p));
NSLog(@"對象類型占用內(nèi)存大小=%lu",sizeof(q));
NSLog(@"對象實(shí)際內(nèi)存大小====%lu",class_getInstanceSize([p class]));
NSLog(@"對象實(shí)際內(nèi)存大小====%lu",class_getInstanceSize([q class]));
NSLog(@"對象實(shí)際分配內(nèi)存大小=%lu",malloc_size((__bridge const void *)(p)));
NSLog(@"對象實(shí)際分配內(nèi)存大小=%lu",malloc_size((__bridge const void *)(q)));

打印結(jié)果:

2020-09-29 14:02:17.810194+0800 KCObjc[20870:761876] 對象類型占用內(nèi)存大小=8
2020-09-29 14:02:17.810897+0800 KCObjc[20870:761876] 對象類型占用內(nèi)存大小=8
2020-09-29 14:02:17.811068+0800 KCObjc[20870:761876] 對象實(shí)際內(nèi)存大小====8
2020-09-29 14:02:17.811165+0800 KCObjc[20870:761876] 對象實(shí)際內(nèi)存大小====0
2020-09-29 14:02:17.811265+0800 KCObjc[20870:761876] 對象實(shí)際分配內(nèi)存大小=16
2020-09-29 14:02:17.811352+0800 KCObjc[20870:761876] 對象實(shí)際分配內(nèi)存大小=0

由打印結(jié)果可以分析出

  • sizeof()傳入是類型诚卸,可以放基本數(shù)據(jù)類型绘迁、對象缀台、指針√鸥可用來計(jì)算類型占用內(nèi)存大小,這個在編譯器編譯階段就會確定辩涝,所以sizeof(p)sizeof(q)的結(jié)果都是一樣的勘天,p和q都是指針類型捉邢,指針大小為8個字節(jié)伏伐。
  • class_getInstanceSize計(jì)算對象的實(shí)際內(nèi)存大小翘狱,大小由類的屬性和變量來決定,實(shí)際上并不是嚴(yán)格意義上的對象內(nèi)存大小。由下面代碼可知茬缩,底層進(jìn)行8字節(jié)對齊凰锡。
#   define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}

LGPerson類中沒有其他的屬性和變量圈暗,但是繼承了NSObject员串,NSObject中有一個isa指針寸齐,所以內(nèi)存大小是8字節(jié)。

  • malloc_size系統(tǒng)分配的內(nèi)存大小扰法,是按16字節(jié)對齊的方式塞颁,即是按16的倍數(shù)分配 祠锣,不足則系統(tǒng)會自動填充字節(jié)珍语。

內(nèi)存對齊原則

每個特定平臺上的編譯器都有自己的默認(rèn)“對齊系數(shù)”(也叫對齊模數(shù))板乙。程序員可以通過預(yù)編譯命令#pragma pack(n)拳氢,n=1,2,4,8,16來改變這一系數(shù)馋评,其中的n就是你要指定的“對齊系數(shù)”留特。在iOS中蜕青,Xcode默認(rèn)為#pragma pack(8)糊渊,即`8字節(jié)對齊渺绒。
內(nèi)存對齊原則主要有以下三點(diǎn):

  • 數(shù)據(jù)成員對齊規(guī)則struct(結(jié)構(gòu))或者union(聯(lián)合)的數(shù)據(jù)成員宗兼,第一個數(shù)據(jù)成員放在offset為0的地方殷绍,以后每個數(shù)據(jù)成員存儲的起始位置要從該成員大小或者成員的子成員大小(只要該成員有子成員殖侵,比如數(shù)據(jù)拢军、結(jié)構(gòu)體等)的整數(shù)倍開始(例如int在32位機(jī)中是4字節(jié)怔鳖,則要從4的整數(shù)倍地址開始存儲)
  • 數(shù)據(jù)成員為結(jié)構(gòu)體:如果一個結(jié)構(gòu)里有某些結(jié)構(gòu)體成員结执,則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲(例如:struct a里面存有struct b献幔,b里面有char(1字節(jié))、int(4字節(jié))蹬蚁、double(8字節(jié))等元素,則b應(yīng)該從8的整數(shù)倍開始存儲)
  • 結(jié)構(gòu)體的整體對齊規(guī)則:結(jié)構(gòu)體的總大小贝乎,即sizeof的結(jié)果览效,必須是其內(nèi)部做大成員的整數(shù)倍锤灿,不足的要補(bǔ)齊

下表是各種數(shù)據(jù)類型在iOS中的占用內(nèi)存大小,根據(jù)對應(yīng)類型來計(jì)算結(jié)構(gòu)體中內(nèi)存大小


數(shù)據(jù)類型對應(yīng)的字節(jié)數(shù)表格

結(jié)構(gòu)體對齊

如下代碼衡招,我們用實(shí)例進(jìn)行探究結(jié)構(gòu)體對齊:

struct LGStruct1{
    long    a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
} LGStruct1;

struct LGStruct2{
    long    a; // 8
    char    d; // 1
    int     b; // 4
    short   c; // 2
} LGStruct2;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"---%lu------%lu",sizeof(LGStruct1),sizeof(LGStruct2));
    }
    return 0;
}

打印結(jié)果

2020-09-29 15:52:17.811352+0800 KCObjc[20870:761876] -----16------24

由上述代碼可看出兩個結(jié)構(gòu)體定義的變量和變量類型都一致,唯一的區(qū)別只是在于定義變量的順序不一致空执,那么為什么會占用的內(nèi)存大小不相等呢穗椅?其實(shí)這就是iOS中的內(nèi)存對齊原則匹表。下面我們就根據(jù)內(nèi)存對齊原則來進(jìn)行簡單的分析和計(jì)算LGStruct1內(nèi)存大小的詳細(xì)過程:

  • 變量a: 占8個字節(jié)默蚌,從0開始苇羡,min(0设江,8)叉存,即0 ~ 7存儲a
  • 變量b: 占4個字節(jié)歼捏,從8開始笨篷,min(8冕屯,4)安聘,即8 ~ 11存儲b
  • 變量c: 占2個字節(jié)浴韭,從12開始脯宿,min(12,2)榴芳,即12~ 13 存儲c
  • 變量d: 占1個字節(jié)窟感,從14開始歉井,min(14,1)躏嚎,即14存儲d

因此LGStruct1的內(nèi)存大小是15字節(jié)卢佣,而LGStruct1中最大的變量是a8個字節(jié)菜谣,所以LGStruct1需要實(shí)際內(nèi)存必須是8的倍數(shù)(內(nèi)存對齊原則)珠漂,15字節(jié)不是8的倍數(shù),15向上取整到16 尾膊,所以系統(tǒng)自動填充成16字節(jié)媳危,最終sizeof(LGStruct1)的大小是16.

image.png

LGStruct2內(nèi)存大小的詳細(xì)過程

  • 變量a: 占8個字節(jié),從0開始冈敛,min(0待笑,8),即0 ~ 7存儲a
  • 變量d: 占1個字節(jié)抓谴,從8開始暮蹂,min(8荆陆,1),即8 存儲d
  • 變量b: 占4個字節(jié)浓体,從9開始,min(9生闲,4)飞醉,9 % 4 != 0,繼續(xù)往后移動直到找到可以整除4的位置12min(12失暂,4),即12 ~ 15 存儲b
  • 變量c: 占2個字節(jié),從16開始系宫,min(16椒惨,2),即16 ~ 17存儲c
    因此LGStruct2的需要的內(nèi)存大小為18字節(jié),而LGStruct2中最大變量long的字節(jié)數(shù)為8,所以LGStruct2實(shí)際的內(nèi)存大小必須是8的整數(shù)倍忱叭,18向上取整到24,主要是因?yàn)?4是8的整數(shù)倍撵彻,所以 sizeof(LGStruct2) 的結(jié)果是24
    LGStruct2內(nèi)存中的存儲情況圖

結(jié)構(gòu)體嵌套結(jié)構(gòu)體

上面的2個示例只是簡單的定義數(shù)據(jù)成員碗短,如果我們在結(jié)構(gòu)體中嵌套結(jié)構(gòu)體結(jié)果又會是怎樣的?我們繼續(xù)探究巡雨,看下面代碼:

struct LGStruct3{
    long    a; // 8
    int     b; // 4
    short   c; // 2
    char    d; // 1
    struct LGStruct1 Str;
}LGStruct3;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSLog(@"LGStruct1----%lu",sizeof(LGStruct1));
        NSLog(@"LGStruct2----%lu",sizeof(LGStruct2));
        NSLog(@"LGStruct3----%lu",sizeof(LGStruct3));
    }
    return 0;
}
//結(jié)果
2020-09-30 11:02:46.509957+0800 001-內(nèi)存對齊原則[22800:939799] LGStruct1----16
2020-09-30 11:02:46.511196+0800 001-內(nèi)存對齊原則[22800:939799] LGStruct2----24
2020-09-30 11:02:46.512537+0800 001-內(nèi)存對齊原則[22800:939799] LGStruct3----32

LGStruct3內(nèi)存大小存儲情況的詳細(xì)過程

  • 變量a: 占8個字節(jié)炕舵,從0開始,min(0奸攻,8)部翘,即0 ~ 7存儲a
  • 變量b: 占4個字節(jié),從8開始,min(8假哎,4),即8 ~ 11存儲b
  • 變量c: 占2個字節(jié)秆剪,從12開始仅讽,min(12,2)苫费,即12~ 13 存儲b
  • 變量d: 占1個字節(jié)牍汹,從14開始嫁蛇,min(14闸拿,1)书幕,即14存儲d
  • 變量Str: 結(jié)構(gòu)體變量Str苟呐,根據(jù)內(nèi)存對齊原則結(jié)構(gòu)體成員要從其內(nèi)部最大元素大小的整數(shù)倍地址開始存儲笆呆,LGStruct1中最大的變量是long 8字節(jié)榕堰,所以Str16位置開始存儲踱讨,而Str的為15字節(jié)味混,即LGStruct1存儲16-31位置

因此LGStruct3的內(nèi)存大小是32字節(jié)夕土,而LGStruct1中最大變量為Str馆衔,其最大成員內(nèi)存字節(jié)數(shù)為8,所以LGStruct3內(nèi)存必須是8的倍數(shù)未蝌,328的倍數(shù),最終sizeof(LGStruct3)的大小是32
其內(nèi)存存儲情況如下圖所示

結(jié)構(gòu)體嵌套結(jié)構(gòu)體的內(nèi)存存儲情況圖

內(nèi)存優(yōu)化(屬性重排)

從上述的示例中绊袋,我們可以得出一個結(jié)論即結(jié)構(gòu)體的內(nèi)存大小與結(jié)構(gòu)體成員內(nèi)存大小的順序有關(guān)

  • 結(jié)構(gòu)體數(shù)據(jù)成員是由內(nèi)存從小到大的順序定義的,根據(jù)內(nèi)存對齊原則來計(jì)算內(nèi)存大小蹋笼,需要增加較多的內(nèi)存占位符胶滋,這樣做浪費(fèi)內(nèi)存。
  • 結(jié)構(gòu)體數(shù)據(jù)成員是由內(nèi)存從大到小的順序定義的,根據(jù)內(nèi)存對齊規(guī)則來計(jì)算結(jié)構(gòu)體內(nèi)存大小作煌,我們只需要補(bǔ)齊少量內(nèi)存占位符即可滿足內(nèi)存對齊規(guī)則赚瘦。

第二種方式就是蘋果采用的將類中的屬性進(jìn)行重排起意,來達(dá)到優(yōu)化內(nèi)存的目的亲善。以下面這個示例來進(jìn)行說明蘋果中屬性重排,即內(nèi)存優(yōu)化:

  • 自定義LGPerson類托享,并定義幾個屬性
//LGPerson.h
@interface LGPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, copy) NSString *nickName;
// @property (nonatomic, copy) NSString *hobby;
@property (nonatomic, assign) int age;
@property (nonatomic, assign) long height;

@property (nonatomic) char c1;
@property (nonatomic) char c2;
@end

//LGPerson.m
@implementation LGPerson

@end
  • main中創(chuàng)建LGPerson的實(shí)例對象肤视,并對其屬性賦值
int main(int argc, char * argv[]) {
    @autoreleasepool {
        LGPerson *person = [LGPerson alloc];
        person.name      = @"Cooci";
        person.nickName  = @"KC";
        person.age       = 18;
        person.c1        = 'a';
        person.c2        = 'b';

        NSLog(@"%@",person);
    }
    return 0;
}
  • 斷點(diǎn)調(diào)試person,根據(jù)LGPerson的對象地址吗跋,查找出屬性的值

    • 通過地址找出 name &nickName
      image.png
  • 通過0x0000001200006261地址找出age等數(shù)據(jù)時侧戴,發(fā)現(xiàn)無法找出age等數(shù)據(jù)值,這是因?yàn)?code>蘋果中針對age跌宛、c1酗宋、c2屬性的內(nèi)存進(jìn)行了重排,將他們存儲在同一塊內(nèi)存中,

    • age通過0x00000012讀取
    • c1通過0x61讀冉小(a的ASCII碼是97)
    • c2通過0x62讀韧擅ā(b的ASCII碼是98)
      image.png
  • 特殊的doublefloat
    我們嘗試把LGPerson中的height屬性類型修改為double,并賦值

@property (nonatomic, assign) double height;
//賦值身高
person.height    = 178;

image.png

我們發(fā)現(xiàn)直接po打印0x4066400000000000,打印不出height的數(shù)值178哎迄。 這是因?yàn)榫幾g器po打印默認(rèn)當(dāng)做int類型處理回右。p/x (double)178:我們以16進(jìn)制打印double類型值打印稀颁,發(fā)現(xiàn)完全相同

  • 綜上總結(jié)蘋果中的內(nèi)存對齊思想:
    1. 大部分的內(nèi)存都是通過固定的內(nèi)存塊進(jìn)行讀取楣黍。
    2. 盡管我們在內(nèi)存中采用了內(nèi)存對齊的方式,但并不是所有的內(nèi)存都可以進(jìn)行浪費(fèi)的棱烂,蘋果會自動對屬性進(jìn)行重排租漂,以此來優(yōu)化內(nèi)存.

8字節(jié)對齊與16字節(jié)對齊

前面我們提及了8字節(jié)對齊16字節(jié)對齊泳姐,這時我們就有疑問沮焕,什么時候在哪里采用哪種字節(jié)對齊贞瞒,接下來我們繼續(xù)源碼探索

  • 我們在objc4源碼中搜索class_getInstanceSize倦卖,可以在runtime.h找到:
/** 
 * Returns the size of instances of a class.
 * 
 * @param cls A class object.
 * 
 * @return The size in bytes of instances of the class \e cls, or \c 0 if \e cls is \c Nil.
 */
OBJC_EXPORT size_t
class_getInstanceSize(Class _Nullable cls) 
    OBJC_AVAILABLE(10.5, 2.0, 9.0, 1.0, 2.0);

objc-class.mm可以找到:

size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}

進(jìn)入alignedInstanceSize:

    // Class's ivar size rounded up to a pointer-size boundary.
    uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }

進(jìn)入word_align

#ifdef __LP64__ // 64位操作系統(tǒng)
#   define WORD_SHIFT 3UL
#   define WORD_MASK 7UL  // 7字節(jié)遮罩
#   define WORD_BITS 64 
#else 
#   define WORD_SHIFT 2UL
#   define WORD_MASK 3UL
#   define WORD_BITS 32
#endif

static inline uint32_t word_align(uint32_t x) {
    // (x + 7) & (~7)  --> 8字節(jié)對齊
    return (x + WORD_MASK) & ~WORD_MASK;
}

可以看到:

  • 系統(tǒng)內(nèi)部設(shè)定64位操作系統(tǒng)兔毒,統(tǒng)一使用8字節(jié)對齊信粮。對于一個對象來說姨俩,其真正的對齊方式是8字節(jié)對齊匆赃。
  • 因外部處理對象太多鸟赫,系統(tǒng)為了防止一些容錯蒜胖,會采用align16內(nèi)存塊來存取,主要是因?yàn)椴捎?字節(jié)對齊時抛蚤,兩個對象的內(nèi)存會緊挨著台谢,顯得比較緊湊,而16字節(jié)比較寬松岁经,避免越界訪問朋沮,提高效率,利于蘋果以后的擴(kuò)展缀壤。

16字節(jié)內(nèi)存對齊算法

目前已知的16字節(jié)內(nèi)存對齊算法有兩種

  • alloc源碼分析中的align16
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
  • malloc源碼分析中的segregated_size_to_fit
#define SHIFT_NANO_QUANTUM      4
#define NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM)   // 16

static MALLOC_INLINE size_t
segregated_size_to_fit(nanozone_t *nanozone, size_t size, size_t *pKey)
{
    size_t k, slot_bytes;

    if (0 == size) {
        size = NANO_REGIME_QUANTA_SIZE; // Historical behavior
    }
    k = (size + NANO_REGIME_QUANTA_SIZE - 1) >> SHIFT_NANO_QUANTUM; // round up and shift for number of quanta
    slot_bytes = k << SHIFT_NANO_QUANTUM;                           // multiply by power of two quanta size
    *pKey = k - 1;                                                  // Zero-based!

    return slot_bytes;
}

算法原理:k + 15 >> 4 << 4 樊拓,其中右移4 + 左移4相當(dāng)于將后4位抹零,跟 k/16 * 16一樣 塘慕,是16字節(jié)對齊算法筋夏,小于16就成0了
以 k = 2為例,如下圖所示

malloc中16字節(jié)對齊算法原理

為什么需要16字節(jié)對齊

原因有一下幾點(diǎn):

  • 通常內(nèi)存是由一個個字節(jié)組成的苍糠,cpu在存取數(shù)據(jù)時叁丧,并不是以字節(jié)為單位存儲,而是以為單位存取岳瞭,塊的大小為內(nèi)存存取力度拥娄。頻繁存取字節(jié)未對齊的數(shù)據(jù),會極大降低cpu的性能瞳筏,所以可以通過減少存取次數(shù)降低cpu的開銷稚瘾,同時使訪問更安全,不會產(chǎn)生訪問混亂的情況姚炕。
  • 16字節(jié)對齊摊欠,是由于在一個對象中丢烘,第一個屬性isa占8字節(jié),當(dāng)然一個對象肯定還有其他屬性些椒,當(dāng)無屬性時播瞳,會預(yù)留8字節(jié),即16字節(jié)對齊免糕,如果不預(yù)留赢乓,相當(dāng)于這個對象的isa和其他對象的isa緊挨著,容易造成訪問混亂石窑。

總結(jié)

綜合前文提及的獲取內(nèi)存大小的方式

  • class_getInstanceSize:是采用8字節(jié)對齊牌芋,參照的對象的屬性內(nèi)存大小
  • malloc_size:采用16字節(jié)對齊,參照的整個對象的內(nèi)存大小松逊,對象實(shí)際分配的內(nèi)存大小必須是16的整數(shù)倍
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末躺屁,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子经宏,更是在濱河造成了極大的恐慌犀暑,老刑警劉巖,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烛恤,死亡現(xiàn)場離奇詭異母怜,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)缚柏,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門苹熏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人币喧,你說我怎么就攤上這事轨域。” “怎么了杀餐?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵干发,是天一觀的道長。 經(jīng)常有香客問我史翘,道長枉长,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任琼讽,我火速辦了婚禮必峰,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘钻蹬。我一直安慰自己吼蚁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布问欠。 她就那樣靜靜地躺著肝匆,像睡著了一般粒蜈。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上旗国,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天枯怖,我揣著相機(jī)與錄音,去河邊找鬼能曾。 笑死嫁怀,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的借浊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼萝招,長吁一口氣:“原來是場噩夢啊……” “哼蚂斤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起槐沼,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤曙蒸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后岗钩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體纽窟,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年兼吓,在試婚紗的時候發(fā)現(xiàn)自己被綠了臂港。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡视搏,死狀恐怖审孽,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情浑娜,我是刑警寧澤佑力,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站筋遭,受9級特大地震影響打颤,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜漓滔,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一编饺、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧次和,春花似錦反肋、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽罕邀。三九已至,卻和暖如春养距,著一層夾襖步出監(jiān)牢的瞬間诉探,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工棍厌, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留肾胯,地道東北人。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓耘纱,卻偏偏與公主長得像敬肚,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子束析,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355