深入理解 Objective-C ? Block

0.前言

日常開發中經常會用到 Block,但如果對它的底層實現沒有深入地挖掘過,就不能算是真正掌握,本篇就來探究一下 Block 的底層實現原理。

1.舉個 ??

先來看一個例子,下邊是一種簡單的 block 使用場景: 無參數、無返回值的 block。

typedef void(^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int age = 30;
        // 創建
        MyBlock blk = ^{
            NSLog(@"My age is %d .", age);
        };
        // 執行
        blk();
    }
    return 0;
}

2.Block的實質

為了探究 Block 的本質,我們需要借助 clang 將含有 Block 語法的源代碼轉換成 C++ 代碼。

2.1 Block 的底層結構

終端執行 $ clang -rewrite-objc main.m 命令,就可以將 main.m 文件編譯生成 main.cpp 文件,這里截取了 main.cpp 文件中與 block 相關的代碼,并添加了部分注釋:

struct __block_impl {
    void *isa;
    int Flags;
    int Reserved;
    void *FuncPtr;
};

static struct __main_block_desc_0 {
    size_t reserved;
    size_t Block_size;
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

// Block 的結構體
struct __main_block_impl_0 {
    
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;
    
    // 構造函數
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock; 
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

// block 的 { } 里邊的代碼構成的函數
static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}

// main() 函數
int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ 
    { 
        __AtAutoreleasePool __autoreleasepool;
        
        int age = 30;

        MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));

        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

下面開始一步步討論源碼。從 main() 函數開始,關于自動釋放池的代碼不在此處討論,先看一下 block 的創建過程:

MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, age));
// 簡化后的代碼: 
MyBlock blk = &__main_block_impl_0(__main_block_func_0, &__main_block_desc_0_DATA, age);

我們注意到這里實際上有兩個函數:__main_block_impl_0()__main_block_func_0()

先來看后者,具體代碼如下,實際是 block 的 { } 里邊的代碼構成的函數。

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    int age = __cself->age; // bound by copy
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_d9ff54_mi_0, age);
}

然后搜索前一個函數的函數名 __main_block_impl_0,發現它位于下邊這個結構體里邊:

struct __main_block_impl_0 {
    
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    int age;    

