阿里開(kāi)源 iOS 協(xié)程開(kāi)發(fā)框架 coobjc源碼分析

本系列博客是本人的開(kāi)發(fā)筆記一睁。為了方便討論,本人新建了一個(gè)微信群(iOS技術(shù)討論群)佃却,想要加入的者吁,請(qǐng)?zhí)砑颖救宋⑿牛簔hujinhui207407,【加我前請(qǐng)備注:iOS 】饲帅,本人博客http://www.kyson.cn 也在不停的更新中复凳,歡迎一起討論

背景

今天朋友圈被一篇文章(以下簡(jiǎn)稱(chēng)“coobjc介紹文章”)刷屏了:剛剛,阿里開(kāi)源 iOS 協(xié)程開(kāi)發(fā)框架 coobjc灶泵!染坯。可能大部分iOS開(kāi)發(fā)者都直接懵逼了:

  • 什么是協(xié)程丘逸?
  • 協(xié)程的作用是什么单鹿?
  • 為什么要使用它?

因此筆者想給大家普及普及協(xié)程的知識(shí)深纲,運(yùn)行一下coobjc的Example仲锄,順便分析一下coobjc源碼劲妙。

分析

協(xié)程的維基百科在這里:協(xié)程。引用里面的解釋如下:

協(xié)程是計(jì)算機(jī)程序的一類(lèi)組件儒喊,推廣了非搶先多任務(wù)的子程序镣奋,允許執(zhí)行被掛起與被恢復(fù)。相對(duì)子例程而言怀愧,協(xié)程更為一般和靈活侨颈,但在實(shí)踐中使用沒(méi)有子例程那樣廣泛。協(xié)程源自Simula和Modula-2語(yǔ)言芯义,但也有其他語(yǔ)言支持哈垢。協(xié)程更適合于用來(lái)實(shí)現(xiàn)彼此熟悉的程序組件,如合作式多任務(wù)扛拨、異常處理耘分、事件循環(huán)、迭代器绑警、無(wú)限列表和管道求泰。
根據(jù)高德納的說(shuō)法, 馬爾文·康威于1958年發(fā)明了術(shù)語(yǔ)coroutine并用于構(gòu)建匯編程序。

對(duì)计盒,還是一知半解渴频。但最起碼我們了解到

  • 協(xié)程的英文是“coroutine”,因此我們能理解阿里的庫(kù)起名為coobjc的含義北启。那么這個(gè)詞又是怎么來(lái)的呢枉氮?筆者再深挖一下,協(xié)程(coroutine)顧名思義就是“協(xié)作的例程”(co-operative routines)暖庄。
  • 協(xié)程是和進(jìn)程或者線程有一定關(guān)系的
  • 協(xié)程的歷史還是比較悠久的聊替,只是Objective-C不支持。筆者經(jīng)過(guò)查閱培廓,發(fā)現(xiàn)很多現(xiàn)代語(yǔ)言都支持協(xié)程惹悄。比如Python以及swift,甚至C語(yǔ)言也是支持協(xié)程的肩钠。

協(xié)程的作用其實(shí)在coobjc介紹文章中有提及泣港,是為了優(yōu)化iOS中的異步操作。解決了如下問(wèn)題:

  • "嵌套地獄"
  • 錯(cuò)誤處理復(fù)雜和冗長(zhǎng)
  • 容易忘記調(diào)用 completion handler
  • 條件執(zhí)行變得很困難
  • 從互相獨(dú)立的調(diào)用中組合返回結(jié)果變得極其困難
  • 在錯(cuò)誤的線程中繼續(xù)執(zhí)行
  • 難以定位原因的多線程崩潰
  • 鎖和信號(hào)量濫用帶來(lái)的卡頓价匠、卡死

聽(tīng)起來(lái)是有點(diǎn)強(qiáng)大当纱,最明顯的好處是可以簡(jiǎn)化代碼;并且在coobjc介紹文章也說(shuō)道踩窖,性能也有所保障:當(dāng)線程的數(shù)量級(jí)大于1000以上時(shí)坡氯,coobjc的優(yōu)勢(shì)就會(huì)非常明顯。為了證明文章的結(jié)論,我們就來(lái)運(yùn)行一下coobjc源碼好了箫柳。
這里下載coobjc源碼手形。
發(fā)現(xiàn)目錄結(jié)構(gòu)如下:

coobjc目錄結(jié)構(gòu)

從目錄結(jié)構(gòu)看還是比較清晰的,根據(jù)coobjc介紹文章中提到的悯恍,coobjc不但提供了基礎(chǔ)的異步操作還提供了基于UIKit的封裝库糠。目錄中

  • cokit 及其子目錄提供的是基于UIKit層的coobjc封裝
  • coobjc目錄是coobjcObjective-C版實(shí)現(xiàn)的源代碼
  • coswift目錄是coobjcSwift版實(shí)現(xiàn)的源代碼
  • Example 下有兩個(gè)目錄,一個(gè)是Objective-C的實(shí)現(xiàn)涮毫,一個(gè)是Swift版的實(shí)現(xiàn)的Demo

我們先分析一下coobjcBaseExample工程:
打開(kāi)項(xiàng)目瞬欧,pod update一下即可運(yùn)行,運(yùn)行結(jié)果如下:

運(yùn)行結(jié)果

可以看到是個(gè)簡(jiǎn)單的列表頁(yè)。

Tips
打開(kāi)podfile可以發(fā)現(xiàn)里面有庫(kù)coobjc以外罢防,還有Specta艘虎、Expecta以及OCMock。這三個(gè)庫(kù)這里不多做介紹了篙梢,大家只需要知道這是用于單元測(cè)試的。

我們先看一下這個(gè)列表的實(shí)現(xiàn)邏輯是什么樣的美旧。我們不難定位到頁(yè)面位于KMDiscoverListViewController中渤滞,其網(wǎng)絡(luò)請(qǐng)求(這里是電影列表)代碼如下:

- (void)requestMovies
{
    co_launch(^{
        NSArray *dataArray = [[KMDiscoverSource discoverSource] getDiscoverList:@"1"];
        [self.refreshControl endRefreshing];
        
        if (dataArray != nil)
        {
            [self processData:dataArray];
        }
        else
        {
            [self.networkLoadingViewController showErrorView];
        }
    });
}

這里很容易理解代碼

        NSArray *dataArray = [[KMDiscoverSource discoverSource] getDiscoverList:@"1"];

是請(qǐng)求網(wǎng)絡(luò)數(shù)據(jù)的,其實(shí)現(xiàn)如下:

- (NSArray*)getDiscoverList:(NSString *)pageLimit;
{
    NSString *url = [NSString stringWithFormat:@"%@&page=%@", [self prepareUrl], pageLimit];
    id json = [[DataService sharedInstance] requestJSONWithURL:url];
    NSDictionary* infosDictionary = [self dictionaryFromResponseObject:json jsonPatternFile:@"KMDiscoverSourceJsonPattern.json"];
    return [self processResponseObject:infosDictionary];
}

以上代碼也能猜出榴嗅,

    id json = [[DataService sharedInstance] requestJSONWithURL:url];

這一行是做了網(wǎng)絡(luò)請(qǐng)求妄呕,但是我們?cè)冱c(diǎn)擊進(jìn)入類(lèi)DataServicerequestJSONWithURL方法的實(shí)現(xiàn)的時(shí)候,發(fā)現(xiàn)已經(jīng)看不懂了:

- (id)requestJSONWithURL:(NSString*)url CO_ASYNC{
    SURE_ASYNC
    return await([self.jsonActor sendMessage:url]);
}

好吧嗽测。既然看不懂了绪励,我們就從頭開(kāi)始學(xué)習(xí),協(xié)程的含義以及使用唠粥。繼而對(duì)coobjc源碼進(jìn)行分析疏魏。

協(xié)程入門(mén)

coobjc介紹文章中有提到

  • 第一種:利用glibcucontext組件(云風(fēng)的庫(kù))。
  • 第二種:使用匯編代碼來(lái)切換上下文(實(shí)現(xiàn)C協(xié)程)晤愧,原理同ucontext大莫。
  • 第三種:利用C語(yǔ)言語(yǔ)法switch-case的奇淫技巧來(lái)實(shí)現(xiàn)(Protothreads)。
  • 第四種:利用了 C 語(yǔ)言的 setjmplongjmp官份。
  • 第五種:利用編譯器支持語(yǔ)法糖只厘。

