OC底層原理03-內存對齊

該文章是對前一篇 OC底層原理01-alloc流程探索cls->instanceSize(計算內存大小)分支的一個拓展和深入

一带饱、查看內存地址

1.1 下斷點
image.png
1.2 用x 打印地址元媚,既可以x該對象疼蛾,也可以x該對象的指針地址
image.png
1.3 上面打印的一串16進制就是內存地址了,前面八位就是存的isa愉舔,但是因為iOS是小端儲存方式钢猛,所以應該倒著讀,那isa的地址實際為:
image.png
1.4 為了方便好看我們優(yōu)化一下打印命令指令:x/4gx轩缤,按照每8位的格式打印4組
image.png
1.5 那我們來po一下看看他里面到底存的什么東西
image.png
1.6 原來我們賦值的屬性存到這里的命迈,證明我們確實找到了他的存儲位置,那么為什么第一行第二組全是0呢火的?
image.png
1.7 原來是因為還有age,height,char1,char2沒賦值躺翻,為了驗證,我們將他們都賦值打印看看情況
image.png
1.8 我們發(fā)現(xiàn)當所有屬性都賦值過后卫玖,沒有出現(xiàn)0x000000000000的情況了公你,證明我們上面的猜想正確。那我們的age,char1,char2假瞬,去哪了呢陕靠?是在0x0000001200004241中嗎?但是他又是一串亂碼數(shù)字脱茉,接下來就是見證奇跡的時刻
image.png
1.9 為什么A變成了65剪芥,B變成了66呢?聰明的你肯定猜到了琴许,因為字符會以ASCII碼的形式存在內存税肪,那么問題又來了:

對象是以16字節(jié)對齊的方式開辟空間,它真正需要的內存是8字節(jié)對齊榜田,那為什么age,char1,char2被存到了一組8字節(jié)的內存里面了呢益兄?

結論:系統(tǒng)進行了內存優(yōu)化,重排箭券,類的本質是結構體净捅,引入我們下一個探討話題

二、結構體中的內存對齊

2.1 C/OC 中各種數(shù)據(jù)類型的大小
c oc 32位 64位
bool BOOL(64位) 1 1
signed char (_ _signed char)int8_t辩块、BOOL(32位) 1 1
unsigned char Boolean 1 1
short int16_t 2 2
unsigned short unichar 2 2
int蛔六、int32_t NSInteger(32位)、 boolean_t(32位) 4 4
unsigned int boolean_t(64位)废亭、NSUInteger(32位) 4 4
long NSInteger(64位) 4 8
unsigned long NSUInteger(64位) 4 8
long long int64_t 8 8
float CGFloat(32位) 4 4
double CGFloat(64位) 8 8
2.2 探究結構體的內存大小
2.2.1 創(chuàng)建兩個結構體国章,并打印它的內存大小
struct GomuStruct1 {
    double a;
    char b;
    int c;
    short d;
} struct1;

struct GomuStruct2 {
    double a;
    int c;
    char b;
    short d;
} struct2;
NSLog(@"%lu - %lu",sizeof(struct1),sizeof(struct2)); 
//: log: 24 - 16
  • 為什么只是交換了c,b的位置豆村,結構體的大小就變了液兽,24顯然也不是16的倍數(shù),說明結構體字節(jié)對齊和對象的字節(jié)對齊規(guī)則不相同
2.2.2 結構體的內存對齊規(guī)則一:

結構體/聯(lián)合體中你画,第一個數(shù)據(jù)成員以offset為0的地方開始抵碟,長度為成員大小桃漾,結束位置如果是第二個成員大小的整數(shù)倍,那么第二個數(shù)據(jù)成員就以此位置開始拟逮,長度為成員大小撬统,若非第二個成員大小的整數(shù)倍,往后推移敦迄,直到該數(shù)字是第二個成員大小的整數(shù)倍恋追,作為第二個成員的開始位置,第三個以此內推罚屋。

2.2.3 結構體的內存對齊規(guī)則二:

結構體的最終大小苦囱,也就是sizeof的結果,必須是其內部最大成員大小的整數(shù)倍脾猛,不足就要補齊撕彤。

//: 通過上面規(guī)則一計算結構體內存大小,與打印一致
struct GomuStruct1 {
    double a; // 8 (0~7)
    char b;   // 1 (8)[1,8],8是1的整數(shù)倍
    int c;    // 4 (12~15)[4,9],9不是4的整數(shù)倍猛拴,往后推移到 10羹铅,11,12
    short d;  // 2 (16~17)[2,16],16是2的整數(shù)倍
} struct1;

