iOS內(nèi)存管理(5)-autorelease原理和autorelease和runloop的結(jié)合使用

內(nèi)存管理之中autorelease部分是相當(dāng)重要的,雖然現(xiàn)在都是ARC的時代了,我們還是要盡量去理解每一個原理,這對于我們理解代碼的實現(xiàn)和原理是有很大的幫助的.MRC中涨共,調(diào)用[obj autorelease]來延遲內(nèi)存的釋放是一件簡單自然的事纽帖,ARC下,我們甚至可以完全不知道Autorelease就能管理好內(nèi)存举反。那么接下來我們就理解下autorelease的原理.

1. autorelease原理

autorelease原理是什么呢?我們可以通過一行代碼來觀看 :

@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
}

這個是很簡單的MRC的代碼,我們可以通過把OC代碼轉(zhuǎn)換成C++代碼:

 {
    __AtAutoreleasePool __autoreleasepool;
    MJPerson *person = ((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)((MJPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("MJPerson"), sel_registerName("alloc")), sel_registerName("init")), sel_registerName("autorelease"));
 }

上邊的代碼看起來還是有一些繁瑣,我們再把C++代碼簡化:

{
    __AtAutoreleasePool __autoreleasepool;
   MJPerson *person = [[[MJPerson alloc] init] autorelease];
 }

最后簡化成的代碼中的__AtAutoreleasePool是什么? __AtAutoreleasePool是一個結(jié)構(gòu)體

struct __AtAutoreleasePool{
    __AtAutoreleasePool{//構(gòu)造函數(shù),在創(chuàng)建結(jié)構(gòu)體變量的時候調(diào)用
        atautoreleasepoolobj = objc_autoreleasePoolPush();
    }
    __AtAutoreleasePool{//析構(gòu)函數(shù),在結(jié)構(gòu)體銷毀的時候調(diào)用
        objc_atautoreleasePoolPop(atautoreleasepoolobj);
    }
    void * atautoreleasepoolobj;
};

在這個結(jié)構(gòu)體中第一次函數(shù)是一個C++的構(gòu)造函數(shù),objc_autoreleasePoolPush();這個方法會在創(chuàng)建結(jié)構(gòu)體變量的時候調(diào)用.
第二個函數(shù)是一個C++的析構(gòu)函數(shù),objc_atautoreleasePoolPop(atautoreleasepoolobj);這個方法會在結(jié)構(gòu)體銷毀的時候調(diào)用.
所以上面的代碼可以轉(zhuǎn)化為:

{
  atautoreleasepoolobj = objc_autoreleasePoolPush();
  Person *person = [[[Person alloc] init] autorelease];
  objc_autoreleasePoolPop(atautoreleasepoolobj);
}

一. AutoreleasePoolPage的結(jié)構(gòu)

自動釋放池的主要底層數(shù)據(jù)結(jié)構(gòu)是:__AtAutoreleasePool懊直、AutoreleasePoolPage,來看一下objc_autoreleasePoolPush 和 objc_autoreleasePoolPop 的實現(xiàn):

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}
void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}

上面的方法看上去是對 AutoreleasePoolPage 對應(yīng)靜態(tài)方法 push 和 pop 的封裝.調(diào)用了autorelease的對象最終都是通過AutoreleasePoolPage對象來管理的.
AutoreleasePoolPage 是一個 C++ 中的類,它在 NSObject.mm 中的定義是這樣的:

class AutoreleasePoolPage
{
    magic_t const magic;
    id *next;
    pthread const thread;
    AutoreleasePoolPage *const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
}

①. magic 用于對當(dāng)前 AutoreleasePoolPage 完整性的校驗.
②. thread 保存了當(dāng)前頁所在的線程.

每個AutoreleasePoolPage對象占用4096字節(jié)內(nèi)存火鼻,除了用來存放它內(nèi)部的成員變量室囊,剩下的空間用來存放autorelease對象的地址.

//源碼中的代碼是這樣的
#define I386_PGBYTES 4096
#define PAGE_SIZE I386_PGBYTES

③. parent 和child 就是用來構(gòu)造雙向鏈表的指針.

自動釋放池中的 AutoreleasePoolPage 是以雙向鏈表的形式連接起來的:

autoreleasePool的雙向鏈表.png

