淺談iOS之weak底層實(shí)現(xiàn)原理

前言

在iOS開發(fā)過(guò)程中庆亡,會(huì)經(jīng)常使用到一個(gè)修飾詞“weak”,使用場(chǎng)景大家都比較清晰录煤,用于一些對(duì)象相互引用的時(shí)候鳄厌,避免出現(xiàn)強(qiáng)強(qiáng)引用,對(duì)象不能被釋放妈踊,出現(xiàn)內(nèi)存泄露的問(wèn)題了嚎。

weak 關(guān)鍵字的作用弱引用,所引用對(duì)象的計(jì)數(shù)器不會(huì)加一廊营,并在引用對(duì)象被釋放的時(shí)候自動(dòng)被設(shè)置為 nil歪泳。

weak底層原理

1.weak編譯解析

首先需要看一下weak編譯之后具體出現(xiàn)什么樣的變化,通過(guò)Clang的方法把weak編譯成C++

int main(){
    NSObject *obj = [[NSObject alloc] init];
    id __weak obj1 = obj;
}

編譯之后的weak露筒,通過(guò)objc_ownership(weak)實(shí)現(xiàn)weak方法呐伞,objc_ownership字面意思是:獲得對(duì)象的所有權(quán),是對(duì)對(duì)象weak的初始化的一個(gè)操作慎式。

在使用clang編譯過(guò)程中會(huì)報(bào)錯(cuò)誤荸哟,使用下方的方法編碼編譯出現(xiàn)error
clang -rewrite-objc -fobjc-arc -stdlib=libc++ -mmacosx-version-min=10.7 -fobjc-runtime=macosx-10.7 -Wno-deprecated-declarations main.m

int main(){
    NSObject *obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    id __attribute__((objc_ownership(weak))) obj1 = obj;
}

2.weak的實(shí)現(xiàn)原理

第一、通過(guò)weak編譯解析瞬捕,可以看出來(lái)weak通過(guò)runtime初始化的并維護(hù)的;
第二舵抹、weak和strong都是Object-C的修飾詞肪虎,而strong是通過(guò)runtime維護(hù)的一個(gè)自動(dòng)計(jì)數(shù)表結(jié)構(gòu)。
綜上:weak是有Runtime維護(hù)的weak表惧蛹。

在runtime源碼中扇救,可以找到'objc-weak.h'和‘objc-weak.mm’文件刑枝,并且在objc-weak.h文件中關(guān)于定義weak表的結(jié)構(gòu)體以及相關(guān)的方法。

2.1.weak表

weak_table_t是一個(gè)全局weak 引用的表迅腔,使用不定類型對(duì)象的地址作為 key装畅,用 weak_entry_t 類型結(jié)構(gòu)體對(duì)象作為 value 。其中的 weak_entries 成員

/**
 * The global weak references table. Stores object ids as keys,
 * and weak_entry_t structs as their values.
 */
struct weak_table_t {
    weak_entry_t *weak_entries; //保存了所有指向指定對(duì)象的weak指針   weak_entries的對(duì)象
    size_t    num_entries;              // weak對(duì)象的存儲(chǔ)空間
    uintptr_t mask;                      //參與判斷引用計(jì)數(shù)輔助量
    uintptr_t max_hash_displacement;    //hash key 最大偏移值
};

weak全局表中的存儲(chǔ)weak定義的對(duì)象的表結(jié)構(gòu)weak_entry_t沧烈,weak_entry_t是存儲(chǔ)在弱引用表中的一個(gè)內(nèi)部結(jié)構(gòu)體掠兄,它負(fù)責(zé)維護(hù)和存儲(chǔ)指向一個(gè)對(duì)象的所有弱引用hash表。其定義如下:

typedef objc_object ** weak_referrer_t;
struct weak_entry_t {
    DisguisedPtr<objc_object> referent;  //范型
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line : 1;
            uintptr_t        num_refs : PTR_MINUS_1;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line=0 is LSB of one of these (don't care which)
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    }
}

在 weak_entry_t 的結(jié)構(gòu)中锌雀,DisguisedPtr referent 是對(duì)泛型對(duì)象的指針做了一個(gè)封裝蚂夕,通過(guò)這個(gè)泛型類來(lái)解決內(nèi)存泄漏的問(wèn)題。從注釋中寫 out_of_line 成員為最低有效位腋逆,當(dāng)其為0的時(shí)候婿牍, weak_referrer_t 成員將擴(kuò)展為多行靜態(tài) hash table。其實(shí)其中的 weak_referrer_t 是二維 objc_object 的別名惩歉,通過(guò)一個(gè)二維指針地址偏移等脂,用下標(biāo)作為 hash 的 key,做成了一個(gè)弱引用散列撑蚌。