// 內部需要的大小為:18(0~17)
// 最大屬性:a 8字節(jié)
// 結構體整數(shù)倍:8的整數(shù)倍:24(18不足補位)[通過上面規(guī)則二得出]

//: 通過上面規(guī)則一計算結構體內存大小愉昆,與打印一致
struct GomuStruct2 {
    double a; // 8 (0~7)
    int c;    // 4 (8~11),[4,8],8是4的整數(shù)倍
    char b;   // 1 (11~12),[1,12],12是1的整數(shù)倍
    short d;  // 2 (14~15),[2,13],13不是2的整數(shù)倍职员,往后推移到 14
} struct2;

// 內部需要的大小為:16(0~15)
// 最大屬性:a 8字節(jié)
// 結構體整數(shù)倍:8的整數(shù)倍:16(16是8的整數(shù)倍)[通過上面規(guī)則二得出]

注意:結構體指針的內存大小8字節(jié),而結構體內部的內存大小是根據(jù)內部成員來進行開辟的跛溉。

2.2.4 結構體的內存對齊規(guī)則三:(針對嵌套結構體)

結構體中嵌套結構體焊切,結構體成員要從其內部最大元素大小的整數(shù)倍地址開始存儲。

struct GomuStruct2 {
    double a; // 8 (0~7)
    int c;    // 4 (8~11),[4,8],8是4的整數(shù)倍
    char b;   // 1 (12),[1,12],12是1的整數(shù)倍
    short d;  // 2 (14~15),[2,13],13不是2的整數(shù)倍芳室,往后推移到 14
} struct2;

struct GomuStruct3 {
    double a; // 8 (0~7)
    int c;    // 4 (8~11),[4,8],8是4的整數(shù)倍
    char b;   // 1 (12),[1,12],12是1的整數(shù)倍
    short d;  // 2 (14~15),[2,13],13不是2的整數(shù)倍专肪,往后推移到 14
    struct GomuStruct2 struct2; // 8 (16,31),[8,16],16是8的整數(shù)倍,往后推移到struct2的大小16的位置:31
} struct3; // (0~31) 32渤愁,32是最大8的整數(shù)倍牵祟,所以為32
2.2.5 結構體的內存對齊規(guī)則四:(針對嵌套結構體)

如果把結構體成員放在第一位,那它依然遵守結構體內存規(guī)則抖格,從offset為0的地方開始

struct GomuStruct2 {
    double a; // 8 (0~7)
    int c;    // 4 (8~11),[4,8],8是4的整數(shù)倍
    char b;   // 1 (12),[1,12],12是1的整數(shù)倍
    short d;  // 2 (14~15),[2,13],13不是2的整數(shù)倍,往后推移到 14
} struct2;

struct GomuStruct3 {
    struct GomuStruct2 struct2; // 8 (0~15)
    char b; // 1 (16)
} struct3; // (0~16) 17咕晋,17不是最大8的整數(shù)倍雹拄,補齊所以為24
2.2.6 結構體的內存對齊規(guī)則五:(針對嵌套結構體)

結構體中嵌套結構體,最后結構體的大小不是結構體成員大小的整數(shù)倍掌呜,而是結構體成員中的成員(即嵌套結構體中的最大成員)的大小/結構體中的最大成員(結構體中除去嵌套結構體的最大成員)的大小的整數(shù)倍滓玖。如上面,24是struct3 的大小质蕉,16是struct2 的大小势篡,24不是16的整數(shù)倍翩肌,他是struct2中 double a 的大小8的整數(shù)倍

2.2.7 根據(jù)結構體內存對齊規(guī)則畫出struct1的內存圖如下:
image.png

為什么要這么分配?
以空間換時間禁悠,方便讀取念祭。

2.2.8 結構體內存讀取步驟:
  • 以最大成員大小 這里的struct1 是 8 字節(jié)為尺度讀取,分成三段碍侦,0~7粱坤,8~1516~23瓷产。
  • 然后再在每個尺度中按照該尺度的最大成員去讀取站玄,如第一段(0~7),最大是8濒旦,則直接讀出a(0~7)株旷,第二段(8~15),最大是4尔邓,讀出8~11晾剖,12~15,讀出c(12~15)铃拇。
  • 再在8~11中取最大1钞瀑,則讀出b:(8)。
  • 以此類推慷荔,也可以讀出d