在自動釋放池中是存在自動釋放池中的棧,被初始化在0x1000~0x2000.其中有56bit用于存儲AutoreleasePoolPage的成員變量,剩下的 0x1038 ~ 0x2000 都是用來存儲加入到自動釋放池中的對象.begin() 和 end() 這兩個類的實例方法幫助我們快速獲取0x1038 ~ 0x2000 這一范圍的邊界地址.

④. next 指向了下一個為空的內(nèi)存地址瘦陈,如果 next指向的地址加入一個 object波俄,它就會如下圖所示移動到下一個為空的內(nèi)存地址中:

next指針的原理.png

autoreleasePool的雙向鏈表的執(zhí)行步驟是:

  1. 調(diào)用push方法會將一個POOL_BOUNDARY入棧捉貌,并且返回其存放的內(nèi)存地址趁窃,即返回給atautoreleasepoolobj醒陆。
  2. 調(diào)用pop方法時傳入一個POOL_BOUNDARY的內(nèi)存地址刨摩,會從最后一個入棧的對象開始發(fā)送release消息澡刹,直到遇到這個POOL_BOUNDARY.
  3. id *next指針作為游標(biāo)指向棧頂最新add進(jìn)來的autorelease對象的下一個位置.
  4. 一個AutoreleasePoolPage的空間被占滿時罢浇,會新建一個AutoreleasePoolPage對象嚷闭,連接鏈表胞锰,后來的autorelease對象在新的page加入.

⑤. POOL_SENTINEL(哨兵對象)

POOL_SENTINELnil的另一個名稱

#define POOL_SENTINEL nil

在每個自動釋放池初始化調(diào)用objc_autoreleasePoolPush的時候胜蛉,都會把一個POOL_SENTINEL push 到自動釋放池的棧頂,并且返回這個 POOL_SENTINEL 哨兵對象的地址暖璧。

int main(int argc, const char * argv[]) {
    {
        void * atautoreleasepoolobj = objc_autoreleasePoolPush();
        objc_autoreleasePoolPop(atautoreleasepoolobj);
    }
    return 0;
}

上面的autoreleasepoolobj就是一個POOL_SENTINEL澎办。
而當(dāng)方法objc_autoreleasePoolPop調(diào)用時麦锯,就會向自動釋放池中的對象發(fā)送release消息扶欣,直到第一個POOL_SENTINEL

Autoreleasepool的原理.png

objc_autoreleasePoolPush的返回值正是這個哨兵對象的地址料祠,objc_autoreleasePoolPop(哨兵對象)作為入?yún)⒊谑牵?/p>

  • 根據(jù)傳入的哨兵對象地址找到哨兵對象所處的page.
  • 在當(dāng)前page中括饶,將晚于哨兵對象插入的所有autorelease對象都發(fā)送一次 release消息巷帝,并向回移動next指針到正確位置.
  • 補充2:從最新加入的對象一直向前清理楞泼,可以向前跨越若干個page堕阔,直到哨兵所在的page.

A. objc_autoreleasePoolPush方法

了解了POOL_SENTINEL牺弹,我們來重新回顧一下objc_autoreleasePoolPush方法:

void *objc_autoreleasePoolPush(void) {
    return AutoreleasePoolPage::push();
}

它調(diào)用AutoreleasePoolPage的類方法push张漂,也非常簡單:

static inline void *push() {
   return autoreleaseFast(POOL_SENTINEL);
}

在這里會進(jìn)入一個比較關(guān)鍵的方法autoreleaseFast航攒,并傳入哨兵對象 POOL_SENTINEL

static inline id *autoreleaseFast(id obj)
{
   AutoreleasePoolPage *page = hotPage();
   if (page && !page->full()) {
       return page->add(obj);
   } else if (page) {
       return autoreleaseFullPage(obj, page);
   } else {
       return autoreleaseNoPage(obj);
   }
}

上述方法分三種情況選擇不同的代碼執(zhí)行:
①.有 hotPage (正在使用的AutoreleasePoolPage)并且當(dāng)前 page 不滿.
????·調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage 的棧中.
②.有 hotPage 并且當(dāng)前 page 已滿.
????·調(diào)用 autoreleaseFullPage初始化一個新的頁.
????·調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage的棧中.
③. 無 hotPage
????·調(diào)用 autoreleaseNoPage 創(chuàng)建一個 hotPage.
????·調(diào)用 page->add(obj) 方法將對象添加至 AutoreleasePoolPage的棧中.
④. 最后的都會調(diào)用 page->add(obj) 將對象添加到自動釋放池中币他。

