iOS 底層拾遺:AutoreleasePool

前言

在陽(yáng)神的 黑幕背后的Autorelease 文章中已經(jīng)把 AutoreleasePool 核心邏輯講明白了哟沫,不過(guò)多是結(jié)論性的東西,筆者通讀源碼以探究更多的細(xì)節(jié)锌介,驗(yàn)證一下老生常談的一些結(jié)論嗜诀。

源碼基于 Runtime 750。

一孔祸、@autoreleasepool {} 干了些什么

main.m 文件代碼:

int main(int argc, const char * argv[]) {
    @autoreleasepool {}
    return 0;
}

使用 clang -rewrite-objc main.m 查看經(jīng)過(guò)編譯器前端處理的代碼:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool; }
    return 0;
}

可以看出@autoreleasepool{}會(huì)創(chuàng)建一個(gè)__AtAutoreleasePool類(lèi)型的局部變量并包含在當(dāng)前作用域隆敢,__AtAutoreleasePool構(gòu)造和析構(gòu)時(shí)分別調(diào)用了兩個(gè)方法,所以簡(jiǎn)化過(guò)程如下:

void *context = objc_autoreleasePoolPush()
// 對(duì)象調(diào)用 autorelease 裝入自動(dòng)釋放池
objc_autoreleasePoolPop(context)

可以猜測(cè) push 和 pop 操作是實(shí)現(xiàn)自動(dòng)釋放的關(guān)鍵融击。

二筑公、AutoreleasePoolPage 內(nèi)存分布

官方文檔 中提到了,主線程以及非顯式創(chuàng)建的線程(比如 GCD)都會(huì)有一個(gè) event loop (RunLoop 就是具體實(shí)現(xiàn))尊浪,在 loop 的每一個(gè)循環(huán)周期的開(kāi)始和結(jié)束會(huì)分別調(diào)用自動(dòng)釋放池的 push 和 pop 方法匣屡,由此來(lái)實(shí)現(xiàn)自動(dòng)的內(nèi)存管理。

objc_autoreleasePoolPush()objc_autoreleasePoolPop(...)實(shí)際上會(huì)調(diào)用到AutoreleasePoolPage類(lèi)的push()pop()方法拇涤,先看一下這個(gè)類(lèi)的數(shù)據(jù)結(jié)構(gòu):

class AutoreleasePoolPage {
    ...
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;

    static void * operator new(size_t size) {
        return malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);
    }
    id * begin() {
        return (id *) ((uint8_t *)this+sizeof(*this));
    }
    id * end() {
        return (id *) ((uint8_t *)this+SIZE);
    } 
    ...
}
  • parentchild正是指向前驅(qū)和后繼指針捣作,自動(dòng)釋放池就是一個(gè)以AutoreleasePoolPage為節(jié)點(diǎn)的雙向鏈表(后文驗(yàn)證)。
  • thread是指當(dāng)前 page 所對(duì)應(yīng)的線程鹅士。
  • magic用于校驗(yàn)內(nèi)存是否損壞券躁。
  • next指向當(dāng)前可插入對(duì)象的地址。

內(nèi)存對(duì)齊

重寫(xiě)了new運(yùn)算符,使用了malloc_zone_memalign(...)進(jìn)行內(nèi)存分配:

extern void *malloc_zone_memalign(malloc_zone_t *zone, size_t alignment, size_t size) ;
    /* 
     * Allocates a new pointer of size size whose address is an exact multiple of alignment.
     * alignment must be a power of two and at least as large as sizeof(void *).
     * zone must be non-NULL.
     */

注釋說(shuō)得很清楚了也拜,這個(gè)方法以alignment對(duì)齊的地址分配size的內(nèi)存空間以舒。調(diào)用時(shí)兩個(gè)參數(shù)都使用了SIZE宏,實(shí)際上就是虛擬內(nèi)存頁(yè)的大新:

#define I386_PGBYTES            4096