經(jīng)過(guò)篩選最終選擇了第二種。那我們來(lái)一個(gè)個(gè)分析舅巷,為什么coobjc摒棄了其他的方式羔味。
首先我們看第一種,coobjc介紹文章中提到ucontext在iOS中被廢棄了钠右,那如果不廢棄赋元,我們?nèi)绾稳ナ褂?code>ucontext呢?如下的一個(gè)Demo可以解釋一下ucontext的用法:

#include <stdio.h>
#include <ucontext.h>
#include <unistd.h>
 
int main(int argc, const char *argv[]){
    ucontext_t context;
    getcontext(&context);
    puts("Hello world");
    sleep(1);
    setcontext(&context);
    return 0;
}

注:示例代碼來(lái)自維基百科.

保存上述代碼到example.c,執(zhí)行編譯命令:

gcc example.c -o example

想想程序運(yùn)行的結(jié)果會(huì)是什么樣?

kysonzhu@ubuntu:~$ ./example 
Hello world
Hello world
Hello world
Hello world
Hello world
Hello world
^C
kysonzhu@ubuntu:~$

上面是程序執(zhí)行的部分輸出们陆,不知道是否和你想得一樣呢寒瓦?我們可以看到,程序在輸出第一個(gè)“Hello world"后并沒(méi)有退出程序坪仇,而是持續(xù)不斷的輸出“Hello world”杂腰。其實(shí)是程序通過(guò)getcontext先保存了一個(gè)上下文,然后輸出“Hello world”,在通過(guò)setcontext恢復(fù)到getcontext的地方,重新執(zhí)行代碼椅文,所以導(dǎo)致程序不斷的輸出“Hello world”喂很,在我這個(gè)菜鳥(niǎo)的眼里,這簡(jiǎn)直就是一個(gè)神奇的跳轉(zhuǎn)皆刺。那么問(wèn)題來(lái)了少辣,ucontext到底是什么?

這里筆者不多做介紹了羡蛾,推薦一篇文章漓帅,講的比較詳細(xì):ucontext-人人都可以實(shí)現(xiàn)的簡(jiǎn)單協(xié)程庫(kù)
這里我們只需要知道,所謂coobjc介紹文章中提到的使用匯編語(yǔ)言模擬ucontext痴怨,其實(shí)就是模擬的上面例子中的setcontextgetcontext等函數(shù)忙干。為了證明筆者的猜想,筆者打開(kāi)了coobjc源碼庫(kù)浪藻,發(fā)現(xiàn)里面的唯一的匯編文件coroutine_context.s

coroutine_context文件

查看該文件捐迫,發(fā)現(xiàn)了這么幾個(gè)函數(shù):

  • _coroutine_getcontext
  • _coroutine_begin
  • _coroutine_setcontext

果然驗(yàn)證了筆者的想法。這三個(gè)方法被暴露在文件coroutine_context.h中爱葵,供后序調(diào)用:

extern int coroutine_getcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_setcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_begin (coroutine_ucontext_t *__ucp);

接下來(lái)說(shuō)另外一個(gè)函數(shù)

int  setcontext(const ucontext_t *cut)

該函數(shù)是設(shè)置當(dāng)前的上下文為cut施戴,setcontext的上下文cut應(yīng)該通過(guò)getcontext或者makecontext取得,如果調(diào)用成功則不返回萌丈。如果上下文是通過(guò)調(diào)用getcontext()取得,程序會(huì)繼續(xù)執(zhí)行這個(gè)調(diào)用赞哗。如果上下文是通過(guò)調(diào)用makecontext取得,程序會(huì)調(diào)用makecontext函數(shù)的第二個(gè)參數(shù)指向的函數(shù),如果func函數(shù)返回,則恢復(fù)makecontext第一個(gè)參數(shù)指向的上下文第一個(gè)參數(shù)指向的上下文context_t中指向的uc_link.如果uc_link為NULL,則線程退出辆雾。

我們畫(huà)個(gè)表類(lèi)比一下ucontextcoobjc的函數(shù):

ucontext coobjc 含義
setcontext coroutine_setcontext 設(shè)置協(xié)程上下文
getcontext coroutine_getcontext 獲取協(xié)程上下文
makecontext coroutine_create 創(chuàng)建一個(gè)協(xié)程上下文

這么一來(lái)懈玻,我們之前的程序可以改寫(xiě)成如下:

#import <coobjc/coroutine_context.h>

int main(int argc, const char *argv[]) {
    coroutine_ucontext_t context;
    coroutine_getcontext(&context);
    puts("Hello world");
    sleep(1);
    coroutine_setcontext(&context);
    return 0;
}