a. page->add 添加對象

id *add(id obj) 將對象添加到自動釋放池頁中:

id *add(id obj) {
    id *ret = next;
    *next = obj;
    next++;
    return ret;
}

這個方法其實就是一個壓棧的操作蝴悉,將對象加入 AutoreleasePoolPage 然后移動棧頂?shù)闹羔槨?/p>

b. autoreleaseFullPage(當(dāng)前 hotPage 已滿)

autoreleaseFullPage會在當(dāng)前的hotPage已滿的時候調(diào)用:

static id *autoreleaseFullPage(id obj, AutoreleasePoolPage *page) {
    do {
        if (page->child) page = page->child;
        else page = new AutoreleasePoolPage(page);
    } while (page->full());

    setHotPage(page);
    return page->add(obj);
}

它會從傳入的 page 開始遍歷整個雙向鏈表,直到:
1.查找到一個未滿的AutoreleasePoolPage.
2.使用構(gòu)造器傳入parent創(chuàng)建一個新的AutoreleasePoolPage.

在查找到一個可以使用的 AutoreleasePoolPage之后廉丽,會將該頁面標(biāo)記成 hotPage,然后調(diào)動上面分析過的page->add 方法添加對象欣福。

c. autoreleaseNoPage(沒有 hotPage)

如果當(dāng)前內(nèi)存中不存在 hotPage拓劝,就會調(diào)用autoreleaseNoPage方法初始化一個AutoreleasePoolPage

static id *autoreleaseNoPage(id obj) {
    AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
    setHotPage(page);

    if (obj != POOL_SENTINEL) {
        page->add(POOL_SENTINEL);
    }

    return page->add(obj);
}

既然當(dāng)前內(nèi)存中不存在AutoreleasePoolPage郑临,就要從頭開始構(gòu)建這個自動釋放池的雙向鏈表厢洞,也就是說躺翻,新的AutoreleasePoolPage 是沒有 parent 指針的公你。
初始化之后陕靠,將當(dāng)前頁標(biāo)記為hotPage剪芥,然后會先向這個page 中添加一個 POOL_SENTINEL對象粗俱,來確保在pop調(diào)用的時候寸认,不會出現(xiàn)異常偏塞。
最后灸叼,將obj 添加到自動釋放池中古今。

B. objc_autoreleasePoolPop 方法

同樣滔以,回顧一下上面提到的 objc_autoreleasePoolPop 方法:

void objc_autoreleasePoolPop(void *ctxt) {
    AutoreleasePoolPage::pop(ctxt);
}
static inline void pop(void *token) {
AutoreleasePoolPage *page = pageForPointer(token);
id *stop = (id *)token;
page->releaseUntil(stop);
  if (page->child) {
    if (page->lessThanHalfFull()) {
        page->child->kill();
    } else if (page->child->child) {
        page->child->child->kill();
    }
}
}

①.使用 pageForPointer獲取當(dāng)前 token所在的 AutoreleasePoolPage.
②.調(diào)用 releaseUntil 方法釋放棧中的對象抵碟,直到 stop.
③.調(diào)用 child 的 kill 方法.

a.pageForPointer 獲取 AutoreleasePoolPage

pageForPointer方法主要是通過內(nèi)存地址的操作坏匪,獲取當(dāng)前指針?biāo)陧摰氖椎刂罚?/p>

static AutoreleasePoolPage *pageForPointer(const void *p) {
    return pageForPointer((uintptr_t)p);
}

static AutoreleasePoolPage *pageForPointer(uintptr_t p) {
    AutoreleasePoolPage *result;
    uintptr_t offset = p % SIZE;

    assert(offset >= sizeof(AutoreleasePoolPage));

    result = (AutoreleasePoolPage *)(p - offset);
    result->fastcheck();

    return result;
}

將指針與頁面的大小敦迄,也就是 4096 取模颅崩,得到當(dāng)前指針的偏移量蕊苗,因為所有的 AutoreleasePoolPage 在內(nèi)存中都是對齊的

p = 0x100816048
p % SIZE = 0x48
result = 0x100816000