out_of_line:最低有效位上遥,也是標(biāo)志位。當(dāng)標(biāo)志位 0 時(shí)锨并,增加引用表指針緯度露该。
num_refs:引用數(shù)值。這里記錄弱引用表中引用有效數(shù)字第煮,因?yàn)槿跻帽硎褂玫氖庆o態(tài) hash 結(jié)構(gòu)解幼,所以需要使用變量來(lái)記錄數(shù)目。
mask:計(jì)數(shù)輔助量包警。
max_hash_displacement:hash 元素上限閥值撵摆。
其實(shí) out_of_line 的值通常情況下是等于零的,所以弱引用表總是一個(gè) objc_objective 指針二維數(shù)組害晦。一維 objc_objective 指針可構(gòu)成一張弱引用散列表特铝,通過(guò)第三緯度實(shí)現(xiàn)了多張散列表,并且表數(shù)量為 WEAK_INLINE_COUNT 壹瘟。

objc_object是weak_entry_t表中weak弱引用對(duì)象的范型對(duì)象的結(jié)構(gòu)體結(jié)構(gòu)鲫剿。

struct objc_object {
private:
    isa_t isa;

public:

    // ISA() assumes this is NOT a tagged pointer object
    Class ISA();

    // getIsa() allows this to be a tagged pointer object
    Class getIsa();

    // initIsa() should be used to init the isa of new objects only.
    // If this object already has an isa, use changeIsa() for correctness.
    // initInstanceIsa(): objects with no custom RR/AWZ
    // initClassIsa(): class objects
    // initProtocolIsa(): protocol objects
    // initIsa(): other objects
    void initIsa(Class cls /*indexed=false*/);
    void initClassIsa(Class cls /*indexed=maybe*/);
    void initProtocolIsa(Class cls /*indexed=maybe*/);
    void initInstanceIsa(Class cls, bool hasCxxDtor);

    // changeIsa() should be used to change the isa of existing objects.
    // If this is a new object, use initIsa() for performance.
    Class changeIsa(Class newCls);

    bool hasIndexedIsa();
    bool isTaggedPointer();
    bool isClass();

    // object may have associated objects?
    bool hasAssociatedObjects();
    void setHasAssociatedObjects();

    // object may be weakly referenced?
    bool isWeaklyReferenced();
    void setWeaklyReferenced_nolock();

    // object may have -.cxx_destruct implementation?
    bool hasCxxDtor();

    // Optimized calls to retain/release methods
    id retain();
    void release();
    id autorelease();

    // Implementations of retain/release methods
    id rootRetain();
    bool rootRelease();
    id rootAutorelease();
    bool rootTryRetain();
    bool rootReleaseShouldDealloc();
    uintptr_t rootRetainCount();

    // Implementation of dealloc methods
    bool rootIsDeallocating();
    void clearDeallocating();
    void rootDealloc();

private:
    void initIsa(Class newCls, bool indexed, bool hasCxxDtor);

    // Slow paths for inline control
    id rootAutorelease2();
    bool overrelease_error();

#if SUPPORT_NONPOINTER_ISA
    // Unified retain count manipulation for nonpointer isa
    id rootRetain(bool tryRetain, bool handleOverflow);
    bool rootRelease(bool performDealloc, bool handleUnderflow);
    id rootRetain_overflow(bool tryRetain);
    bool rootRelease_underflow(bool performDealloc);

    void clearDeallocating_weak();

    // Side table retain count overflow for nonpointer isa
    void sidetable_lock();
    void sidetable_unlock();

    void sidetable_moveExtraRC_nolock(size_t extra_rc, bool isDeallocating, bool weaklyReferenced);
    bool sidetable_addExtraRC_nolock(size_t delta_rc);
    bool sidetable_subExtraRC_nolock(size_t delta_rc);
    size_t sidetable_getExtraRC_nolock();
#endif

    // Side-table-only retain count
    bool sidetable_isDeallocating();
    void sidetable_clearDeallocating();

    bool sidetable_isWeaklyReferenced();
    void sidetable_setWeaklyReferenced_nolock();

    id sidetable_retain();
    id sidetable_retain_slow(SideTable *table);

    bool sidetable_release(bool performDealloc = true);
    bool sidetable_release_slow(SideTable *table, bool performDealloc = true);