返回的結(jié)果仍然不變,一直打印“hello world”乾颁。

深入?yún)f(xié)程

(1)目錄分析

目錄結(jié)構(gòu)

上圖是coobjc的目錄結(jié)構(gòu)涂乌,其中

  • core目錄提供了核心的協(xié)程函數(shù)
  • api目錄是coobjc基于Objective-C的封裝
  • csp,目錄從庫(kù)libtask引入英岭,提供了一些鏈?zhǔn)讲僮?/li>
  • objc提供了coobjc對(duì)象聲明周期管理的一些類(lèi)
    下面的文章湾盒,筆者會(huì)先從核心的core目錄開(kāi)始研究,后面的大家理解起來(lái)也就不復(fù)雜了诅妹。

(2)協(xié)程的構(gòu)成

上面我們只簡(jiǎn)單的介紹了coobjc罚勾,也了解到coobjc基本都是參考了ucontext毅人。那下面的例子中,筆者盡可能先介紹ucontext尖殃,然后再應(yīng)用到coobjc對(duì)應(yīng)的方法中丈莺。
我們繼續(xù)討論上文提到的幾個(gè)函數(shù),并說(shuō)明一下其作用:

int  getcontext(ucontext_t *uctp)

這個(gè)方法是送丰,獲取當(dāng)前上下文缔俄,并將上下文設(shè)置到uctp中,uctp是個(gè)上下文結(jié)構(gòu)體器躏,其定義如下:

_STRUCT_UCONTEXT
{
    int                     uc_onstack;
    __darwin_sigset_t       uc_sigmask;     /* signal mask used by this context */
    _STRUCT_SIGALTSTACK     uc_stack;       /* stack used by this context */
    _STRUCT_UCONTEXT        *uc_link;       /* pointer to resuming context */
    __darwin_size_t         uc_mcsize;      /* size of the machine context passed in */
    _STRUCT_MCONTEXT        *uc_mcontext;   /* pointer to machine specific context */
#ifdef _XOPEN_SOURCE
    _STRUCT_MCONTEXT        __mcontext_data;
#endif /* _XOPEN_SOURCE */
};

/* user context */
typedef _STRUCT_UCONTEXT    ucontext_t;     /* [???] user context */    

以上是ucontext的數(shù)據(jù)結(jié)構(gòu)俐载,其內(nèi)部的幾個(gè)屬性介紹一下:
當(dāng)當(dāng)前上下文(如使用makecontext創(chuàng)建的上下文)運(yùn)行終止時(shí)系統(tǒng)會(huì)恢復(fù)uc_link指向的上下文;uc_sigmask為該上下文中的阻塞信號(hào)集合登失;uc_stack為該上下文中使用的棧遏佣;uc_mcontext保存的上下文的特定機(jī)器表示,包括調(diào)用線程的特定寄存器等揽浙。其實(shí)還蠻好理解的状婶,ucontext其實(shí)就存放一些必要的數(shù)據(jù),這些數(shù)據(jù)還包括拯救成功或者失敗的情況需要的數(shù)據(jù)馅巷。

相比較而言膛虫,coobjc的定義和ucontext有一定區(qū)別:

/**
     The structure store coroutine's context data.
     */
struct coroutine {
    coroutine_func entry;                   // Process entry.
    void *userdata;                         // Userdata.
    coroutine_func userdata_dispose;        // Userdata's dispose action.
    void *context;                          // Coroutine's Call stack data.
    void *pre_context;                      // Coroutine's source process's Call stack data.
    int status;                             // Coroutine's running status.
    uint32_t stack_size;                    // Coroutine's stack size
    void *stack_memory;                     // Coroutine's stack memory address.
    void *stack_top;                    // Coroutine's stack top address.
    struct coroutine_scheduler *scheduler;  // The pointer to the scheduler.
    int8_t   is_scheduler;                  // The coroutine is a scheduler.
    
    struct coroutine *prev;
    struct coroutine *next;
    
    void *autoreleasepage;                  // If enable autorelease, the custom autoreleasepage.
    bool is_cancelled;                      // The coroutine is cancelled
};
typedef struct coroutine coroutine_t;

其中

    struct coroutine *prev;
    struct coroutine *next;

