BLOCK 基礎(chǔ)到深入

一 簡介

Objective-C 中的block,是匿名函數(shù),匿名函數(shù)在別的語言中也被稱作閉包、λ表達式等。Objective-C 中的 block,有以下幾個特性:

  1. 可以將block作為函數(shù),也能作為對象,當(dāng)做屬性持有,定義內(nèi)存修飾詞。
  2. 可以長期存在,出了定義的作用域,內(nèi)部實現(xiàn)也能執(zhí)行。
  3. 可以捕獲外部變量,甚至使用__block修飾后,block 可以修改這個捕獲變量。

這背后的原理是什么呢,看了很多博客和代碼,理清楚了其中奧秘,在這里總結(jié)一下。

二 結(jié)構(gòu)

block的結(jié)構(gòu),用clang -rewrite-objcobjc代碼轉(zhuǎn)成cpp代碼,就能很清楚的看到了。在cpp中,block實際被編譯器轉(zhuǎn)寫成結(jié)構(gòu)體了。結(jié)構(gòu)體的介紹網(wǎng)上已經(jīng)有很多完善的資料了,我這里直接參考了這篇博客

// objc 代碼
int test()
{
    void (^blk)(void) = ^{
        printf("Block\n");
    };
    blk();
    return 0;
}
// cpp 重寫后
// 自己定義的 test 方法
int test()
{
    // 聲明&賦值
    void (*blk)(void) = ((void (*)())&__test_block_impl_0/*構(gòu)造函數(shù)*/(
                                      (void *)__test_block_func_0,
                                      &__test_block_desc_0_DATA)
                                      );
    // 執(zhí)行
    ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    return 0;
}