    // 構造函數
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int flags=0) : age(_age) {
        impl.isa = &_NSConcreteStackBlock;  // 指明該 block 的類型(此處是棧上的 block)。
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

而這個結構體就是 block 經編譯后得到的結構,很明顯 __main_block_impl_0() 是它的構造函數,我們發現,這個構造函數里邊都是在給 block 的前 3 個元素 (2 個結構體和一個 int age) 賦值。

第一個元素 impl ,它的組成是這樣的:

struct __block_impl {
    void *isa;       // 用于說明 block 的類型
    int Flags;       // 標識位
    int Reserved;    // 保留字段
    void *FuncPtr;   // 指針
};
  • FuncPtr 是一個指針,根據名字推斷應該是一個函數指針,結合 main() 函數中執行構造函數創建 block 的過程可以看出,FuncPtr 指向的是 block 的 { } 里邊的代碼構成的函數。

  • isa 指明了block 的類型,構造函數中給它賦的值是 &_NSConcreteStackBlock,說明他是棧上的 block。關于 block 的類型,下一小節就會講到。

第二個元素 Desc__main_block_desc_0 類型的結構體,如下所示:

static struct __main_block_desc_0 {
    size_t reserved;    // 保留字段
    size_t Block_size;  // block 的大小
} __main_block_desc_0_DATA = { 0, sizeof(struct __main_block_impl_0)};

其中 Block_size 從名字推斷應該是結構體的大小,緊隨其后定義了一個 __main_block_desc_0 類型的變量 __main_block_desc_0_DATA,它的第二個元素值就是當前 block 的大小 sizeof(struct __main_block_impl_0),從 main() 函數中執行 block 構造函數的語句可以看出,__main_block_desc_0_DATA 最終賦值給了 block 中的 Desc,進一步驗證了 Block_size 中存放的是 block 的大小。

第三個元素 int age 是 block 捕獲的一個 auto 變量,關于捕獲變量的機制,后面會詳細討論。

最后回到 main() 函數的最后一行代碼:

((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
// 簡化后:
blk->FuncPtr(blk);

很明顯這是執行 blk 里邊的指針 FuncPtr 指向的函數,而且將 blk 自己傳了進去,這樣,就可以在函數內部訪問到 block 捕獲的變量,如前文提到的 int age。

至此,本文開頭的 block 的底層結構基本介紹完了,看起來比較零散,這里繪制了一張總圖做個簡單小結:

Block的結構.png

2.2 Block 的類型

在此,簡單說明一下 block 的類型,block 的 3 種類型及其內存分布如下:

Block的分類.png

那么這 3 種類型的 Block 有什么區別呢,為了搞清楚這個問題,我們需要先回顧一下 4 種常見的變量類型及其代碼示例:

  • 自動變量(auto 變量)
  • 靜態局部變量
  • 全局變量
  • 靜態全局變量
// *** 4 中變量的代碼示例:

// 全局變量
int global_var = 10;
// 靜態全局變量
static int static_global_var = 20;

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        // 自動變量(局部變量)
        int local_var = 30; // <==> auto int local_var = 30;
        // 靜態局部變量
        static int local_static_var = 40;
        
        MyBlock blk = ^{
            NSLog(@"\n global_var: %d\n static_global_var: %d\n local_var: %d\n local_static_var: %d\n", global_var, static_global_var, local_var, local_static_var);
        };
        blk();
    }
    return 0;
}

關于各種 Block 的區別,可以簡單匯總成下邊的圖表:

block的3種類型的區別.png

也就是說:

  • 如果 Block 里邊訪問了 auto 變量,那么他就是棧上的 Block;
  • 如果沒有訪問 auto 變量,就是全局的 Block;
  • 如果對棧上的 Block 執行了 copy 操作,就變成了堆上的 Block。

2.3 Block 的 copy

上文提到了對棧上 Block 的 copy 操作,那么為什么需要 copy 呢?原因是:設置在棧上的 Block 如果其所屬的作用域結束,該 Block 就會被廢棄,為了延長它的生命周期,就需要將其復制到堆上。

既然棧上的 block 經 copy 后會從棧上復制到堆上,那么另外兩種 Block 執行 copy 操作又會發生什么呢? 每一種 Block 被 copy 后的結果如下:

Block的copy.png

ARC 環境下,編譯器會根據情況自動將棧上的 block 復制到堆上,比如滿足一下條件之一時:

  • block 作為函數返回值時;
  • 將 block 賦值給 __strong 指針時;
  • block 作為 Cocoa API 中方法名含有 usingBlock 的方法參數時;
  • block 作為 GCD API 的方法參數時。

MRC 環境下,需要手動調用 block 的 copy 操作,才能將棧上的 block 復制到堆上。

3.變量捕獲

為了保證 Block 內部能夠正常訪問外部的變量,block有個變量捕獲機制,我們以前邊介紹常見變量類型的代碼為例,看看 Block 是怎么捕獲變量的。

執行 clang -rewrite-objc main.m 之后,轉換的 block 的源碼如下:

struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;

  // 捕獲的變量
  int local_var;
  int *local_static_var;

  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, int _local_var, int *_local_static_var, int flags=0) : local_var(_local_var), local_static_var(_local_static_var) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};

從上邊的源碼可以看出來,對于這 4 種不同的變量,實際只捕獲了自動變量 local_var靜態局部標量 local_static_var,而且自動變量是捕獲了值,靜態局部變量捕獲的是變量地址。至于為什么這么設計,推測可能的原因如下:

block捕獲變量.png

3.1 捕獲自動變量

實際開發中,block 捕獲到的變量基本都是自動變量(局部變量),理由是:對于全局變量,任何地方都可以訪問它,不安全;對于靜態局部變量,它會一直存在于內存中,對內存是一種浪費。

對于基本數據類型的自動變量,前邊已經講過了,就是簡單的值捕獲,接下來我們重點討論一下對象類型 auto 變量的捕獲。

下邊是 block 訪問外部對象類型 auto 變量的簡單實例。

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        NSObject *obj = [[NSObject alloc] init];
        
        MyBlock blk = ^{
            NSLog(@"%@", obj);
        };
        
        blk();
    }
    return 0;
}

執行 clang -rewrite-objc main.m 后,生成的源碼中有這 2 點不同:

  • 捕獲了 NSObject *obj;
struct __main_block_impl_0 {
  struct __block_impl impl;
  struct __main_block_desc_0* Desc;
    
  NSObject *obj;
    
  __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, NSObject *_obj, int flags=0) : obj(_obj) {
    impl.isa = &_NSConcreteStackBlock;
    impl.Flags = flags;
    impl.FuncPtr = fp;
    Desc = desc;
  }
};
  • __main_block_desc_0 中增加了兩個指針 copydispose,整個文件里新增了 2 個函數 __main_block_copy_0()__main_block_dispose_0(),結合上下問可以知道,這兩個函數地址最終傳給了 block 里 Desc 中的 copydispose 這 2 個指針。
static struct __main_block_desc_0 {
  size_t reserved;
  size_t Block_size;
  void (*copy)(struct __main_block_impl_0*, struct __main_block_impl_0*);
  void (*dispose)(struct __main_block_impl_0*);
} __main_block_desc_0_DATA = {
    0,
    sizeof(struct __main_block_impl_0),
    __main_block_copy_0,
    __main_block_dispose_0
};