    bool sidetable_tryRetain();

    uintptr_t sidetable_retainCount();
#if !NDEBUG
    bool sidetable_present();
#endif
};

總之:
1.weak_table_t(weak 全局表):采用hash(哈希表)的方式把所有weak引用的對(duì)象,存儲(chǔ)所有引用weak對(duì)象
2.weak_entry_t(weak_table_t表中hash表的value值稻轨,weak對(duì)象體):用于記錄hash表中weak對(duì)象
3.objc_object(weak_entry_t對(duì)象中的范型對(duì)象灵莲,用于標(biāo)記對(duì)象weak對(duì)象):用于標(biāo)示weak引用的對(duì)象。

詳細(xì)講解weak存儲(chǔ)對(duì)象結(jié)構(gòu)殴俱,對(duì)接下來(lái)對(duì)weak操作使用可以更加清晰的理解weak的使用政冻。

2.2.weak底層實(shí)現(xiàn)原理

在runtime源碼中的NSObject.mm文件中找到了關(guān)于初始化和管理weak表的方法

  • 初始化weak表方法
//初始化weak表
/** 
 * Initialize a fresh weak pointer to some object location. 
 * It would be used for code like: 
 *
 * (The nil case) 
 * __weak id weakPtr;
 * (The non-nil case) 
 * NSObject *o = ...;
 * __weak id weakPtr = o;
 * 
 * @param addr Address of __weak ptr. 
 * @param val Object ptr. 
 */
id objc_initWeak(id *addr, id val)
{
    *addr = 0;
    if (!val) return nil;
    return objc_storeWeak(addr, val); // 存儲(chǔ)weak對(duì)象
}
  • 存儲(chǔ)weak對(duì)象的方法
/** 
 * This function stores a new value into a __weak variable. It would
 * be used anywhere a __weak variable is the target of an assignment.
 * 
 * @param location The address of the weak pointer itself
 * @param newObj The new object this weak ptr should now point to
 * 
 * @return \e newObj
 */
id
objc_storeWeak(id *location, id newObj)
{
    id oldObj;
    SideTable *oldTable;
    SideTable *newTable;
    spinlock_t *lock1;
#if SIDE_TABLE_STRIPE > 1
    spinlock_t *lock2;
#endif

    // Acquire locks for old and new values.
    // Order by lock address to prevent lock ordering problems. 
    // Retry if the old value changes underneath us.
 retry:
    oldObj = *location;
    
    oldTable = SideTable::tableForPointer(oldObj);
    newTable = SideTable::tableForPointer(newObj);
    
    lock1 = &newTable->slock;
#if SIDE_TABLE_STRIPE > 1
    lock2 = &oldTable->slock;
    if (lock1 > lock2) {
        spinlock_t *temp = lock1;
        lock1 = lock2;
        lock2 = temp;
    }
    if (lock1 != lock2) spinlock_lock(lock2);
#endif
    spinlock_lock(lock1);

    if (*location != oldObj) {
        spinlock_unlock(lock1);
#if SIDE_TABLE_STRIPE > 1
        if (lock1 != lock2) spinlock_unlock(lock2);
#endif
        goto retry;
    }

    weak_unregister_no_lock(&oldTable->weak_table, oldObj, location);
    newObj = weak_register_no_lock(&newTable->weak_table, newObj, location);
    // weak_register_no_lock returns nil if weak store should be rejected

    // Set is-weakly-referenced bit in refcount table.
    if (newObj  &&  !newObj->isTaggedPointer()) {
        newObj->setWeaklyReferenced_nolock();
    }

    // Do not set *location anywhere else. That would introduce a race.
    *location = newObj;
    
    spinlock_unlock(lock1);
#if SIDE_TABLE_STRIPE > 1
    if (lock1 != lock2) spinlock_unlock(lock2);
#endif

    return newObj;
}
  • 舊對(duì)象解除注冊(cè)操作 weak_unregister_no_lock

該方法主要作用是將舊對(duì)象在 weak_table 中接觸 weak 指針的對(duì)應(yīng)綁定枚抵。根據(jù)函數(shù)名,稱之為解除注冊(cè)操作明场。從源碼中汽摹,可以知道其功能就是從 weak_table 中接觸 weak 指針的綁定。而其中的遍歷查詢苦锨,就是針對(duì)于 weak_entry 中的多張弱引用散列表逼泣。

  • 新對(duì)象添加注冊(cè)操作 weak_register_no_lock