而最后調(diào)用的方法fastCheck()用來檢查當(dāng)前的result是不是一個 AutoreleasePoolPage朽砰。

b. releaseUntil 釋放對象

releaseUntil 方法的實現(xiàn)如下:

void releaseUntil(id *stop) {
    while (this->next != stop) {
        AutoreleasePoolPage *page = hotPage();

        while (page->empty()) {
            page = page->parent;
            setHotPage(page);
        }

        page->unprotect();
        id obj = *--page->next;
        memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
        page->protect();

        if (obj != POOL_SENTINEL) {
            objc_release(obj);
        }
    }

    setHotPage(this);
}

它的實現(xiàn)還是很容易的瞧柔,用一個while循環(huán)持續(xù)釋放AutoreleasePoolPage 中的內(nèi)容造锅,直到 next指向了stop哥蔚。
使用memset 將內(nèi)存的內(nèi)容設(shè)置成 SCRIBBLE,然后使用objc_release釋放對象渤愁。

c. kill() 方法

到這里抖格,沒有分析的方法就只剩下 kill 了雹拄,而它會將當(dāng)前頁面以及子頁面全部刪除:

void kill() {
    AutoreleasePoolPage *page = this;
    while (page->child) page = page->child;

    AutoreleasePoolPage *deathptr;
    do {
        deathptr = page;
        page = page->parent;
        if (page) {
            page->unprotect();
            page->child = nil;
            page->protect();
        }
        delete deathptr;
    } while (deathptr != this);
}

二. 代碼分析

如果代碼中是嵌套形式的autorelease是什么樣子的?

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()
        
        Person *p1 = [[[Person alloc] init] autorelease];
        Person *p2 = [[[Person alloc] init] autorelease];
        
        @autoreleasepool { // r2 = push()
            for (int i = 0; i < 5; i++) {
                Person *p3 = [[[Person alloc] init] autorelease];
            }
            
            @autoreleasepool { // r3 = push()
                Person *p4 = [[[Person alloc] init] autorelease];
                _objc_autoreleasePoolPrint();
            } // pop(r3)
        } // pop(r2)
    } // pop(r1)
    return 0;
}
打印輸出結(jié)果.png
  1. 因為只打印了一個PAGE坪哄,所以說明他們是在同一個AutoreleasePoolPage损姜,只是每次一個新的autoreleasepool摧阅,都會插入一個POOL_BOUNDARY棒卷。
  2. 每次釋放對象時比规,都是從后往前釋放拦英,直到遇到POOL_BOUNDARY為止灾常。

那如果對象特別多又是什么樣子的呢?

extern void _objc_autoreleasePoolPrint(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool { //  r1 = push()
        
        Person *p1 = [[[Person alloc] init] autorelease];
        Person *p2 = [[[Person alloc] init] autorelease];
        
        @autoreleasepool { // r2 = push()
            for (int i = 0; i < 5; i++) {
                Person *p3 = [[[Person alloc] init] autorelease];
            }
            
            @autoreleasepool { // r3 = push()
                Person *p4 = [[[Person alloc] init] autorelease];
                _objc_autoreleasePoolPrint();
            } // pop(r3)
        } // pop(r2)
    } // pop(r1)
    return 0;
}
打印結(jié)果第一頁.png
打印結(jié)果第二頁.png
打印結(jié)果第三頁.png

2. autorelease和runloop的結(jié)合使用

MRC的autorelease對象在什么時機會被調(diào)用release?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    // 這個Person什么時候調(diào)用release,是由RunLoop來控制的
    // 它可能是在某次RunLoop循環(huán)中慷荔,RunLoop休眠之前調(diào)用了release
    MJPerson *person = [[[MJPerson alloc] init] autorelease];
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated
{
    [super viewWillAppear:animated];
    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:animated];
    
    NSLog(@"%s", __func__);
}
打印結(jié)果.png

iOS在主線程的Runloop中注冊了2個Observer

  1. 第1個Observer監(jiān)聽了kCFRunLoopEntry事件贷岸,會調(diào)用objc_autoreleasePoolPush().
  2. 第2個Observer:
  • 監(jiān)聽了kCFRunLoopBeforeWaiting事件墓卦,會調(diào)用objc_autoreleasePoolPop()户敬、objc_autoreleasePoolPush()
  • 監(jiān)聽了kCFRunLoopBeforeExit事件尿庐,會調(diào)用objc_autoreleasePoolPop().