static void __main_block_copy_0(struct __main_block_impl_0*dst, struct __main_block_impl_0*src) {
    _Block_object_assign((void*)&dst->obj, (void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

static void __main_block_dispose_0(struct __main_block_impl_0*src) {
    _Block_object_dispose((void*)src->obj, 3/*BLOCK_FIELD_IS_OBJECT*/);
}

這兩個函數調用時機和作用如下:

  • 如果 block 從棧上拷貝到堆上
    會調用 block 內部的 copy() 函數,此函數內部會調用 _Block_object_assign() 函數,它會根據 auto 變量的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成 強引用 或 弱引用。

  • 如果 block 從堆上移除
    會調用 block 內部的 dispose() 函數,此函數內部會調用 _Block_object_dispose() 函數,它會自動釋放引用的 auto 變量(即 release)。

另外,如果 block 一直是在棧上,將不會對 auto 變量產生強引用。

3.2 捕獲 __block 變量

前邊我們只是在 block 內部使用變量,事實上,如果直接修改變量的話,比如下邊這個例子,就會報錯:此變量不可賦值 (錯誤信息見注釋)。

typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        int age = 10;
        
        MyBlock blk = ^{
            age = 20; // Error: Variable is not assignable (missing __block type specifier)
            NSLog(@"%d", age);
        };
        
        blk();
    }
    return 0;
}

按照錯誤信息的提示,如果給 int age = 10; 前邊加上 __block,就可以解決 block 內部無法修改 auto 變量的問題,實際操作后,發現果然可以正常輸出 age 的新值 20。

3.2.1 __block 變量能夠被 block 修改的原因

現在來看看 __block 修飾符到底做了什么,先將上邊的代碼轉成 C++ 源碼,下邊截取了其中部分關鍵代碼:

// 新出現的結構體
struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

struct __main_block_impl_0 {
    struct __block_impl impl;
    struct __main_block_desc_0* Desc;
    
    __Block_byref_age_0 *age; // by ref
    
    __main_block_impl_0(void *fp, struct __main_block_desc_0 *desc, __Block_byref_age_0 *_age, int flags=0) : age(_age->__forwarding) {
        impl.isa = &_NSConcreteStackBlock;
        impl.Flags = flags;
        impl.FuncPtr = fp;
        Desc = desc;
    }
};

static void __main_block_func_0(struct __main_block_impl_0 *__cself) {
    __Block_byref_age_0 *age = __cself->age; // bound by ref
    
    (age->__forwarding->age) = 20;
    NSLog((NSString *)&__NSConstantStringImpl__var_folders_hh_nkvzrckj32q41pptw9t27cvc0000gn_T_main_1dfa13_mi_0, (age->__forwarding->age));
}

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
        
        __attribute__((__blocks__(byref))) __Block_byref_age_0 age = {(void*)0,(__Block_byref_age_0 *)&age, 0, sizeof(__Block_byref_age_0), 10};
        
        MyBlock blk = ((void (*)())&__main_block_impl_0((void *)__main_block_func_0, &__main_block_desc_0_DATA, (__Block_byref_age_0 *)&age, 570425344));
        
        ((void (*)(__block_impl *))((__block_impl *)blk)->FuncPtr)((__block_impl *)blk);
    }
    return 0;
}

先看 main() 函數,__block int age 變成了 __Block_byref_age_0 類型的 age ,也就是說編譯器將 __block 修飾的變量包裝成了一個新的結構:

struct __Block_byref_age_0 {
    void *__isa;
    __Block_byref_age_0 *__forwarding;
    int __flags;
    int __size;
    int age;
};

__Block_byref_age_0 這個結構體里邊有一個 int age ,用于存儲 age 的值 (10)。還有一個重要成員 __Block_byref_age_0 *__forwarding; ,結合 block 的構造函數,我們知道 __forwarding 指針實際指向了它所在的結構體。

__block變量復制前.png

之所以這么做是為了當 block 被拷貝到堆上以后,無論訪問棧上的 block 還是 堆上的 block,最終都是訪問的堆上的同一個 block(拷貝后,堆上的 __forwarding 指向自己所在的 __block 變量,棧上的 __forwarding 指向堆上的 __block 變量),如下圖所示。

__block變量復制后.png

接下來,看看 block 的結構 __main_block_impl_0,里邊多了一個變量 __Block_byref_age_0 *age;,即 block 捕獲了這個新的結構體 __Block_byref_age_0 的地址,所以 block 里邊就可以通過地址訪問這個結構體,進而修改里邊 int age 的值。