一個(gè) page 的內(nèi)存空間設(shè)置過(guò)小會(huì)導(dǎo)致更多的開(kāi)辟空間操作降低效率蔓钟,大量的parent/child指針變量也會(huì)占用可觀的內(nèi)存;空間設(shè)置過(guò)大可能會(huì)導(dǎo)致一個(gè) page 的利用率低浪費(fèi)過(guò)多內(nèi)存卵贱。設(shè)置為 4096 是比較考究的滥沫,在保證內(nèi)存對(duì)齊的情況下最大化利用空間避免內(nèi)存碎片。這么做過(guò)后 page 的地址總是 4096 的整數(shù)倍键俱,可以讓某些運(yùn)算更便捷(比如后文會(huì)說(shuō)的通過(guò)指針地址尋找對(duì)應(yīng)的 page)兰绣。

begin() 與 end()

AutoreleasePoolPage本身的大小遠(yuǎn)不及 4096,而超出的空間正是用來(lái)存放“期望被自動(dòng)管理的對(duì)象”编振。begin()end()方法標(biāo)記了這個(gè)范圍缀辩。

sizeof(*this)表示AutoreleasePoolPage本身的大小,那么(uint8_t *)this+sizeof(*this)就是最低地址党觅,(uint8_t *)this+SIZE就是最高地址雌澄。逐個(gè)插入對(duì)象時(shí),next指針從begin()end()逐個(gè)移動(dòng)杯瞻,后面的full()方法就是指next == end()empty()就是指next == begin()炫掐。

值得注意的是next/end()/begin()等都是id *類(lèi)型的魁莉,即指向指針的指針,進(jìn)行 +1 -1 運(yùn)算時(shí)移動(dòng)的是一個(gè)id大小的距離募胃。

三旗唁、push 邏輯

push()方法會(huì)調(diào)用autoreleaseFast(POOL_BOUNDARY)

    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);
        }
    }

hotPage 指的是當(dāng)前可插入對(duì)象的 page,放到后面一點(diǎn)分析痹束,先來(lái)看插入對(duì)象的邏輯检疫,分三種情況:

1、當(dāng) page 存在且沒(méi)滿時(shí)祷嘶,直接添加對(duì)象:

    id *add(id obj)
    {
        assert(!full());
        unprotect();
        id *ret = next;  // faster than `return next-1` because of aliasing
        *next++ = obj;
        protect();
        return ret;
    }

unprotect()/protect()內(nèi)部使用了int mprotect(void *a, size_t b, int c)屎媳,設(shè)置內(nèi)存起點(diǎn)a長(zhǎng)度b的內(nèi)存區(qū)域?yàn)?code>c類(lèi)型的訪問(wèn)限制:

    inline void protect() {
#if PROTECT_AUTORELEASEPOOL
        mprotect(this, SIZE, PROT_READ);
        check();
#endif
    }
    inline void unprotect() {
#if PROTECT_AUTORELEASEPOOL
        check();
        mprotect(this, SIZE, PROT_READ | PROT_WRITE);
#endif
    }

unprotect()設(shè)置為可讀可寫(xiě),protect()設(shè)置為只讀论巍,所以這里的目的是保證 page 寫(xiě)安全烛谊。不過(guò)有#define PROTECT_AUTORELEASEPOOL 0定義說(shuō)明目前版本還沒(méi)有開(kāi)放這個(gè)保護(hù)功能。

2嘉汰、當(dāng) page 存在且滿了時(shí)丹禀,拓展 page 節(jié)點(diǎn)并添加對(duì)象:

    static __attribute__((noinline))
    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);
    }

循環(huán)的邏輯:從 child 方向找到未滿的 page,若找不到則創(chuàng)建一個(gè)新 page 拼接到鏈表尾部(AutoreleasePoolPage 構(gòu)造方法會(huì)把傳入的 page 參數(shù)作為 parent 前驅(qū)對(duì)象)。后面再設(shè)置最新的 page 為 hotpage 并將 obj 添加進(jìn) page双泪。

3持搜、當(dāng) page 不存在時(shí),初始化一個(gè)

    static __attribute__((noinline))
    id *autoreleaseNoPage(id obj) 
    {   ...
        AutoreleasePoolPage *page = new AutoreleasePoolPage(nil);
        setHotPage(page);
        ...
        return page->add(obj);
    }

這個(gè)方法核心就是創(chuàng)建第一個(gè) page 然后加入線程局部存儲(chǔ)焙矛。

hotPage