表明其是一個(gè)鏈表結(jié)構(gòu)。
既然是鏈表令杈,那么就會(huì)有添加元素走敌,以及刪除某個(gè)元素的方法碴倾,果然我們?cè)?code>coroutine.m中發(fā)現(xiàn)了對(duì)應(yīng)的鏈表操作方法:

// add routine to the queue
void scheduler_add_coroutine(coroutine_list_t *l, coroutine_t *t) {
    if(l->tail) {
        l->tail->next = t;
        t->prev = l->tail;
    } else {
        l->head = t;
        t->prev = nil;
    }
    l->tail = t;
    t->next = nil;
}

// delete routine from the queue
void scheduler_delete_coroutine(coroutine_list_t *l, coroutine_t *t) {
    if(t->prev) {
        t->prev->next = t->next;
    } else {
        l->head = t->next;
    }
    
    if(t->next) {
        t->next->prev = t->prev;
    } else {
        l->tail = t->prev;
    }
}

其中coroutine_list_t是為了標(biāo)識(shí)鏈表的頭尾節(jié)點(diǎn):

/**
 Define the linked list of scheduler's queue.
 */
struct coroutine_list {
    coroutine_t *head;
    coroutine_t *tail;
};
typedef struct coroutine_list coroutine_list_t;

為了管理所有的協(xié)程狀態(tài)逗噩,還設(shè)置了一個(gè)調(diào)度器:

/**
 Define the scheduler.
 One thread own one scheduler, all coroutine run this thread shares it.
 */
struct coroutine_scheduler {
    coroutine_t         *main_coroutine;
    coroutine_t         *running_coroutine;
    coroutine_list_t     coroutine_queue;
};
typedef struct coroutine_scheduler coroutine_scheduler_t;

看命名就大概能猜到,main_coroutine中包含了主協(xié)程(可能是即將設(shè)置數(shù)據(jù)的協(xié)程跌榔,或者即將使用的協(xié)程)异雁;running_coroutine是當(dāng)前正在運(yùn)行的協(xié)程。

(3)協(xié)程的操作

協(xié)程擁有和線程一樣類(lèi)似的操作僧须,例如創(chuàng)建纲刀,啟動(dòng),出讓控制權(quán)担平,恢復(fù)示绊,以及死亡。對(duì)應(yīng)的暂论,我們?cè)?code>coroutine.h看到了如下的幾個(gè)函數(shù)聲明:

//關(guān)閉一個(gè)協(xié)程如果它已經(jīng)死亡
void coroutine_close_ifdead(coroutine_t *co);
//添加協(xié)程到調(diào)度器面褐,并且立刻啟動(dòng)
void coroutine_resume(coroutine_t *co);
//添加協(xié)程到調(diào)度器
void coroutine_add(coroutine_t *co);
//出讓控制權(quán)
void coroutine_yield(coroutine_t *co);

為了更好的控制各個(gè)操作中的數(shù)據(jù),coobjc還提供了以下兩個(gè)方法:

void coroutine_setuserdata(coroutine_t *co, void *userdata, coroutine_func userdata_dispose);
void *coroutine_getuserdata(coroutine_t *co);

至此取胎,coobjc的核心代碼都分析完成了展哭。

(3)協(xié)程的Objective-C層面的封裝

我們?cè)俅位氐轿恼麻_(kāi)頭的例子- (void)requestMovies方法的實(shí)現(xiàn)中湃窍,第一步就是調(diào)用一個(gè)co_launch()的方法,這個(gè)方法最終會(huì)調(diào)用到

+ (instancetype)coroutineWithBlock:(void(^)(void))block onQueue:(dispatch_queue_t _Nullable)queue stackSize:(NSUInteger)stackSize {
    if (queue == NULL) {
        queue = co_get_current_queue();
    }
    if (queue == NULL) {
        return nil;
    }
    COCoroutine *coObj = [[self alloc] initWithBlock:block onQueue:queue];
    coObj.queue = queue;
    coroutine_t  *co = coroutine_create((void (*)(void *))co_exec);
    if (stackSize > 0 && stackSize < 1024*1024) {   // Max 1M
        co->stack_size = (uint32_t)((stackSize % 16384 > 0) ? ((stackSize/16384 + 1) * 16384) : stackSize/16384);        // Align with 16kb
    }
    coObj.co = co;
    coroutine_setuserdata(co, (__bridge_retained void *)coObj, co_obj_dispose);
    return coObj;
}

