初探coobjc源碼

文丨清楓

協(xié)程

今年年初阿里開源的coobjc,可謂是另iOS開發(fā)者們大開眼界柱锹。coobjc這個(gè)名字哪自,以co開頭,其實(shí)可以分解為co-objc禁熏,co就是coroutine(協(xié)程)單詞縮寫壤巷。
協(xié)程子例程一樣,協(xié)程(coroutine)也是一種程序組件瞧毙。相對(duì)子例程而言胧华,協(xié)程更為一般和靈活,但在實(shí)踐中使用沒(méi)有子例程那樣廣泛宙彪。協(xié)程源自 SimulaModula-2 語(yǔ)言矩动,但也有其他語(yǔ)言支持。
協(xié)程不是進(jìn)程或線程释漆,其執(zhí)行過(guò)程更類似于子例程悲没,或者說(shuō)不帶返回值的函數(shù)調(diào)用
一個(gè)程序可以包含多個(gè)協(xié)程男图,可以對(duì)比與一個(gè)進(jìn)程包含多個(gè)線程示姿,因而下面我們來(lái)比較協(xié)程和線程。我們知道多個(gè)線程相對(duì)獨(dú)立享言,有自己的上下文峻凫,切換受系統(tǒng)控制;而協(xié)程也相對(duì)獨(dú)立览露,有自己的上下文荧琼,但是其切換由自己控制,由當(dāng)前協(xié)程切換到其他協(xié)程由當(dāng)前協(xié)程來(lái)控制差牛。
協(xié)程的概念在60年代就已經(jīng)提出命锄,目前在服務(wù)端中應(yīng)用比較廣泛,在高并發(fā)場(chǎng)景下使用極其合適偏化,可以極大降低單機(jī)的線程數(shù)脐恩,提升單機(jī)的連接和處理能力,但是在移動(dòng)研發(fā)中侦讨,iOSandroid目前都不支持協(xié)程的使用驶冒。

coobjc實(shí)現(xiàn)了什么(來(lái)自官方文檔)

這個(gè)庫(kù)為 Objective-CSwift 提供了協(xié)程功能苟翻。coobjc支持 awaitgeneratoractor model骗污,接口參考了 C# 崇猫、JavascriptKotlin 中的很多設(shè)計(jì)。還提供了 cokit 庫(kù)FoundationUIKit 中的部分 API 提供了協(xié)程化支持需忿,包括 NSFileManager , JSON , NSData , UIImage 等诅炉。coobjc 也提供了元組的支持。

coobjc 是由手機(jī)淘寶架構(gòu)團(tuán)隊(duì)推出的能在 iOS 上使用的協(xié)程開發(fā)框架屋厘,目前支持 Objective-CSwift 中使用涕烧,底層使用匯編和 C 語(yǔ)言進(jìn)行開發(fā),上層進(jìn)行提供了 Objective-CSwift 的接口汗洒,目前以 Apache 開源協(xié)議進(jìn)行了開源议纯。

iOS異步編程的問(wèn)題

基于 Block 的異步編程回調(diào)是目前 iOS 使用最廣泛的異步編程方式,iOS 系統(tǒng)提供的 GCD 庫(kù)讓異步開發(fā)變得很簡(jiǎn)單方便仲翎,但是基于這種編程方式的缺點(diǎn)也有很多痹扇,主要有以下幾點(diǎn):

  • 容易進(jìn)入"嵌套地獄"
  • 錯(cuò)誤處理復(fù)雜和冗長(zhǎng)
  • 容易忘記調(diào)用 completion handler
  • 條件執(zhí)行變得很困難
  • 從互相獨(dú)立的調(diào)用中組合返回結(jié)果變得極其困難
  • 在錯(cuò)誤的線程中繼續(xù)執(zhí)行
  • 難以定位原因的多線程崩潰
  • 鎖和信號(hào)量濫用帶來(lái)的卡頓、卡死
    上述問(wèn)題反應(yīng)到線上應(yīng)用本身就會(huì)出現(xiàn)大量的多線程崩潰