__block 修飾的對象類型的 auto 變量與此類似,差別僅在于新生成的結構體:

image.png

從上圖可知,__block 修飾的對象類型轉換后的結構體里邊多了兩個函數指針,他們分別指向下面 2 個函數,負責內存管理的相關操作,下邊就會講到。

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

3.2.2 被 __block 修飾的對象類型的內存管理

對于 被 __block 修飾的對象類型,內存管理分以下 3 種情況:

  • 1.當 __block 變量 在棧上時,不會對指向的對象產生強引用。

  • 2.當 __block 變量 被 copy 到堆時,分兩種情況:

    • ARC 環境下,會調用 __block 變量內部 的 copy 函數,它會調用 _Block_object_assign() 函數,此函數會根據所指向對象的修飾符(__strong、__weak、__unsafe_unretained)做出相應的操作,形成強引用(retain)或者弱引用。
ARC環境.png
  • MRC 環境下,不會形成強引用(retain)。
typedef void (^MyBlock)(void);

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        
        __block NSObject *obj = [[NSObject alloc] init];
        
        MyBlock block = [^{
            NSLog(@"%p", obj);
        } copy];
        
        block();
        
        [obj release];
    }
    return 0;
}

如上所示,在 MRC 環境下,對象前加了 __block,不會對 block 形成強引用, 即當執行完 [obj release]; 之后,person 就被釋放了。

MRC環境.png
  • 3.如果 __block 變量從堆上移除,會調用 __block 變量內部dispose() 函數,它會調用 _Block_object_dispose() 函數,此函數會自動釋放指向的對象(release)

3.2.3 對象類型的auto變量 和 __block 變量

  • 當block在棧上時,對它們都不會產生強引用

  • 當 block 拷貝到堆上時,都會通過 copy 函數來處理它們

    • 對于 __block變量(假設變量名叫做a),最終會執行 _Block_object_assign((void*)&dst->a, (void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

    • 對于對象類型的 auto 變量(假設變量名叫做p),最終會執行 _Block_object_assign((void*)&dst->p, (void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

      以上兩者最終調用的方法是相同的,只不過最后一個參數有差別,前者是 8(表示引用類型),后者是 3 (表示對象),下面對 _Block_object_dispose() 函數的調用與之類似。

  • 當 block 從堆上移除時,都會通過 dispose 函數來釋放它們

    • 對于 __block變量(假設變量名叫做a),最終會執行 _Block_object_dispose((void*)src->a, 8/*BLOCK_FIELD_IS_BYREF*/);

    • 對于對象類型的auto變量(假設變量名叫做p),最終會在執行 _Block_object_dispose((void*)src->p, 3/*BLOCK_FIELD_IS_OBJECT*/);

4.循環引用

關于使用 block 可能遇到的循環引用問題,我們分 ARC 和 MRC 兩種情況進行討論。

ARC 環境

在 ARC 環境下,目前大概有 3 種常見的解決循環引用的方式:

  • __weak

此方式是最常見也是推薦使用的一種方式,基本原理是,self 對 block 的引用維持強引用,不過將 block 對 self 的引用改成了弱引用。

    __weak typeof(self) weakSelf = self;
    self.block = ^{
        printf("%p", weakSelf);
    };
  • __unsafe_unreturned

這種方式與上邊的方式類似,不過當 weakSelf 指向的對象銷毀后,指針已然指向那塊已經被回收的內存,可能發生野指針錯誤,所以是不安全的。

    __unsafe_unretained typeof(self) weakSelf = self;
    self.block = ^{
        printf("%p", weakSelf);
    };
  • __block
    __block typeof(self) weakSelf = self;
    self.block = ^{
        printf("%p", weakSelf);
        weakSelf = nil;
    };
    self.block();

我們知道,當在變量前邊加了 __block 之后就多了一個 __blcok 變量,于是里邊的引用關系就變成了:

__block解決循環引用A.png

為了打破這個循環引用的關系,需要在 block 里邊將對象置為 nil,而且必須執行 block 才能斷開 __block 變量對對象的強引用。

__block解決循環引用B.png

MRC 環境

MRC 環境下解決循環引用的方式與 ARC 環境類似,只是由于 MRC 環境下不可以使用 weak,所以只有 __unsafe_unreturned__block 2 種解決方式。對于 __block 的方式,在MRC中,__block 變量 不會對 weakSelf 產生強引用,也就不需要將其置為 nil 并執行 block 了。

    __block typeof(self) weakSelf = self;
    self.block = ^{
        printf("%p", weakSelf);
    };

5.小結

以上就是對 Block 底層實現的一個簡單討論,受自己的知識積累所限,難免有理解不到位的地方,后期會及時修正。

# 參考

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

推薦閱讀更多精彩內容