三雕什、類的內存開辟規(guī)則

3.1 分別打印sizeof,class_getInstanceSize,malloc_size

GomuPerson *person = [GomuPerson alloc];
person.name = @"Gomu";
person.nickName = @"iOS";
NSLog(@"%@ - %lu - %lu - %lu",person,sizeof(person),class_getInstanceSize([GomuPerson class]),malloc_size((__bridge const void *)(person)));
//: <GomuPerson: 0x10050ca10> - 8 - 40 - 48
  • sizeof打印的是person這個指針所占的內存大小:8
  • class_getInstanceSize打印的是GomuPerson對象所需要的真正內存:40
  • malloc_size打印的則是系統(tǒng)給person開辟的內存:48
  • 成員變量显晶、方法不占用對象內存贷岸。

3.2 class_getInstanceSize中的內存開辟規(guī)則

3.2.1 進入objc4-781.2源碼,搜索class_getInstanceSize
image.png
3.2.2 進入class_getInstanceSize方法
size_t class_getInstanceSize(Class cls)
{
    if (!cls) return 0;
    return cls->alignedInstanceSize();
}
3.2.3 進入alignedInstanceSize方法
uint32_t alignedInstanceSize() const {
        return word_align(unalignedInstanceSize());
    }
3.2.3 進入word_align方法
#   define WORD_MASK 7UL
static inline uint32_t word_align(uint32_t x) {
    return (x + WORD_MASK) & ~WORD_MASK;
}
// 舉例  x 為 2
// x + WORD_MASK 2 + 7 = 9
// 轉換成2進制                      0000 1001
// WORD_MASK = 7 轉換成二級制       0000 0111
// ~WORD_MASK                     1111 1000
// (x + WORD_MASK) & ~WORD_MASK
// 0000 1001
// 1111 1000
// 0000 1000 //因為7取反后磷雇,后三位肯定是0偿警,與任何值&運算,后三位都為0
// 所以該算法的結果必然是8的倍數(shù)
  • WORD_MASK這個宏是7唯笙。
  • (x + WORD_MASK) & ~WORD_MASK 這是一個取8的倍數(shù)的算法螟蒸。
  • 對象中真正的對齊方式是8字節(jié)對齊。

3.3 alloc初始化三部曲中崩掘,第一步cls->instanceSize(extraBytes) 中的內存開辟規(guī)則

//: cls->instanceSize(extraBytes)七嫌,該方法最終會來到`align16 `方法
//: 該算法在前一篇中已經講解,這里就不過多贅述
static inline size_t align16(size_t x) {
    return (x + size_t(15)) & ~size_t(15);
}
  • cls->instanceSize(extraBytes)方法是取16的倍數(shù)的一個算法苞慢。
  • alloc初始化三部曲中诵原,第一步就是上述方法,計算對象的內存空間。
  • 系統(tǒng)給對象分配內存的方式是16字節(jié)對齊绍赛。

3.4 alloc初始化三部曲中蔓纠,第二步calloc中的內存開辟規(guī)則

3.4.1 由符號斷點得出,calloclibsystem_malloc.dylib庫中
image.png
3.4.2 由于當前源碼在libobjc.A.dylib中吗蚌,所以我們無法進入源碼調試calloc腿倚,為了研究calloc,配置了一份libsystem_malloc.dylib的可編譯源碼

libsystem_malloc可編譯源碼

3.4.3 開始調式褪测,創(chuàng)建一個calloc對象
void *p = calloc(1, 32); //: 在這里打個斷點開始調試
3.4.4 進入calloc(size_t num_items, size_t size)
void *
calloc(size_t num_items, size_t size)
{
    void *retval;
    retval = malloc_zone_calloc(default_zone, num_items, size);
    if (retval == NULL) {
        errno = ENOMEM;
    }
    return retval;
}
  • 重點:default_zone猴誊,(下回分解)