解決方案

上述問(wèn)題在很多系統(tǒng)和語(yǔ)言中都會(huì)遇到溯香,解決問(wèn)題的標(biāo)準(zhǔn)方式就是使用協(xié)程。這里不介紹太多的理論浓恶,簡(jiǎn)單說(shuō)協(xié)程就是對(duì)基礎(chǔ)函數(shù)的擴(kuò)展玫坛,可以讓函數(shù)異步執(zhí)行的時(shí)候掛起然后返回值。協(xié)程可以用來(lái)實(shí)現(xiàn) generator 包晰,異步模型以及其他強(qiáng)大的能力湿镀。

Kotlin 是這兩年由 JetBrains 推出的支持現(xiàn)代多平臺(tái)應(yīng)用的靜態(tài)編程語(yǔ)言,支持 JVM 伐憾,Javascript 勉痴,目前也可以在iOS上執(zhí)行,這兩年在開發(fā)者社區(qū)中也是比較火树肃。
Kotlin 語(yǔ)言中基于協(xié)程的 async/await 蒸矛,generator/yield 等異步化技術(shù)都已經(jīng)成了語(yǔ)法標(biāo)配,Kotlin 協(xié)程官方文檔胸嘴。

官方文檔

coobjc的設(shè)計(jì)


最底層是協(xié)程內(nèi)核,包含了棧切換的管理耳奕、協(xié)程調(diào)度器的實(shí)現(xiàn)绑青、協(xié)程間通信channel的實(shí)現(xiàn)等诬像。
中間層是基于協(xié)程的操作符的包裝,目前支持async/await闸婴、Generator坏挠、Actor等編程模型。
最上層是對(duì)系統(tǒng)庫(kù)的協(xié)程化擴(kuò)展掠拳,目前基本上覆蓋了FoundationUIKit的所有IO和耗時(shí)方法癞揉。

核心實(shí)現(xiàn)原理

協(xié)程的核心思想是控制調(diào)用棧的主動(dòng)讓出和恢復(fù)。一般的協(xié)程實(shí)現(xiàn)都會(huì)提供兩個(gè)重要的操作:
Yield:是讓出cpu的意思溺欧,它會(huì)中斷當(dāng)前的執(zhí)行喊熟,回到上一次Resume的地方。
Resume:繼續(xù)協(xié)程的運(yùn)行姐刁。執(zhí)行Resume后芥牌,回到上一次協(xié)程Yield的地方。
基于線程的代碼執(zhí)行時(shí)候聂使,是沒(méi)法做出暫停操作的壁拉,現(xiàn)在要做的事情就是要代碼執(zhí)行能夠暫停,還能夠再恢復(fù)柏靶。 基本上代碼執(zhí)行都是一種基于調(diào)用棧的模型弃理,所以如果能把當(dāng)前調(diào)用棧上的狀態(tài)都保存下來(lái),然后再能從緩存中恢復(fù)屎蜓,那就能夠?qū)崿F(xiàn)yieldresume痘昌。
實(shí)現(xiàn)這樣操作有幾種方法呢?
第一種:利用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ǔ)法糖。

上述第三種和第四種只是能過(guò)做到跳轉(zhuǎn)荐吵,但是沒(méi)法保存調(diào)用棧上的狀態(tài)骑冗,看起來(lái)基本上不能算是實(shí)現(xiàn)了協(xié)程,只能算做做demo捍靠,第五種除非官方支持沐旨,否則自行改寫編譯器通用性很差。而第一種方案的 ucontextiOS上是廢棄了的榨婆,不能使用磁携。coobjc使用的是第二種方案,自己用匯編模擬一下 ucontext良风。

coobjc的實(shí)現(xiàn)代碼