在ARC中方法里有局部對象抄瑟, 出了方法后會立即釋放嗎?

- (void)viewDidLoad {
    [super viewDidLoad];
    
    Person *person = [[Person alloc] init];
    NSLog(@"%s", __func__);
}

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];
    
    NSLog(@"%s", __func__);
}

- (void)viewDidAppear:(BOOL)animated {
    [super viewDidAppear:animated];
    
    NSLog(@"%s", __func__);
}
打印結(jié)果.png

通過打印結(jié)果可知皮假,當(dāng)person對象出了其作用域后就銷毀惹资,即系統(tǒng)會在它出作用域的時候褪测,自動調(diào)用其release方法侮措。

autoreleasePool 在何時被釋放分扎?

App啟動后畏吓,蘋果在主線程 RunLoop 里注冊了兩個 Observer庵佣,其回調(diào)都是 _wrapRunLoopWithAutoreleasePoolHandler()巴粪。
第一個Observer監(jiān)視的事件是 Entry(即將進(jìn)入Loop)肛根,其回調(diào)內(nèi)會調(diào)用 _objc_autoreleasePoolPush() 創(chuàng)建自動釋放池派哲。其 order 是 -2147483647,優(yōu)先級最高储矩,保證創(chuàng)建釋放池發(fā)生在其他所有回調(diào)之前持隧。
第二個 Observer 監(jiān)視了兩個事件: BeforeWaiting(準(zhǔn)備進(jìn)入休眠) 時調(diào)用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()釋放舊的池并創(chuàng)建新池;Exit(即將退出Loop) 時調(diào)用_objc_autoreleasePoolPop()來釋放自動釋放池褥实。這個 Observer 的 order 是 2147483647哥艇,優(yōu)先級最低她奥,保證其釋放池子發(fā)生在其他所有回調(diào)之后哩俭。
在主線程執(zhí)行的代碼,通常是寫在諸如事件回調(diào)凡资、Timer回調(diào)內(nèi)的隙赁。這些回調(diào)會被 RunLoop 創(chuàng)建好的 AutoreleasePool 環(huán)繞著,所以不會出現(xiàn)內(nèi)存泄漏厚掷,開發(fā)者也不必顯示創(chuàng)建 Pool 了。

                            想了解更多iOS學(xué)習(xí)知識請聯(lián)系:QQ(814299221)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冒黑,一起剝皮案震驚了整個濱河市田绑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌抡爹,老刑警劉巖掩驱,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異冬竟,居然都是意外死亡欧穴,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門诱咏,熙熙樓的掌柜王于貴愁眉苦臉地迎上來苔可,“玉大人缴挖,你說我怎么就攤上這事」堆欤” “怎么了瘫析?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵烂瘫,是天一觀的道長。 經(jīng)常有香客問我,道長茄茁,這世上最難降的妖魔是什么愈犹? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任勋锤,我火速辦了婚禮谈宛,結(jié)果婚禮上琼牧,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布苦银。 她就那樣靜靜地躺著贝椿,像睡著了一般渣窜。 火紅的嫁衣襯著肌膚如雪详瑞。 梳的紋絲不亂的頭發(fā)上肝陪,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天柒竞,我揣著相機與錄音,去河邊找鬼霎俩。 笑死,一個胖子當(dāng)著我的面吹牛彤守,可吹牛的內(nèi)容都是我干的侈离。 我是一名探鬼主播,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼樱拴!你這毒婦竟也來了禽最?” 一聲冷哼從身側(cè)響起疹味,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤坏快,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體殿雪,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年奈揍,在試婚紗的時候發(fā)現(xiàn)自己被綠了租冠。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片玻褪。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡券勺,死狀恐怖朱灿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤校焦,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布岳链,位于F島的核電站,受9級特大地震影響服猪,放射性物質(zhì)發(fā)生泄漏供填。R本人自食惡果不足惜拐云,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望近她。 院中可真熱鬧叉瘩,春花似錦、人聲如沸粘捎。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽攒磨。三九已至泳桦,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間娩缰,已是汗流浹背灸撰。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留拼坎,地道東北人浮毯。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像泰鸡,于是被迫代替她去往敵國和親债蓝。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345