3.4.5 進入malloc_zone_calloc(default_zone, num_items, size)
void *
malloc_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_START, (uintptr_t)zone, num_items, size, 0);

    void *ptr;
    if (malloc_check_start && (malloc_check_counter++ >= malloc_check_start)) {
        internal_check();
    }

    ptr = zone->calloc(zone, num_items, size);
    
    if (malloc_logger) {
        malloc_logger(MALLOC_LOG_TYPE_ALLOCATE | MALLOC_LOG_TYPE_HAS_ZONE | MALLOC_LOG_TYPE_CLEARED, (uintptr_t)zone,
                (uintptr_t)(num_items * size), 0, (uintptr_t)ptr, 0);
    }

    MALLOC_TRACE(TRACE_calloc | DBG_FUNC_END, (uintptr_t)zone, num_items, size, (uintptr_t)ptr);
    return ptr;
}
3.4.6 到zone->calloc(zone, num_items, size)這里就沒了
image.png
  • 當不能再進入的時候,在zone->calloc(zone, num_items, size)處加個斷點
    image.png
  • 執(zhí)行po zone->calloc侮措,拿到下個流程default_zone_calloc
3.4.7 進入default_zone_calloc
static void *
default_zone_calloc(malloc_zone_t *zone, size_t num_items, size_t size)
{
    zone = runtime_default_zone();
    return zone->calloc(zone, num_items, size);
}
  • 又出現(xiàn)zone->calloc(zone, num_items, size)懈叹,同樣的方法我們拿到下一個流程nano_calloc
3.4.8 進入nano_calloc
static void *
nano_calloc(nanozone_t *nanozone, size_t num_items, size_t size)
{
    size_t total_bytes;

    if (calloc_get_size(num_items, size, 0, &total_bytes)) {
        return NULL;
    }

    if (total_bytes <= NANO_MAX_SIZE) {
        void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
        if (p) {
            return p;
        } else {
            /* FALLTHROUGH to helper zone */
        }
    }
    malloc_zone_t *zone = (malloc_zone_t *)(nanozone->helper_zone);
    return zone->calloc(zone, 1, total_bytes);
}
  • 定位到關鍵代碼void *p = _nano_malloc_check_clear(nanozone, total_bytes, 1);
3.4.9 進入_nano_malloc_check_clear
static void *
_nano_malloc_check_clear(nanozone_t *nanozone, size_t size, boolean_t cleared_requested)
{
    MALLOC_TRACE(TRACE_nano_malloc, (uintptr_t)nanozone, size, cleared_requested, 0);

    void *ptr;
    size_t slot_key;
    size_t slot_bytes = segregated_size_to_fit(nanozone, size, &slot_key); // Note slot_key is set here
    mag_index_t mag_index = nano_mag_index(nanozone);

    nano_meta_admin_t pMeta = &(nanozone->meta_data[mag_index][slot_key]);

    ptr = OSAtomicDequeue(&(pMeta->slot_LIFO), offsetof(struct chained_block_s, next));
    if (ptr) {... 該部分代碼省略}else {
        ptr = segregated_next_block(nanozone, pMeta, slot_bytes, mag_index);
    }

    if (cleared_requested && ptr) {
        memset(ptr, 0, slot_bytes); // TODO: Needs a memory barrier after memset to ensure zeroes land first?
    }
    return ptr;
}
  • 別忘了我們的目的,探索calloc開辟內存的規(guī)則分扎,所以抓住size不放澄成,得到核心方法segregated_size_to_fit
3.4.10 進入核心方法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;
}
  • SHIFT_NANO_QUANTUM 的值是4
  • NANO_REGIME_QUANTA_SIZE (1 << SHIFT_NANO_QUANTUM) 這個的值1往左移4位,即 0000 0001 -> 0001 0000 畏吓,所以他的值是16
  • 第一步墨状,如果size為0,給size賦值16
  • 第二步菲饼,size+NANO_REGIME_QUANTA_SIZE - 1肾砂,即 先 + 15 ,然后往右移 4 位
  • 第三步宏悦,再往左移 4 位
//: 舉例:假如 size = 4
//: 4+15 = 21 ,  二進制為:           0001 0101
//: 右移4位:                        0000 0001
//: 做移4位:                        0001 0000 
//: 結果為16
//: 左移4位镐确,再右移4位,即對后4位置0饼煞,那得到的結果必然為16的倍數(shù)
  • 這個算法和(x + size_t(15)) & ~size_t(15)這個作用一樣源葫。
  • 探索calloc再次說明,開辟內存的時候砖瞧,系統(tǒng)是以16字節(jié)對齊進行開辟息堂。

四、拓展

4.1 sizeof
  • sizeof 是一個運算符块促,并不是一個函數(shù)荣堰。
  • 用于計算參數(shù)的類型大小,而不是參數(shù)值