coroutine_context.h文件中聲明的方法:

extern int coroutine_getcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_setcontext (coroutine_ucontext_t *__ucp);
extern int coroutine_begin (coroutine_ucontext_t *__ucp);
extern void coroutine_makecontext (coroutine_ucontext_t *__ucp, IMP func, void *arg, void *stackTop);

原有的C協(xié)程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;
}

可改寫成:

#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é)果同樣是不斷輸出以下內(nèi)容:

Hello world
Hello world
Hello world
Hello world
...

協(xié)程結(jié)構(gòu)的設(shè)計(jì)

coroutine.h文件中看到如下結(jié)構(gòu):

/**
     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.
        
        struct coroutine *prev;
        struct coroutine *next;
        
        void *autoreleasepage;                  // If enable autorelease, the custom autoreleasepage.
        void *chan_alt;                         // If blocking by a channel, record the alt
        bool is_cancelled;                      // The coroutine is cancelled
        int8_t   is_scheduler;                  // The coroutine is a scheduler.
    };
    typedef struct coroutine coroutine_t;
    /**
     Define the linked list of scheduler's queue.
     */
    struct coroutine_list {
        coroutine_t *head;
        coroutine_t *tail;
    };
    typedef struct coroutine_list coroutine_list_t;

從以上結(jié)構(gòu)我們不難看出谊迄,結(jié)構(gòu)體的內(nèi)容包含了一個(gè)協(xié)程的所在狀態(tài)和所所持有的信息闷供。協(xié)程隊(duì)列的增減是通過(guò)鏈表結(jié)構(gòu)實(shí)現(xiàn)。

    /**
     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;

上面的結(jié)構(gòu)是管理每個(gè)協(xié)程的調(diào)度器結(jié)構(gòu)统诺。

    /**
     Close and free a coroutine if dead.

     @param co coroutine object
     */
    void coroutine_close_ifdead(coroutine_t *co);
    
    /**
     Add coroutine to scheduler, and resume the specified coroutine whatever.
     */
    void coroutine_resume(coroutine_t *co);
    
    /**
     Add coroutine to scheduler, and resume the specified coroutine if idle.
     */
    void coroutine_add(coroutine_t *co);
    
    /**
     Yield the specified coroutine now.
     */
    void coroutine_yield(coroutine_t *co);

上面可以看到協(xié)程的生命周期歪脏。

使用coobjc

async/await

  • 創(chuàng)建協(xié)程

使用 co_launch 方法創(chuàng)建協(xié)程

co_launch(^{
    ...
});

co_launch 創(chuàng)建的協(xié)程默認(rèn)在當(dāng)前線程進(jìn)行調(diào)度

  • await 異步方法

在協(xié)程中我們使用 await 方法等待異步方法執(zhí)行結(jié)束,得到異步執(zhí)行結(jié)果

- (void)viewDidLoad{
    ...
        co_launch(^{
            NSData *data = await(downloadDataFromUrl(url));
            UIImage *image = await(imageFromData(data));
            self.imageView.image = image;
        });
}

上述代碼將原本需要 dispatch_async 兩次的代碼變成了順序執(zhí)行粮呢,代碼更加簡(jiǎn)潔

  • 錯(cuò)誤處理

在協(xié)程中婿失,我們所有的方法都是直接返回值的,并沒(méi)有返回錯(cuò)誤啄寡,我們?cè)趫?zhí)行過(guò)程中的錯(cuò)誤是通過(guò) co_getError() 獲取的豪硅,比如我們有以下從網(wǎng)絡(luò)獲取數(shù)據(jù)的接口,在失敗的時(shí)候挺物, promise 會(huì) reject:error懒浮。

- (COPromise*)co_GET:(NSString*)url
  parameters:(NSDictionary*)parameters{
    COPromise *promise = [COPromise promise];
    [self GET:url parameters:parameters progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {
        [promise fulfill:responseObject];
    } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
        [promise reject:error];
    }];
    return promise;
}