這一步與上一步相反,通過(guò) weak_register_no_lock 函數(shù)把心的對(duì)象進(jìn)行注冊(cè)操作逆屡,完成與對(duì)應(yīng)的弱引用表進(jìn)行綁定操作圾旨。

  • 初始化弱引用對(duì)象流程一覽
    弱引用的初始化,從上文的分析中可以看出魏蔗,主要的操作部分就在弱引用表的取鍵砍的、查詢散列、創(chuàng)建弱引用表等操作莺治,可以總結(jié)出如下的流程圖:
111.png

SideTable 這個(gè)結(jié)構(gòu)體廓鞠,是對(duì)weak_table_t表的再次封裝操作,避免對(duì)weak_table_t直接操作谣旁,SideTable使用更加方便床佳。

class SideTable {
private:
    static uint8_t table_buf[SIDE_TABLE_STRIPE * SIDE_TABLE_SIZE];

public:
    spinlock_t slock;
    RefcountMap refcnts;
    weak_table_t weak_table;

    SideTable() : slock(SPINLOCK_INITIALIZER)
    {
        memset(&weak_table, 0, sizeof(weak_table));
    }
    
    ~SideTable() 
    {
        // never delete side_table in case other threads retain during exit
        assert(0);
    }

    static SideTable *tableForPointer(const void *p) 
    {
#     if SIDE_TABLE_STRIPE == 1
        return (SideTable *)table_buf;
#     else
        uintptr_t a = (uintptr_t)p;
        int index = ((a >> 4) ^ (a >> 9)) & (SIDE_TABLE_STRIPE - 1);
        return (SideTable *)&table_buf[index * SIDE_TABLE_SIZE];
#     endif
    }

    static void init() {
        // use placement new instead of static ctor to avoid dtor at exit
        for (int i = 0; i < SIDE_TABLE_STRIPE; i++) {
            new (&table_buf[i * SIDE_TABLE_SIZE]) SideTable;
        }
    }
};

總之根據(jù)以上對(duì)weak進(jìn)行存儲(chǔ)的過(guò)程可以通過(guò)下邊的流程圖詳細(xì)的描述出來(lái)


333.png

3.weak釋放為nil過(guò)程

weak被釋放為nil,需要對(duì)對(duì)象整個(gè)釋放過(guò)程了解榄审,如下是對(duì)象釋放的整體流程:
1砌们、調(diào)用objc_release
2、因?yàn)閷?duì)象的引用計(jì)數(shù)為0搁进,所以執(zhí)行dealloc
3浪感、在dealloc中,調(diào)用了_objc_rootDealloc函數(shù)
4饼问、在_objc_rootDealloc中影兽,調(diào)用了object_dispose函數(shù)
5、調(diào)用objc_destructInstance
6莱革、最后調(diào)用objc_clear_deallocating峻堰。

對(duì)象準(zhǔn)備釋放時(shí),調(diào)用clearDeallocating函數(shù)盅视。clearDeallocating函數(shù)首先根據(jù)對(duì)象地址獲取所有weak指針地址的數(shù)組捐名,然后遍歷這個(gè)數(shù)組把其中的數(shù)據(jù)設(shè)為nil,最后把這個(gè)entry從weak表中刪除闹击,最后清理對(duì)象的記錄桐筏。

在對(duì)象被釋放的流程中,需要對(duì)objc_clear_deallocating方法進(jìn)行深入的分析

void objc_clear_deallocating(id obj) 
{
    assert(obj);
    assert(!UseGC);
    if (obj->isTaggedPointer()) return;
    obj->clearDeallocating();
}

//執(zhí)行 clearDeallocating方法
inline void objc_object::clearDeallocating()
{
    sidetable_clearDeallocating();
}
// 執(zhí)行sidetable_clearDeallocating,找到weak表中的value值
void  objc_object::sidetable_clearDeallocating()
{
    SideTable *table = SideTable::tableForPointer(this);
    // clear any weak table items
    // clear extra retain count and deallocating bit
    // (fixme warn or abort if extra retain count == 0 ?)
    spinlock_lock(&table->slock);
    RefcountMap::iterator it = table->refcnts.find(this);
    if (it != table->refcnts.end()) {
        if (it->second & SIDE_TABLE_WEAKLY_REFERENCED) {
            weak_clear_no_lock(&table->weak_table, (id)this);
        }
        table->refcnts.erase(it);
    }
    spinlock_unlock(&table->slock);
}