int a = 1;
double b = 2.0;
GomuPerson *person = [GomuPerson alloc];
        
NSLog(@"%lu - %lu - %lu",sizeof(a),sizeof(b),sizeof(person));
NSLog(@"%lu - %lu - %lu",sizeof(int),sizeof(double),sizeof(void *));
//: 打印都為 : 4 - 8 - 8
//: sizeof(a) 等價于 sizeof(int)
//: sizeof(b) 等價于 sizeof(double)
//: sizeof(person)  等價于 sizeof(void *) person 是一個結構體指針
4.2 LLDB 調試常用指令
(lldb) po person
<GomuPerson: 0x10108e740>
(lldb) p person
(GomuPerson *) $24 = 0x000000010108e740
(lldb) x person
0x10108e740: b5 23 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  .#..............
0x10108e750: 10 10 00 00 01 00 00 00 30 10 00 00 01 00 00 00  ........0.......
(lldb) memory read person
0x10108e740: b5 23 00 00 01 80 1d 00 00 00 00 00 00 00 00 00  .#..............
0x10108e750: 10 10 00 00 01 00 00 00 30 10 00 00 01 00 00 00  ........0.......
(lldb) x/4gx person
0x10108e740: 0x001d8001000023b5 0x0000000000000000
0x10108e750: 0x0000000100001010 0x0000000100001030
(lldb) x/4gw person
0x10108e740: 0x000023b5 0x001d8001 0x00000000 0x00000000
(lldb) p/t 15
(int) $29 = 0b00000000000000000000000000001111
(lldb) p/x 15
(int) $30 = 0x0000000f
(lldb) 
  • po:打印對象
  • p: 打印基本數(shù)據(jù)類型
  • xmemory read : 以16進制的方式竭翠,打印對象的內存情況
  • x/4gx :以16進制的方式持隧,打印4段8字節(jié)的內存地址
  • x/4gw :以16進制的方式,打印4段4字節(jié)的內存地址
  • p/t : 轉換成2進制打印
  • p/x : 轉換成16進制打印
4.3 double/float 在內存的存儲方式
  • double/float 會轉成16進制存逃片,直接po不出來
//: person.height = 180.1; 給height定義成double
(lldb) x/6gx person
0x10182b960: 0x001d8001000033c5 0x0000001200000000
0x10182b970: 0x0000000100002010 0x0000000100002030
0x10182b980: 0x4066833333333333 0x0000000000000000
(lldb) po 0x4066833333333333
4640540721977439027
(lldb) p/x 180.1
(double) $13 = 0x4066833333333333
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子褥实,更是在濱河造成了極大的恐慌呀狼,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件损离,死亡現(xiàn)場離奇詭異哥艇,居然都是意外死亡,警方通過查閱死者的電腦和手機僻澎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門貌踏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人窟勃,你說我怎么就攤上這事祖乳。” “怎么了秉氧?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵眷昆,是天一觀的道長。 經常有香客問我汁咏,道長亚斋,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任攘滩,我火速辦了婚禮帅刊,結果婚禮上,老公的妹妹穿的比我還像新娘漂问。我一直安慰自己赖瞒,他們只是感情好,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布级解。 她就那樣靜靜地躺著冒黑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪勤哗。 梳的紋絲不亂的頭發(fā)上抡爹,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天,我揣著相機與錄音芒划,去河邊找鬼冬竟。 笑死,一個胖子當著我的面吹牛民逼,可吹牛的內容都是我干的泵殴。 我是一名探鬼主播,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼拼苍,長吁一口氣:“原來是場噩夢啊……” “哼笑诅!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤吆你,失蹤者是張志新(化名)和其女友劉穎弦叶,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體妇多,經...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡伤哺,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了者祖。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片立莉。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖七问,靈堂內的尸體忽然破棺而出蜓耻,到底是詐尸還是另有隱情,我是刑警寧澤烂瘫,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布媒熊,位于F島的核電站,受9級特大地震影響坟比,放射性物質發(fā)生泄漏芦鳍。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一葛账、第九天 我趴在偏房一處隱蔽的房頂上張望柠衅。 院中可真熱鬧,春花似錦籍琳、人聲如沸菲宴。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽喝峦。三九已至,卻和暖如春呜达,著一層夾襖步出監(jiān)牢的瞬間谣蠢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工查近, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留眉踱,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓霜威,卻偏偏與公主長得像谈喳,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子戈泼,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345