從上面的push()方法分析可知葫盼,被自動(dòng)管理的對(duì)象會(huì)不斷插入雙向鏈表從前到后第一個(gè)未滿 page ,hotPage()其實(shí)就是指向這個(gè) page薄扁,還有個(gè)coldPage()方法是根據(jù)hotPage()找到第一個(gè) page剪返。

既然自動(dòng)釋放池是由AutoreleasePoolPage組成的雙向鏈表,那這個(gè)鏈表該如何訪問(wèn)呢邓梅?可能常規(guī)的思路是創(chuàng)建一個(gè)全局變量來(lái)訪問(wèn)它脱盲,不過(guò)這里使用了另外一個(gè)方式:

    static inline AutoreleasePoolPage *hotPage() 
    {
        AutoreleasePoolPage *result = (AutoreleasePoolPage *)
            tls_get_direct(key);
        // EMPTY_POOL_PLACEHOLDER 表示沒(méi)有 page
        if ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;
        if (result) result->fastcheck();
        return result;
    }
    static inline void setHotPage(AutoreleasePoolPage *page) 
    {
        if (page) page->fastcheck();
        tls_set_direct(key, (void *)page);
    }

tls_get_direct(...)tls_set_direct(...)內(nèi)部就是使用線程的局部存儲(chǔ)(TLS: Thread Local Storage)將 page 存儲(chǔ)起來(lái),這樣可以避免維護(hù)額外的空間來(lái)記錄尾部的 page日缨。由此也驗(yàn)證了自動(dòng)釋放池與線程一一對(duì)應(yīng)的關(guān)系钱反。

在 YYKit 中有一個(gè)使用廣泛的技巧:將某個(gè)對(duì)象最后使用時(shí)放在異步線程,如果這個(gè)對(duì)象釋放就(可能?)會(huì)在這個(gè)異步線程匣距,從而降低主線程壓力面哥。實(shí)際上就是編譯器插入 autorelease 代碼將對(duì)象加入到異步線程的自動(dòng)釋放池,而如果異步線程的釋放池先于主線程的釋放池pop()而調(diào)用對(duì)象的release()方法毅待,那么這個(gè)對(duì)象如果釋放就會(huì)在異步線程尚卫。所以筆者認(rèn)為這個(gè)優(yōu)化并非絕對(duì)有效(這里衍生出一個(gè)問(wèn)題:一個(gè)對(duì)象被多個(gè)自動(dòng)釋放池管理,若對(duì)象釋放這些釋放池怎么避免的野指針問(wèn)題尸红?)吱涉。

POOL_BOUNDARY

push()方法調(diào)用autoreleaseFast(POOL_BOUNDARY)時(shí)傳入的是一個(gè) POOL_BOUNDARY 并非需要被管理的對(duì)象,它的定義如下:

#   define POOL_BOUNDARY nil

在調(diào)用autoreleaseFast(obj)方法會(huì)返回指向obj指針的指針外里,它是一個(gè)id *類(lèi)型怎爵,也就是說(shuō),這個(gè)返回值關(guān)心的只是obj指針的地址盅蝗,而不是obj值的地址鳖链,obj指針的地址就是對(duì)應(yīng)AutoreleasePoolPage對(duì)象內(nèi)存中的某段區(qū)域。

再看一下上層調(diào)用:

void *context = objc_autoreleasePoolPush()
...
objc_autoreleasePoolPop(context)

pop 時(shí)會(huì)將這個(gè)obj指針的地址傳入進(jìn)去墩莫。pop 的邏輯是把 hotPage 里面裝的對(duì)象依次移除并發(fā)送 release 消息(后面會(huì)詳細(xì)分析)芙委,當(dāng)前 page 移除完了,繼續(xù)移除 parent 節(jié)點(diǎn)內(nèi)的對(duì)象贼穆,以此反復(fù)题山,而移除對(duì)象操作何時(shí)停止就是到這個(gè)obj指針的地址。

所以故痊,push 操作加入一個(gè) POOL_BOUNDARY 實(shí)際上就是加一個(gè)邊界顶瞳,pop 操作時(shí)根據(jù)邊界判斷范圍,這就是一個(gè)入棧與出棧的過(guò)程。

magic 校驗(yàn)