- (void)resumeNow {
    [self performBlockOnQueue:^{
        if (self.isResume) {
            return;
        }
        self.isResume = YES;
        coroutine_resume(self.co);
    }];
}

這兩個(gè)方法匪傍。其實(shí)代碼已經(jīng)很容易理解了您市,第一個(gè)方法是創(chuàng)建一個(gè)協(xié)程,第二個(gè)是啟動(dòng)役衡。
最后我們?cè)谡f(shuō)一下文章開(kāi)頭提到的await方法茵休,其實(shí)最終就交給chan去處理了:

- (COActorCompletable *)sendMessage:(id)message {
    COActorCompletable *completable = [COActorCompletable promise];
    dispatch_async(self.queue, ^{
        COActorMessage *actorMessage = [[COActorMessage alloc] initWithType:message completable:completable];
        [self.messageChan send_nonblock:actorMessage];
    });
    return completable;
}

所有的操作雖然丟到了同一個(gè)線程中,但其實(shí)最終是通過(guò)chan來(lái)調(diào)度了映挂。關(guān)于chan就不在本文討論范圍了泽篮,后面如果有時(shí)間,筆者會(huì)再進(jìn)行對(duì)chan的分析柑船。

總結(jié)

本文介紹了協(xié)程的概念帽撑,通過(guò)對(duì)比ucontext以及coobjc來(lái)說(shuō)明協(xié)程的用法,并分析了coobjc的源代碼鞍时,希望對(duì)大家有所幫助亏拉。

擴(kuò)展閱讀

iOS單元測(cè)試:Specta + Expecta + OCMock + OHHTTPStubs + KIF

我所理解的ucontext族函數(shù)

一個(gè)“蠅量級(jí)” C 語(yǔ)言協(xié)程庫(kù)

協(xié)程(Coroutine)并不是真正的多線程

ucontext-人人都可以實(shí)現(xiàn)的簡(jiǎn)單協(xié)程庫(kù)

協(xié)程分析之context上下文切換

廣告

我的首款個(gè)人開(kāi)發(fā)的APP壁紙寶貝上線了,歡迎大家下載逆巍。

壁紙寶貝

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末及塘,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子锐极,更是在濱河造成了極大的恐慌笙僚,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,284評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件灵再,死亡現(xiàn)場(chǎng)離奇詭異肋层,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)翎迁,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)栋猖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人汪榔,你說(shuō)我怎么就攤上這事蒲拉。” “怎么了痴腌?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,614評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵雌团,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我士聪,道長(zhǎng)锦援,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,671評(píng)論 1 293
  • 正文 為了忘掉前任戚嗅,我火速辦了婚禮雨涛,結(jié)果婚禮上枢舶,老公的妹妹穿的比我還像新娘。我一直安慰自己替久,他們只是感情好凉泄,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,699評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著蚯根,像睡著了一般后众。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上颅拦,一...
    開(kāi)封第一講書(shū)人閱讀 51,562評(píng)論 1 305
  • 那天雷袋,我揣著相機(jī)與錄音与倡,去河邊找鬼致板。 笑死匾寝,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的碌秸。 我是一名探鬼主播绍移,決...
    沈念sama閱讀 40,309評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼讥电!你這毒婦竟也來(lái)了蹂窖?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 39,223評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤恩敌,失蹤者是張志新(化名)和其女友劉穎瞬测,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體纠炮,經(jīng)...
    沈念sama閱讀 45,668評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡月趟,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,859評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抗碰。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片狮斗。...
    茶點(diǎn)故事閱讀 39,981評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡绽乔,死狀恐怖弧蝇,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情折砸,我是刑警寧澤看疗,帶...
    沈念sama閱讀 35,705評(píng)論 5 347
  • 正文 年R本政府宣布,位于F島的核電站睦授,受9級(jí)特大地震影響两芳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜去枷,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,310評(píng)論 3 330
  • 文/蒙蒙 一怖辆、第九天 我趴在偏房一處隱蔽的房頂上張望是复。 院中可真熱鬧,春花似錦竖螃、人聲如沸淑廊。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,904評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)季惩。三九已至,卻和暖如春腻格,著一層夾襖步出監(jiān)牢的瞬間画拾,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,023評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工菜职, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留青抛,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,146評(píng)論 3 370
  • 正文 我出身青樓酬核,卻偏偏與公主長(zhǎng)得像脂凶,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子愁茁,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,933評(píng)論 2 355

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