// 我們創(chuàng)建的 block 的結(jié)構(gòu)體
struct __test_block_impl_0 {
  struct __block_impl impl; // 核心:函數(shù)指針
  struct __test_block_desc_0* Desc; // 描述
  // 構(gòu)造函數(shù)
  __test_block_impl_0(void *fp, struct __test_block_desc_0 *desc, int flags=0) {
    impl.isa = &_NSConcreteStackBlock;// 棧 block
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

struct __block_impl {
  void *isa; // isa指針,所以是objc對象
  int Flags;
  int Reserved;
  void *FuncPtr; // 函數(shù)指針
};

// 靜態(tài)函數(shù)指針
static void __test_block_func_0(struct __test_block_impl_0 *__cself) {
    printf("Block\n");
}

// 描述
static struct __test_block_desc_0 {
  size_t reserved;
  size_t Block_size; // 大小
} __test_block_desc_0_DATA = { 0, sizeof(struct __test_block_impl_0)}; // __test_block_desc_0_DATA 是這個結(jié)構(gòu)體的實例

這個結(jié)構(gòu)剛才引用的博客中已經(jīng)分析的很透徹了,這里就不一步步細(xì)講了。直接說一下我總結(jié)的結(jié)構(gòu)。

__xx_block_impl_0 // block完整結(jié)構(gòu)體
{
    __block_impl, // 核心結(jié)構(gòu),具體結(jié)構(gòu)在下面
    desc, // block 描述,具體結(jié)構(gòu)在下面
    int foo, // 捕獲的變量,依次排列開
    ...
    __xx_block_impl_0(fp, desc, param0,param1...)//構(gòu)造函數(shù)
}

struct __block_impl
{
    id isa, // isa 指針
    void *fp, // 函數(shù)指針
    flag // 標(biāo)記位,用來描述 block 類型
}

block_desc // block 描述
{
    size,// 大小
    void *copy,// 捕獲對象時,捕獲變量們的copy 函數(shù)
    void *dispose// 捕獲對象時,捕獲變量們的dispose 函數(shù)
}
// 靜態(tài)函數(shù)指針
static void __xx_block_func_0() {
    // 具體實現(xiàn)
}

在講一下該結(jié)構(gòu)引出的比較讓人疑惑的點:

  1. 一個 block 定義會被編譯器clang編譯生成一套特殊結(jié)構(gòu)體,依據(jù)block行為不同(入?yún)ⅰ⒉东@變量),生成的結(jié)構(gòu)體也不一樣。從結(jié)構(gòu)體名稱類名__方法名__block__impl__序號也可以知道,它是動態(tài)創(chuàng)建的。
    相關(guān)的創(chuàng)建詳情可以參考 clanggenCode 模塊,GCBlocks
  2. 結(jié)構(gòu)體是對象,分成三類:
  • 棧block_NSConcreteStackBlock
  • 全局block_NSConcreteGlobalBlock
  • 堆block_NSConcreteMallocBlock

詳細(xì)的區(qū)別參考這里。這里只說一點,棧block在賦值時,會被拷貝到堆上,這樣就通過引用計數(shù)管理了(引用計數(shù)的內(nèi)容可以看我的博客坑位、待填)。
關(guān)于這個拷貝操作,開始我并沒有在代碼中看見,只看見了賦值void (*blk)(void) = ((void (*)())&__test_block_impl_0...,找了很久,才發(fā)現(xiàn)真實依據(jù)是NSObject.mm

// The -fobjc-arc flag causes the compiler to issue calls to objc_{retain/release/autorelease/retain_block}
id objc_retainBlock(id x) {
    return (id)_Block_copy(x);
}
  1. 一個奇怪的細(xì)節(jié):block聲明時,得到的是__test_block_impl_0類型變量 blk;執(zhí)行時,卻轉(zhuǎn)成了__block_impl類型。
    這里是因為__block_impl__test_block_impl_0的第一個變量,二者的指針地址相同的,所以可以直接強轉(zhuǎn),詳見這篇的 2.3 部分

三 參數(shù)捕獲

上面說到block捕獲的變量不同,動態(tài)生成的結(jié)構(gòu)也不一樣。block可以捕獲:objc 對象基礎(chǔ)數(shù)據(jù)類型另一個 block。我們常用的是捕獲基礎(chǔ)數(shù)據(jù)類型和 objc 對象基礎(chǔ)數(shù)據(jù)類型
下面就常用情況再分成四種情況討論:

這部分有大牛珠玉在前,解釋的比較透徹了。所以我就簡單總結(jié)一下:

  1. 捕獲基礎(chǔ)數(shù)據(jù),在 block 結(jié)構(gòu)中,會將捕獲參數(shù)添加進去。前面講了,在 block 賦值時,會被執(zhí)行一個 _Block_copy 操作,這其中對整個 block 結(jié)構(gòu)進行拷貝,捕獲的基礎(chǔ)數(shù)據(jù)值也會被拷貝到堆上。
  2. 捕獲對象變量時,除了會被添加到block結(jié)構(gòu)體中,還會額外生成一對被捕獲變量的拷貝函數(shù)銷毀函數(shù),保存在__block_desc結(jié)構(gòu)中。這里面描述了被捕獲的變量,如何被拷貝到堆上去。
// 捕獲變量的copy
// _Block_copy 的時候會調(diào)用
// _Block_copy 在 block 被賦值時候調(diào)用
static void __TestClass__testMethod_block_copy_2(struct __TestClass__testMethod_block_impl_2*dst, struct __TestClass__testMethod_block_impl_2*src) {
    _Block_object_assign((void*)&dst->tmpB, (void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

// 捕獲變量的dispose
// _Block_release 的時候會調(diào)用
// _Block_release 的調(diào)用時機是堆 block 引用計數(shù)為 0 時
static void __TestClass__testMethod_block_dispose_2(struct __TestClass__testMethod_block_impl_2*src) {
    _Block_object_dispose((void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_dispose((void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}
  1. 捕獲__block修飾的基礎(chǔ)數(shù)據(jù),該基礎(chǔ)數(shù)據(jù)會被轉(zhuǎn)化成一個特殊的結(jié)構(gòu)體:
// 定義處,在棧上
__block int tmpB = 1;
void(^blk003)(int a) = ^(int a) {
    // 使用處,在堆上
    NSLog(@"tmpB=%d", tmpB);
};

// __block 修飾的 int 變量 tempB
struct __Block_byref_tmpB_0 {
  void *__isa;
__Block_byref_tmpB_0 *__forwarding; 
 int __flags;
 int __size;
 int tmpB;
};

我們稱它為byref 封裝,可以看到它也是一個 objc 對象。所以byref 封裝和被捕獲變量一樣,會被添加到block 結(jié)構(gòu)里之外,還會生成copydispose函數(shù)指針。
byref 封裝除了封裝了自身的實際值之外,還持有一個自己類型的__forwarding指針。當(dāng)byref 封裝在棧上的時候,__forwarding指針會指向它堆上的拷貝,在堆上的時候回指向自己。這樣就保證了定義處和使用處調(diào)用方式是一樣的,至于源碼是如何實現(xiàn)的,下面會講到。

  1. 捕獲__block修飾的對象,同樣也會生成一個byref 封裝,結(jié)構(gòu)也和上一條類似:
 __block NSString * tempC = [NSString stringWithFormat:@"1"];
 void (^test)() = ^ {
        NSLog(@"%@",tempC);
 };

// __block 修飾的 int 變量 tempC
struct __Block_byref_tempC_0 {
  void *__isa;
__Block_byref_tmpC_0 *__forwarding; 
 int __flags;
 int __size;
 void (*__Block_byref_id_object_copy)(void*, void*);//實際賦值__Block_byref_id_object_copy_131
 void (*__Block_byref_id_object_dispose)(void*);//同上
 NSString *tempC;
};

static void __Block_byref_id_object_copy_131(void *dst, void *src) {
 _Block_object_assign((char*)dst + 40, *(void * *) ((char*)src + 40), 131);
}

static void __Block_byref_id_object_dispose_131(void *src) {
 _Block_object_dispose(*(void * *) ((char*)src + 40), 131);
}

但是多處量函數(shù)指針變量,這倆變量在創(chuàng)建時,被傳值__Block_byref_id_object_copy_131__Block_byref_id_object_dispose_131。這倆在byref 封裝拷貝時會調(diào)用到,用來拷貝NSString *tempC的。

總結(jié):可以看到 __xx_block_impl_0結(jié)構(gòu)隨著捕獲變量的復(fù)雜而變得復(fù)雜。而block從棧到堆的拷貝,是由自身結(jié)構(gòu)->捕獲變量->byref的value,層層進行的。

四 堆 block 拷貝

上面總結(jié)了拷貝的流程,這一節(jié)擼一下代碼,一來加深印象,二來找下幾個常見問題的答案:

  1. 在 block 中被修改了,棧上的原值也會被修改嗎?
  2. 循環(huán)引用是如何產(chǎn)生的,weak-strong-dance 為何能解決?
  3. 使用weak-strong-dance,被引用對象何時會為 nil ?

block 結(jié)構(gòu)體的拷貝方法 _Block_copy

// Copy, or bump refcount, of a block.  If really copying, call the copy helper if present.
void *_Block_copy(const void *arg) {
    struct Block_layout *aBlock;

    if (!arg) return NULL;
    
    // The following would be better done as a switch statement
    aBlock = (struct Block_layout *)arg;
    if (aBlock->flags & BLOCK_NEEDS_FREE) {
        // latches on high
        latching_incr_int(&aBlock->flags);
        return aBlock;
    }
    else if (aBlock->flags & BLOCK_IS_GLOBAL) {
        return aBlock;
    }
    else {
        // Its a stack block.  Make a copy.
       // !!!!開辟堆上內(nèi)存空間
        struct Block_layout *result =
            (struct Block_layout *)malloc(aBlock->descriptor->size);
        if (!result) return NULL;
        // !!!!將內(nèi)容移到指定堆上
        memmove(result, aBlock, aBlock->descriptor->size); // bitcopy first
#if __has_feature(ptrauth_calls)
        // Resign the invoke pointer as it uses address authentication.
        result->invoke = aBlock->invoke;
#endif
        // reset refcount
        result->flags &= ~(BLOCK_REFCOUNT_MASK|BLOCK_DEALLOCATING);    // XXX not needed
        result->flags |= BLOCK_NEEDS_FREE | 2;  // logical refcount 1
        _Block_call_copy_helper(result, aBlock);
        // Set isa last so memory analysis tools see a fully-initialized object.
        result->isa = _NSConcreteMallocBlock;
        return result;
    }
}

_Block_call_copy_helper 參數(shù)拷貝助手

static void _Block_call_copy_helper(void *result, struct Block_layout *aBlock)
{
    struct Block_descriptor_2 *desc = _Block_descriptor_2(aBlock);
    if (!desc) return;

    (*desc->copy)(result, aBlock); // do fixup
}

static void __TestClass__testMethod_block_copy_2(struct __TestClass__testMethod_block_impl_2*dst, struct __TestClass__testMethod_block_impl_2*src) {
    _Block_object_assign((void*)&dst->tmpB, (void*)src->tmpB, 8/*BLOCK_FIELD_IS_BYREF*/);
    _Block_object_assign((void*)&dst->self, (void*)src->self, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

_Block_object_assign 參數(shù)拷貝函數(shù)

void _Block_object_assign(void *destArg, const void *object, const int flags) {
    const void **dest = (const void **)destArg;
    switch (os_assumes(flags & BLOCK_ALL_COPY_DISPOSE_FLAGS)) {
      case BLOCK_FIELD_IS_OBJECT:
        /*******
        id object = ...;
        [^{ object; } copy];
        ********/

        _Block_retain_object(object);
// static void _Block_retain_object_default(const void *ptr __unused) { }
// static void _Block_destructInstance_default(const void *aBlock __unused) {}
// !!!!都是空方法
        *dest = object;
        break;

      case BLOCK_FIELD_IS_BLOCK:
        /*******
        void (^object)(void) = ...;
        [^{ object; } copy];
        ********/
// !!!!block 參數(shù),再調(diào)一次 `_Block_copy`
        *dest = _Block_copy(object);
        break;
    
      case BLOCK_FIELD_IS_BYREF | BLOCK_FIELD_IS_WEAK:
      case BLOCK_FIELD_IS_BYREF:
        /*******
         // copy the onstack __block container to the heap
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __block ... x;
         __weak __block ... x;
         [^{ x; } copy];
         ********/
// !!!!__block 修飾的基礎(chǔ)數(shù)據(jù)類型
        *dest = _Block_byref_copy(object);
        break;
        
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK:
        /*******
         // copy the actual field held in the __block container
         // Note this is MRC unretained __block only. 
         // ARC retained __block is handled by the copy helper directly.
         __block id object;
         __block void (^object)(void);
         [^{ object; } copy];
         ********/
// !!!!__block 修飾的對象
// !!!!直接賦值,對其引用計數(shù)+1
        *dest = object;
        break;

      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_OBJECT | BLOCK_FIELD_IS_WEAK:
      case BLOCK_BYREF_CALLER | BLOCK_FIELD_IS_BLOCK  | BLOCK_FIELD_IS_WEAK:
        /*******
         // copy the actual field held in the __block container
         // Note this __weak is old GC-weak/MRC-unretained.
         // ARC-style __weak is handled by the copy helper directly.
         __weak __block id object;
         __weak __block void (^object)(void);
         [^{ object; } copy];
         ********/
// !!!!__block __weak 雙重修飾的對象
// !!!!直接賦值,對其引用計數(shù)+1
// !!!!相當(dāng)于 __weak 沒有起作用
        *dest = object;
        break;

      default:
        break;
    }
}

_Block_byref_copy__forwarding 指針的操作

// Runtime entry points for maintaining the sharing knowledge of byref data blocks.

// A closure has been copied and its fixup routine is asking us to fix up the reference to the shared byref data
// Closures that aren't copied must still work, so everyone always accesses variables after dereferencing the forwarding ptr.
// We ask if the byref pointer that we know about has already been copied to the heap, and if so, increment and return it.
// Otherwise we need to copy it and update the stack forwarding pointer
static struct Block_byref *_Block_byref_copy(const void *arg) {
    struct Block_byref *src = (struct Block_byref *)arg;
 // !!!! 通過引用計數(shù)來判斷原 byref 的 forwarding 是否為堆上變量
    if ((src->forwarding->flags & BLOCK_REFCOUNT_MASK) == 0) {
        // src points to stack
        struct Block_byref *copy = (struct Block_byref *)malloc(src->size);
        copy->isa = NULL;
        // byref value 4 is logical refcount of 2: one for caller, one for stack
        copy->flags = src->flags | BLOCK_BYREF_NEEDS_FREE | 4;
 // !!!! 堆上 byref 指向自己地址本身
        copy->forwarding = copy; // patch heap copy to point to itself
 // !!!! 原棧上 byref 指向堆上地址
        src->forwarding = copy;  // patch stack to point to heap copy
        copy->size = src->size;
 // !!!! 判斷有 copy 或 dispose 函數(shù),會執(zhí)行相應(yīng)函數(shù)
        if (src->flags & BLOCK_BYREF_HAS_COPY_DISPOSE) {
            // Trust copy helper to copy everything of interest
            // If more than one field shows up in a byref block this is wrong XXX
            struct Block_byref_2 *src2 = (struct Block_byref_2 *)(src+1);
            struct Block_byref_2 *copy2 = (struct Block_byref_2 *)(copy+1);
            copy2->byref_keep = src2->byref_keep;
            copy2->byref_destroy = src2->byref_destroy;

            if (src->flags & BLOCK_BYREF_LAYOUT_EXTENDED) {
                struct Block_byref_3 *src3 = (struct Block_byref_3 *)(src2+1);
                struct Block_byref_3 *copy3 = (struct Block_byref_3*)(copy2+1);
                copy3->layout = src3->layout;
            }

            (*src2->byref_keep)(copy, src);
        }
        else {
            // Bitwise copy.
            // This copy includes Block_byref_3, if any.
            memmove(copy+1, src+1, src->size - sizeof(*src));
        }
    }
    // already copied to heap
    else if ((src->forwarding->flags & BLOCK_BYREF_NEEDS_FREE) == BLOCK_BYREF_NEEDS_FREE) {
//!!!! 引用計數(shù)+1 難道所有對象的引用計數(shù),都是用 flag 來表示的??
        latching_incr_int(&src->forwarding->flags);
    }
    
    return src->forwarding;
}


static int32_t latching_incr_int(volatile int32_t *where) {
    while (1) {
        int32_t old_value = *where;
        if ((old_value & BLOCK_REFCOUNT_MASK) == BLOCK_REFCOUNT_MASK) {
            return BLOCK_REFCOUNT_MASK;
        }
        if (OSAtomicCompareAndSwapInt(old_value, old_value+2, where)) {
            return old_value+2;
        }
    }
}

五 總結(jié)

關(guān)于 __block 修飾的變量,怎么同修改的
1、__block 修飾的變量,會生成一個結(jié)構(gòu)體 blk_byref。里面有一個 forwarding 指針、一個實際值,開始時 forwarding 指向自己。
2、在 blk 賦值時,(void (^blk2)(void) = ^{ .....} )會把 blk copy 到堆上,同時 copy 捕獲的參數(shù)(剛才的棧上 blk_byref)到堆上。 見 _Block_copy 方法。
3、copy 捕獲的參數(shù)時,設(shè)置 forwarding 指針,棧->堆,堆->自己。見 _Block_object_assign 方法。
4、一個參數(shù)如果被多個 blk 捕獲了(如果 forwarding 指針有值),在 copy 時候,就不在堆上創(chuàng)建新的 blk_byref,這樣就能保證一個變量被多個blk捕獲時,有且只有一份堆結(jié)構(gòu) blk_byref。見 _Block_byref_copy 方法。
5、實際上,所有訪問操作(blk內(nèi),blk外),都是 blk_byref->forwardig->val,因為棧/堆forwarding都到堆上了,最后實際修改的都是堆上的blk_ref ->val,不會改棧上blk_ref ->val。

sunnyxx 面試題
看完了上面內(nèi)容,可以用下面的幾道題測試下自己的掌握程度


sunnyxx面試題.png

參考:
破弓的《iOS Block》系列
libclosure 源碼
objc4 源碼
http://www.lxweimin.com/p/51d04b7639f1
http://www.lxweimin.com/p/b554e813fce1
http://www.lxweimin.com/p/e42f86a81045
http://clang.llvm.org/docs/Block-ABI-Apple.html#block-escapes
GCBlocks

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,698評論 6 539
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,202評論 3 426
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,742評論 0 382
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,580評論 1 316
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,297評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,688評論 1 327
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,693評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,875評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,438評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,183評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,384評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,931評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,612評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,022評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,297評論 1 292
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,093評論 3 397
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,330評論 2 377

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