多次出現(xiàn)的check()方法如下:

    void check(bool die = true)  {
        if (!magic.check() || !pthread_equal(thread, pthread_self())) busted(die);
    }
    void fastcheck(bool die = true)  {
//補(bǔ)充:#define CHECK_AUTORELEASEPOOL (DEBUG)
#if CHECK_AUTORELEASEPOOL
        check(die);
#else
        if (! magic.fastcheck()) busted(die);
#endif
    }

可以看到慨菱,它們都調(diào)用了magic的 check 方法焰络,在 DEBUG 時(shí)還會(huì)去檢查當(dāng)前線程是否與 page 的線程一致。

magicmagic_t類(lèi)型的符喝,這個(gè)結(jié)構(gòu)體主要是有個(gè)uint32_t m[4];數(shù)組闪彼,構(gòu)造時(shí)內(nèi)存直接會(huì)寫(xiě)為0xA1A1A1A1 AUTORELEASE!,然后check()邏輯就是判斷構(gòu)造時(shí)的值是否發(fā)生了改變协饲,若發(fā)生改變說(shuō)明這個(gè) page 已經(jīng)被破壞畏腕。

四、autorelease 邏輯

上層對(duì)象調(diào)用 autorelease 方法會(huì)調(diào)用到AutoreleasePoolPage的以下方法:

    static inline id autorelease(id obj)
    {
        assert(obj);
        assert(!obj->isTaggedPointer());
        id *dest __unused = autoreleaseFast(obj);
        assert(!dest  ||  dest == EMPTY_POOL_PLACEHOLDER  ||  *dest == obj);
        return obj;
    }

顯然茉稠,最終還是會(huì)調(diào)用前面解析的autoreleaseFast(...)方法進(jìn)行對(duì)象插入描馅。由此也可以推斷,在一個(gè) Thread 沒(méi)有 Runloop 自動(dòng)執(zhí)行自動(dòng)釋放池的 push 和 pop 時(shí)而线,對(duì)象進(jìn)行 autorelease 時(shí)若發(fā)現(xiàn)沒(méi)有自動(dòng)釋放池節(jié)點(diǎn)會(huì)自動(dòng)創(chuàng)建 page 并加入線程局部存儲(chǔ)(參考前面的autoreleaseNoPage(...)方法分析)铭污。

五、pop 邏輯

objc_autoreleasePoolPop(context)context參數(shù)是objc_autoreleasePoolPush()返回的膀篮,實(shí)際上就是POOL_BOUNDARY對(duì)應(yīng)的在AutoreleasePoolPage中的地址嘹狞。最終會(huì)調(diào)用到pop()方法:

    static inline void pop(void *token) 
    {
        AutoreleasePoolPage *page;
        id *stop;
        ...
        // 拿到 token 邊界對(duì)應(yīng)的 page
        page = pageForPointer(token);
        stop = (id *)token;
        ...
        // pop 內(nèi)部對(duì)象直到 stop 邊界
        page->releaseUntil(stop);
        ...
        // 刪除空的 child 鏈表節(jié)點(diǎn),如果當(dāng)前頁(yè)對(duì)象超過(guò)一半誓竿,保留下一個(gè)空節(jié)點(diǎn)
        if (page->lessThanHalfFull()) {
            page->child->kill();
        }
        else if (page->child->child) {
            page->child->child->kill();
        }
    }

pop()的邏輯應(yīng)該很好理解了磅网,token參數(shù)就是邊界,下面分別分析步驟:

找到邊界對(duì)應(yīng)的 page

    static AutoreleasePoolPage *pageForPointer(const void *p) {
        return pageForPointer((uintptr_t)p);
    }
    static AutoreleasePoolPage *pageForPointer(uintptr_t p) {
        AutoreleasePoolPage *result;
        uintptr_t offset = p % SIZE;
        ....
        result = (AutoreleasePoolPage *)(p - offset);
        result->fastcheck();
        return result;
    }

看上面?zhèn)€函數(shù)筷屡,const void *p是指針的指針知市,((uintptr_t)p)才表示POOL_BOUNDARY指針在對(duì)應(yīng) page 中的地址。