那我們?cè)趨f(xié)程中可以如下使用:

co_launch(^{
    id response = await([self co_GET:feedModel.feedUrl parameters:nil]);
    if(co_getError()){
        //處理錯(cuò)誤信息
    }
    ...
});

生成器

  • 創(chuàng)建生成器

我們使用 co_sequence 創(chuàng)建生成器

COCoroutine *co1 = co_sequence(^{
            int index = 0;
            while(co_isActive()){
                yield_val(@(index));
                index++;
            }
        });

在其他協(xié)程中,我們可以調(diào)用 next 方法识藤,獲取生成器中的數(shù)據(jù)

co_launch(^{
            for(int i = 0; i < 10; i++){
                val = [[co1 next] intValue];
            }
        });
  • 使用場(chǎng)景

生成器可以在很多場(chǎng)景中進(jìn)行使用砚著,比如消息隊(duì)列、批量下載文件痴昧、批量加載緩存等:

int unreadMessageCount = 10;
NSString *userId = @"xxx";
COSequence *messageSequence = sequenceOnBackgroundQueue(@"message_queue", ^{
   //在后臺(tái)線程執(zhí)行
    while(1){
        yield(queryOneNewMessageForUserWithId(userId));
    }
});

//主線程更新UI
co(^{
   for(int i = 0; i < unreadMessageCount; i++){
       if(!isQuitCurrentView()){
           displayMessage([messageSequence take]);
       }
   }
});

通過(guò)生成器稽穆,我們可以把傳統(tǒng)的生產(chǎn)者加載數(shù)據(jù)->通知消費(fèi)者模式,變成消費(fèi)者需要數(shù)據(jù)->告訴生產(chǎn)者加載模式赶撰,避免了在多線程計(jì)算中秧骑,需要使用很多共享變量進(jìn)行狀態(tài)同步,消除了在某些場(chǎng)景下對(duì)于鎖的使用扣囊。

Actor

_ Actor 的概念來(lái)自于 Erlang ,在 AKKA 中绒疗,可以認(rèn)為一個(gè) Actor 就是一個(gè)容器侵歇,用以存儲(chǔ)狀態(tài)、行為吓蘑、Mailbox 以及子 Actor 與 Supervisor 策略惕虑。Actor 之間并不直接通信,而是通過(guò) Mail 來(lái)互通有無(wú)磨镶。_

  • 創(chuàng)建 actor

我們可以使用 co_actor_onqueue 在指定線程創(chuàng)建 actor

COActor *actor = co_actor_onqueue(^(COActorChan *channel) {
    ...  //定義 actor 的狀態(tài)變量
    for(COActorMessage *message in channel){
        ...//處理消息
    }
}, q);
  • 給 actor 發(fā)送消息

actor 的 send 方法可以給 actor 發(fā)送消息

COActor *actor = co_actor_onqueue(^(COActorChan *channel) {
    ...  //定義actor的狀態(tài)變量
    for(COActorMessage *message in channel){
        ...//處理消息
    }
}, q);

// 給actor發(fā)送消息
[actor send:@"sadf"];
[actor send:@(1)];

元組

  • 創(chuàng)建元組

使用 co_tuple 方法來(lái)創(chuàng)建元組

COTuple *tup = co_tuple(nil, @10, @"abc");
NSAssert(tup[0] == nil, @"tup[0] is wrong");
NSAssert([tup[1] intValue] == 10, @"tup[1] is wrong");
NSAssert([tup[2] isEqualToString:@"abc"], @"tup[2] is wrong");

可以在元組中存儲(chǔ)任何數(shù)據(jù)

  • 元組取值

可以使用 co_unpack 方法從元組中取值