對(duì)weak置nil的操作最終調(diào)用執(zhí)行weak_clear_no_lock方法用于執(zhí)行置nil的操作梅忌。執(zhí)行方法如下:

/** 
 * Called by dealloc; nils out all weak pointers that point to the 
 * provided object so that they can no longer be used.
 * 
 * @param weak_table 
 * @param referent The object being deallocated. 
 */
void 
weak_clear_no_lock(weak_table_t *weak_table, id referent_id) 
{
    objc_object *referent = (objc_object *)referent_id;

    weak_entry_t *entry = weak_entry_for_referent(weak_table, referent);
    if (entry == nil) {
        /// XXX shouldn't happen, but does with mismatched CF/objc
        //printf("XXX no entry for clear deallocating %p\n", referent);
        return;
    }

    // zero out references
    weak_referrer_t *referrers;
    size_t count;
    
    if (entry->out_of_line) {
        referrers = entry->referrers;
        count = TABLE_SIZE(entry);
    } 
    else {
        referrers = entry->inline_referrers;
        count = WEAK_INLINE_COUNT;
    }
    
    for (size_t i = 0; i < count; ++i) {
        objc_object **referrer = referrers[i];
        if (referrer) {
            if (*referrer == referent) {
                *referrer = nil;
            }
            else if (*referrer) {
                _objc_inform("__weak variable at %p holds %p instead of %p. "
                             "This is probably incorrect use of "
                             "objc_storeWeak() and objc_loadWeak(). "
                             "Break on objc_weak_error to debug.\n", 
                             referrer, (void*)*referrer, (void*)referent);
                objc_weak_error();
            }
        }
    }
    
    weak_entry_remove(weak_table, entry);
}

objc_clear_deallocating該函數(shù)的動(dòng)作如下:

1、從weak表中獲取廢棄對(duì)象的地址為鍵值的記錄
2除破、將包含在記錄中的所有附有 weak修飾符變量的地址牧氮,賦值為nil
3、將weak表中該記錄刪除
4瑰枫、從引用計(jì)數(shù)表中刪除廢棄對(duì)象的地址為鍵值的記錄

其實(shí)Weak表是一個(gè)hash(哈希)表踱葛,然后里面的key是指向?qū)ο蟮牡刂罚琕alue是Weak指針的地址的數(shù)組光坝。

總結(jié)

weak是Runtime維護(hù)了一個(gè)hash(哈希)表尸诽,用于存儲(chǔ)指向某個(gè)對(duì)象的所有weak指針。weak表其實(shí)是一個(gè)hash(哈希)表盯另,Key是所指對(duì)象的地址性含,Value是weak指針的地址(這個(gè)地址的值是所指對(duì)象指針的地址)數(shù)組。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末鸳惯,一起剝皮案震驚了整個(gè)濱河市商蕴,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌芝发,老刑警劉巖绪商,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異辅鲸,居然都是意外死亡格郁,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門独悴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)例书,“玉大人,你說(shuō)我怎么就攤上這事绵患∥戆龋” “怎么了?”我有些...
    開封第一講書人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵落蝙,是天一觀的道長(zhǎng)织狐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)筏勒,這世上最難降的妖魔是什么移迫? 我笑而不...
    開封第一講書人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮管行,結(jié)果婚禮上厨埋,老公的妹妹穿的比我還像新娘。我一直安慰自己捐顷,他們只是感情好荡陷,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開白布雨效。 她就那樣靜靜地躺著,像睡著了一般废赞。 火紅的嫁衣襯著肌膚如雪徽龟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,727評(píng)論 1 305
  • 那天唉地,我揣著相機(jī)與錄音据悔,去河邊找鬼。 笑死耘沼,一個(gè)胖子當(dāng)著我的面吹牛极颓,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播群嗤,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼菠隆,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了骚烧?” 一聲冷哼從身側(cè)響起浸赫,我...
    開封第一講書人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎赃绊,沒(méi)想到半個(gè)月后既峡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡碧查,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年运敢,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忠售。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡传惠,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出稻扬,到底是詐尸還是另有隱情卦方,我是刑警寧澤,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布泰佳,位于F島的核電站盼砍,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏逝她。R本人自食惡果不足惜浇坐,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望黔宛。 院中可真熱鬧近刘,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至疆拘,卻和暖如春蜕猫,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背哎迄。 一陣腳步聲響...
    開封第一講書人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留隆圆,地道東北人漱挚。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓兜叨,卻偏偏與公主長(zhǎng)得像坊饶,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子驾锰,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

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