看下面?zhèn)€函數(shù)速蕊,前面分析過(guò)內(nèi)存對(duì)齊的處理,那么 page 的起始地址必然是 SIZE (也就是頁(yè)大小 4096) 的倍數(shù)娘赴,那么p % SIZE就得到了這個(gè)p在 page 中的地址偏移规哲,最后通過(guò)p - offset就拿到了 page 的起始地址,這個(gè)處理比較秀诽表。

移除被管理對(duì)象并發(fā)送 release 消息

    void releaseUntil(id *stop)  {
        while (this->next != stop) {
            AutoreleasePoolPage *page = hotPage();
            // 如果當(dāng)前 page 空了唉锌,指向 parent
            while (page->empty()) {
                page = page->parent;
                setHotPage(page);
            }
            // 將即將要移除對(duì)象對(duì)應(yīng) page 中的內(nèi)存置為 SCRIBBLE
            page->unprotect();
            id obj = *--page->next;
            memset((void*)page->next, SCRIBBLE, sizeof(*page->next));
            page->protect();
            // 調(diào)用對(duì)象的 release 方法
            if (obj != POOL_BOUNDARY) {
                objc_release(obj);
            }
        }
        // 把當(dāng)前 page 設(shè)置 hotpage(調(diào)用時(shí) this 就是對(duì)應(yīng)期望釋放邊界的 page)
        setHotPage(this);
        ...
    }

清除 child

    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);
    }

這個(gè)邏輯一目了然了:找到當(dāng)前 page 的 child 方向尾部 page,然后反向挨著釋放并且把其 parent 節(jié)點(diǎn)的 child 指針置空竿奏。前面也說(shuō)明了unprotectprotect內(nèi)部并沒(méi)有開(kāi)啟寫(xiě)入安全保護(hù)袄简。

后語(yǔ)

以上就是自動(dòng)釋放池大部分源碼的分析了,這部分源碼沒(méi)有涉及匯編并且代碼量比較少泛啸,所以看起來(lái)相對(duì)容易绿语。多理解一些內(nèi)存管理底層有利于理解各種上層特性、定位內(nèi)存難題,也有助于寫(xiě)出更穩(wěn)定的代碼吕粹。并且在這個(gè)過(guò)程中种柑,不可避免需要接觸操作系統(tǒng)和編譯原理相關(guān)知識(shí),也算是能培養(yǎng)通識(shí)性能力匹耕。

讀源碼遠(yuǎn)比記結(jié)論重要聚请,遇到某些優(yōu)秀的代碼細(xì)節(jié)往往令人驚喜,不失為一種樂(lè)趣稳其。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末驶赏,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子既鞠,更是在濱河造成了極大的恐慌煤傍,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件损趋,死亡現(xiàn)場(chǎng)離奇詭異患久,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)浑槽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門(mén)蒋失,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人桐玻,你說(shuō)我怎么就攤上這事篙挽。” “怎么了镊靴?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵铣卡,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我偏竟,道長(zhǎng)煮落,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任踊谋,我火速辦了婚禮蝉仇,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘殖蚕。我一直安慰自己轿衔,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布睦疫。 她就那樣靜靜地躺著害驹,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蛤育。 梳的紋絲不亂的頭發(fā)上宛官,一...
    開(kāi)封第一講書(shū)人閱讀 51,443評(píng)論 1 302
  • 那天葫松,我揣著相機(jī)與錄音,去河邊找鬼摘刑。 笑死进宝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的枷恕。 我是一名探鬼主播党晋,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼徐块!你這毒婦竟也來(lái)了未玻?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤胡控,失蹤者是張志新(化名)和其女友劉穎扳剿,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體昼激,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡庇绽,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了橙困。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瞧掺。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖凡傅,靈堂內(nèi)的尸體忽然破棺而出辟狈,到底是詐尸還是另有隱情,我是刑警寧澤夏跷,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布哼转,位于F島的核電站,受9級(jí)特大地震影響槽华,放射性物質(zhì)發(fā)生泄漏壹蔓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一猫态、第九天 我趴在偏房一處隱蔽的房頂上張望庶溶。 院中可真熱鬧,春花似錦懂鸵、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至酿联,卻和暖如春终息,著一層夾襖步出監(jiān)牢的瞬間夺巩,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工周崭, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留柳譬,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓续镇,卻偏偏與公主長(zhǎng)得像美澳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子摸航,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

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