id val0;
NSNumber *number = nil;
NSString *str = nil;
co_unpack(&val0, &number, &str) = co_tuple(nil, @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");

co_unpack(&val0, &number, &str) = co_tuple(nil, @10, @"abc", @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");

co_unpack(&val0, &number, &str, &number, &str) = co_tuple(nil, @10, @"abc");
NSAssert(val0 == nil, @"val0 is wrong");
NSAssert([number intValue] == 10, @"number is wrong");
NSAssert([str isEqualToString:@"abc"], @"str is wrong");

NSString *str1;

co_unpack(nil, nil, &str1) = co_tuple(nil, @10, @"abc");
NSAssert([str1 isEqualToString:@"abc"], @"str1 is wrong");
  • 在協(xié)程中使用元組

首先創(chuàng)建一個(gè) promise 來(lái)處理元組里的值

COPromise<COTuple*>*
cotest_loadContentFromFile(NSString *filePath){
    return [COPromise promise:^(COPromiseFullfill  _Nonnull resolve, COPromiseReject  _Nonnull reject) {
        if ([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
            NSData *data = [[NSData alloc] initWithContentsOfFile:filePath];
            resolve(co_tuple(filePath, data, nil));
        }
        else{
            NSError *error = [NSError errorWithDomain:@"fileNotFound" code:-1 userInfo:nil];
            resolve(co_tuple(filePath, nil, error));
        }
    }];
}

然后溃蔫,你可以像下面這樣獲取元組里的值:

co_launch(^{
    NSString *tmpFilePath = nil;
    NSData *data = nil;
    NSError *error = nil;
    co_unpack(&tmpFilePath, &data, &error) = await(cotest_loadContentFromFile(filePath));
    XCTAssert([tmpFilePath isEqualToString:filePath], @"file path is wrong");
    XCTAssert(data.length > 0, @"data is wrong");
    XCTAssert(error == nil, @"error is wrong");
});

使用元組你可以從 await 返回值中獲取多個(gè)值

協(xié)程的優(yōu)點(diǎn)

  • 簡(jiǎn)明
    • 概念少:只有很少的幾個(gè)操作符,相比響應(yīng)式幾十個(gè)操作符琳猫,簡(jiǎn)直不能再簡(jiǎn)單了
    • 原理簡(jiǎn)單: 協(xié)程的實(shí)現(xiàn)原理很簡(jiǎn)單伟叛,整個(gè)協(xié)程庫(kù)只有幾千行代碼
  • 易用
    • 使用簡(jiǎn)單:它的使用方式比 GCD 還要簡(jiǎn)單,接口很少
    • 改造方便:現(xiàn)有代碼只需要進(jìn)行很少的改動(dòng)就可以協(xié)程化脐嫂,同時(shí)我們針對(duì)系統(tǒng)庫(kù)提供了大量協(xié)程化接口
  • 清晰
    • 同步寫異步邏輯:同步順序方式寫代碼是人類最容易接受的方式统刮,這可以極大的減少出錯(cuò)的概率
    • 可讀性高: 使用協(xié)程方式編寫的代碼比 block 嵌套寫出來(lái)的代碼可讀性要高很多
  • 性能
    • 調(diào)度性能更快:協(xié)程本身不需要進(jìn)行內(nèi)核級(jí)線程的切換紊遵,調(diào)度性能快,即使創(chuàng)建上萬(wàn)個(gè)協(xié)程也毫無(wú)壓力
    • 減少卡頓卡死: 協(xié)程的使用以幫助開發(fā)減少鎖侥蒙、信號(hào)量的濫用暗膜,通過(guò)封裝會(huì)引起阻塞的 IO 等協(xié)程接口,可以從根源上減少卡頓鞭衩、卡死学搜,提升應(yīng)用整體的性能

參考文獻(xiàn)

官方文檔
剛剛,阿里開源 iOS 協(xié)程開發(fā)框架 coobjc
阿里開源 iOS 協(xié)程開發(fā)框架coobjc源碼分析

最后編輯于
?著作權(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)容