記一次NSURLSession下載開發(fā)任務(wù)

1. 需求背景

公司的app需要需要支持訂閱更新的自動下載功能缺狠。當(dāng)訂閱更新的靜默推送將app啟動到后臺時吧恃,在后臺開始下載更新的內(nèi)容。

本文主要記錄以下我開發(fā)過程中對于方案的選擇牲剃,開發(fā)需要注意的細(xì)節(jié)和踩過的坑鳖轰。如果需要先了解系統(tǒng)生命周期和NSURLSession后臺下載的相關(guān)機制清酥,那么可以移步本文最后相關(guān)資料的部分,找到相關(guān)資料的鏈接自行了解蕴侣。

2. 整體方案

2.1 NSURLSession對后臺下載的支持

使用如下backgroundSessionConfiguration創(chuàng)建的NSURLSession能夠提供以下幾個特別重要的能力焰轻,對于iOS上的后臺下載任務(wù)background session是不二選擇。(后面就叫backgroundSession了)
當(dāng)你通過backgroundSession創(chuàng)建的NSURLSessionDownloadTask開始之后:

  1. app切換到后臺進(jìn)入suspended狀態(tài)昆雀,這個downloadTask仍然可以在一個獨立的進(jìn)程中繼續(xù)進(jìn)行辱志,并在下載完成后系統(tǒng)會將你的app從suspended狀態(tài)切換到UIApplicationStateBackground, 讓你的app繼續(xù)處理下載任務(wù)蝠筑。
  2. app在后臺被系統(tǒng)殺死之后,downloadTask同樣會繼續(xù)進(jìn)行(注意如果用戶force quit你的app揩懒,那么下載任務(wù)會被取消什乙,并且在下一次啟動恢復(fù)backgroundSession后會有下載失敗的回調(diào))。任務(wù)完成時旭从,系統(tǒng)同樣會重新啟動你的app到UIApplicationStateBackground稳强,app可以通過相同的identifier重新創(chuàng)建backgroundSession并獲取到完成的downloadTask场仲,然后通過代理事件處理下載任務(wù)和悦。
  3. backgroundSession只支持HTTP and HTTPS協(xié)議,如果需要通過其他協(xié)議來進(jìn)行下載任務(wù)(比如ftp)渠缕,那么這部分的下載任務(wù)就需要考慮另行處理了鸽素。

2.2 AFNetworking or URLSession

AFNetworking對于大部分iOS開發(fā)者AFN已經(jīng)是相當(dāng)于網(wǎng)絡(luò)基礎(chǔ)庫一樣的存在了。AFNetworking封裝的API亦鳞,通過傳遞block的方式能夠非常方便的應(yīng)對復(fù)雜多樣的數(shù)據(jù)接口請求馍忽。但是下載任務(wù)一般就是一個GET請求,下載完成之后將文件存儲到對應(yīng)的沙盒路徑燕差。通過NSURLSession本身的代理回調(diào)到XXXDownloadManager之類的管理類中集中處理也不會增加很多工作量遭笋。

但是,這一次對開發(fā)我還是選擇了在AFN的基礎(chǔ)上進(jìn)行封裝:因為項目中原本的下載庫已經(jīng)使用了AFN來創(chuàng)建下載請求徒探,本著盡量減少改動的想法瓦呼,就沿用了之前的方案。

ps:后續(xù)的開發(fā)中發(fā)現(xiàn)测暗,AFN的封裝方式對于后臺下載的支持并不太友好央串,相比直接使用URLSession進(jìn)行開發(fā)也并沒有節(jié)省太多工作量。

3. 一些技術(shù)細(xì)節(jié)實現(xiàn)

3.1 暫停下載 - suspend vs cancelByProducingResumeData:

方案:很糾結(jié)的選擇了用suspend來暫停下載任務(wù)碗啄,原因如下:

實際測試這兩個方法都可以達(dá)到暫停下載的目的(至少用戶看起來是暫停)质和,但是蘋果開發(fā)論壇上找到如下官方回復(fù)
Background Transfer Service: suspend DownloadTa… |Apple Developer Forums

Tasks suspension is rarely used and, when it is, it’s mostly used to temporarily disable callbacks as part of some sort of concurrency control system. That’s because a suspended task can still be active on the wire; all that the suspend does is prevent it making progress internally, issuing callbacks, and so on.
OTOH, if you’re implementing a long-term pause (for example, the user wants to pause a download), you’d be better off calling -cancelByProducingResumeData

來自官方的說法是很明確的,cancelByProducingResumeData應(yīng)該是最合理的選擇稚字。但是所有下載任務(wù)在cancel時都能生成resumeData嗎饲宿?文檔是這么說的:

1CB402F8-BE44-4217-B456-2DF03B0A5639.png

結(jié)合以上兩點我是這么判斷的:

  1. 下載源是可控并滿足生成resumeData,那么毫無疑問通過cancelByProducingResumeData來暫停下載是最佳選擇胆描。
  2. 如果下載源不可控褒傅,那么可以選擇suspend來暫停下載。但是需要做好下載任務(wù)最終超時失敗的處理袄友,不然用戶就會奇怪的發(fā)現(xiàn)殿托,我已經(jīng)暫停了所有下載問題怎么一直提醒我下載失敗剧蚣?支竹。

綜上旋廷,由于app面對的下載源不可控最終還是選擇了suspend來暫停任務(wù)。

ps:iOS10.2之前的?生成的resumeData有bug礼搁,可能會導(dǎo)致resumeData不可用饶碘,可以用下面鏈接中的方式對判斷系統(tǒng)版本resumeData進(jìn)行處理。
ios - Resume NSUrlSession on iOS10 - Stack Overflow馒吴;

3.2 關(guān)于移動網(wǎng)絡(luò)訪問的限制

方案:使用兩個NSURLSession搭配不同的allowCellularAccess屬性進(jìn)行移動網(wǎng)絡(luò)訪問限制扎运。

3.2.1 為什么不監(jiān)聽Reachability變化的通知來做網(wǎng)絡(luò)限制?

之前項目中下載的網(wǎng)絡(luò)環(huán)境限制是通過AFReachabilityManager的通知來做的饮戳。這種方式無論app在前臺或者后臺豪治,只要代碼正在運行的時是沒有問題的。在使用backgroundSession之后扯罐,app沒有運行的時候下載任務(wù)仍然在繼續(xù)负拟,這種方式就有點力不從心了。 在這種情況下想要限制網(wǎng)絡(luò)的訪問歹河,就必須通過NSURLSession的本身機制來實現(xiàn)網(wǎng)絡(luò)限制掩浙。

NSURLSession中有兩個地方可以限制移動網(wǎng)絡(luò)訪問:

  1. NSURLSessionallowsCellularAccess屬性,這是一個readonly的屬性秸歧,在session初始化的時候會根據(jù)傳入的configuration對象的對應(yīng)屬性決定厨姚。
  2. NSURLRequestallowCellularAccess屬性,在創(chuàng)建request對象的時候可以設(shè)置键菱。

需要注意的是以上兩個地方都只能在實例初始化的時候設(shè)置一次谬墙,不能隨時改動。并且在NSURLSessionNSURLRequest同時設(shè)置該屬性的時候纱耻,更嚴(yán)格的設(shè)置會生效芭梯,也就是說只要有一個地方限制了移動網(wǎng)絡(luò)那么最終生成的downloadTask

ps:雖然下載任務(wù)的流量限制已經(jīng)完全交給session管理弄喘。但是AFReachabilityManager的網(wǎng)絡(luò)監(jiān)聽仍然保留玖喘,因為網(wǎng)絡(luò)變化時session對請求的暫停或開始事件并沒有回調(diào)蘑志,仍然需要這個監(jiān)聽在網(wǎng)絡(luò)環(huán)境變化的更新UI用累奈。

3.2.2 下載任務(wù)網(wǎng)絡(luò)限制狀態(tài)的切換

方案:使用兩個NSURLSession,在session的層面做控制急但。

從產(chǎn)品的角度需要可以單獨控制每個下載請求的移動網(wǎng)絡(luò)訪問限制澎媒。(比如用戶在移動網(wǎng)絡(luò)環(huán)境下手動繼續(xù)了某個下載,那么就需要解除這個下載的移動網(wǎng)絡(luò)訪問限制)波桩。
考慮到以單個請求作為控制的粒度戒努,放在NSURLRequest上設(shè)置看起來是合理的,為了重新設(shè)置allowsCellularAccess屬性镐躲,需要先取消當(dāng)前的downloadTask然后重新發(fā)起請求储玫。但在cancelByProducingResumeData:downloadTaskWithResumeData:之間卻發(fā)現(xiàn)并沒有API可以切換下載任務(wù)的allowsCellularAccess屬性侍筛。
這下尷尬了,難道要舍棄已經(jīng)下載了一大半的數(shù)據(jù)重新創(chuàng)建一個NSURLRequest嗎撒穷? 這顯然不劃算匣椰。

所以我采用的方案是:
使用兩個NSURLSession在session的層面做控制。
一個允許移動網(wǎng)絡(luò)訪問的commonSession端礼,
一個禁止移動網(wǎng)絡(luò)訪問的nonCellularAccessSession禽笑。

需要切換網(wǎng)絡(luò)權(quán)限時,先通過cancelByProducingResumeData:拿到resumeData蛤奥,然后在對應(yīng)的session上downloadTaskWithResumeData:繼續(xù)下載任務(wù)佳镜。

完美解決后臺下載網(wǎng)絡(luò)限制的需求, 這部分的代碼長這樣


- (void)p_changeCellularAccessForDownload:(XXXDownloadModel *)download newStrategy:(BOOL)allowed {
    //  1. nonCellularSessionManager 中的任務(wù)
    BOOL foo = download.dataTask && [self.nonCellularSessionManager.downloadTasks containsObject:(NSURLSessionDownloadTask *)download.dataTask];
    if (foo) {
        if (allowed == NO) {
            download.allowCellularAccess = allowed;
            return;
        } else {
            NSURLSessionDownloadTask *downloadTask = (NSURLSessionDownloadTask *)download.dataTask;
            BOOL suspended = (downloadTask.state != NSURLSessionTaskStateRunning);
            [downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
                xxx_dispatch_queue_async_safe(self.syncQueue, ^{
                    download.resumeData = resumeData;
                    download.allowCellularAccess = allowed;
                    [self p_addDownloadItem:download suspended:suspended];
                });
            }];
            return;
        }
    }
    
    //   2. commonSessionManager 中的任務(wù)
    BOOL bar = download.dataTask && [self.commonSessionManager.downloadTasks containsObject:(NSURLSessionDownloadTask *)download.dataTask];
    if (bar) {
        if (YES == allowed) {
            download.allowCellularAccess = allowed;
            return;
        } else {
            NSURLSessionDownloadTask *downloadTask = (NSURLSessionDownloadTask *)download.dataTask;
            BOOL suspended = (downloadTask.state == NSURLSessionTaskStateSuspended);
            download.preferToIgnoreErrorToast = YES;
            [downloadTask cancelByProducingResumeData:^(NSData * _Nullable resumeData) {
                xxx_dispatch_queue_async_safe(self.syncQueue, ^{
                    download.resumeData = resumeData;
                    download.allowCellularAccess = allowed;
                    [self p_addDownloadItem:download suspended:suspended];
                });
            }];
            return;
        }
    }
    //  3. 設(shè)置未開始的任務(wù)
    if ([self.waitingModels containsObject:download]) {
        download.allowCellularAccess = allowed;
        return;
    }
    //  4. addSuspendDownloadWithURL添加的任務(wù) (通過[task suspend]的任務(wù)應(yīng)該也會在2.3.步驟中處理)
    download.allowCellularAccess = allowed;
}

3.3 啟動時從Session中恢復(fù)下載任務(wù)

方案:每次app啟動后都要使用相同的identifier創(chuàng)建以上兩個session,然后通過session getTasksWithCompletionHandler:獲得所有downloadTask喻括,然后使用downloadTask.originalRequest.URL作為唯一標(biāo)識邀杏,從持久化的數(shù)據(jù)中恢復(fù)下載任務(wù)贫奠。

為什么使用URL而不是downloadTask.description或者downloadTask.identifier作為恢復(fù)任務(wù)時的唯一標(biāo)識唬血?

首先需要注意的是,請求可能被重定向唤崭,所以要使用originalRequest.URL而不是downloadTask.currentReqeust.URL拷恨。

task.description這個屬性在app被系統(tǒng)殺死后會丟失,顯然不適合谢肾。
downloadTask.identifier實測可以在app兩次啟動之間保持一致腕侄。但是因為下載任務(wù)分布在兩個session,使用該屬性判斷需要先判斷session芦疏,增加了額外的復(fù)雜度冕杠。而一般情況下,每個下載任務(wù)的URL都是不同的酸茴。使用URL作為唯一標(biāo)識已經(jīng)能夠滿足需要分预。

重建session的線程同步問題

從session中恢復(fù)下載任務(wù)的過程可以分為以下兩種情況:

  1. 手動啟動app過程中發(fā)現(xiàn)進(jìn)行中的下載任務(wù)。


    下載總結(jié).002.jpeg
  2. 下載任務(wù)完成通過background session的回調(diào)啟動app

    下載總結(jié).001.jpeg

可以看到這種情況任務(wù)的恢復(fù)陷入的僵局薪捍。實際上任務(wù)已經(jīng)下載完成笼痹,但是不得不重新下載任務(wù),而且可能會陷入不斷重新下載任務(wù)的死循環(huán)酪穿。(雖然iOS會限制background session 在后臺啟動的次數(shù)但是仍然很不好 )

一開始調(diào)試的時候app啟動頻繁凳干,基本上遇到的是情況1. 雖然會遇到一些下載狀態(tài)異常,但是因為考慮到background session的機制比較tricky被济,而且又是開發(fā)中救赐,出現(xiàn)也沒有那么頻繁沒有介意(其實是因為任務(wù)丟失之后,我會自動重新開始下載只磷。经磅。)少欺。但是在版本發(fā)布之后因為一些統(tǒng)計數(shù)據(jù)的異常發(fā)現(xiàn)了情況2的問題,我的解決方案如下:

下載總結(jié).003.jpeg

目前來看應(yīng)該可以滿足業(yè)務(wù)需求馋贤。
至于為什么不直接在NSURLDownloadTask回調(diào)時再從持久化數(shù)據(jù)中尋找和綁定到對應(yīng)的任務(wù)上赞别。

  1. 考慮到機制改動的風(fēng)險和開發(fā)成本。配乓。
  2. 懶仿滔。。

3.4 基于AFN完成下載時需要注意的地方

3.4.1 通過AFURLSessionManager的回調(diào)

必須通過AFURLSessionManager中的setXXXBlock:等方法設(shè)置回調(diào)的block犹芹,在這些block中處理下載請求的回調(diào)

先看AFURLSession的初始化中的部分代碼

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {
   //   根據(jù)傳入的configuration進(jìn)行初始化...

    //  在初始化方法的最后從session中獲取tasks并且設(shè)置delegate
    [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {
    //  獲取downloadTask并且為downloadTask設(shè)置AFURLSessionManagerTaskDelegate, 然而設(shè)置的progressBlock & destinationBlock & completionBlock都是nil
        for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {
            [self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];
        }
    }];
//  設(shè)置dataTasks & uploadTasks的代理
//  ...
    return self;
}

可以看到調(diào)用addDelegateForDownloadTask: progress:destination:completionHandler:方法的時候崎页,幾個block參數(shù)都傳的nil。按照默認(rèn)的處理邏輯腰埂,這些在AFURLSessionManager初始化時恢復(fù)的task最終會默默的無感知的以失敗結(jié)束飒焦。如果你想要自己設(shè)置AFURLSessionManagerTaskDelegate的參數(shù),AFN并沒有對外暴露這個類屿笼。幸運的是AFN還提供了setBlock…系列API牺荠。AFNURLSessionManager在收到session的代理事件回調(diào)時,在把事件傳遞給會AFURLSessionManagerTaskDelegate的同時會執(zhí)行這些設(shè)置好的block驴一。

AFNetworking setblock api.png

problem solved休雌!

3.4.2 關(guān)于AFURLSessionManagercompletionQueue

completionQueue只針對AFURLSessionManagerTaskDelegatesetXXXBlock:系列方法設(shè)置的block并不會在completionQueue上執(zhí)行肝断,而是在session初始化的時候制定的operationQueue上執(zhí)行杈曲,所以你提供的block需要自己處理線程同步

類似下面這樣:

- (void)setTaskDidCompleteBlockForSessionManager:(AFHTTPSessionManager *)manager {
    __weak typeof(self) weakSelf = self;

    [manager setTaskDidCompleteBlock:^(NSURLSession * _Nonnull session, NSURLSessionTask * _Nonnull task, NSError * _Nullable error) {
        __strong typeof(weakSelf) self = weakSelf;
        __block NSError *blockError = error;
        p_dispatch_queue_async_safe(self.syncQueue, ^{
                //  處理下載任務(wù)完成
        };
    }];
}

3.5 關(guān)于下載并發(fā)數(shù)量的限制

如果你所有下載源都來自同一個host,那么你大可以通過NSURLSessionHTTPMaximumConnectionsPerHost屬性來進(jìn)行限制胸懈。
如果你的下載源來自很多不同的host(比如我面對的情況)担扑,那么并發(fā)數(shù)量只能自己進(jìn)行限制了,具體限制的并發(fā)數(shù)量是多少就見仁見智了趣钱。

還有一點需要考慮的是涌献,backgroundSession喚起你app的間隔是會隨著喚起次數(shù)指數(shù)遞增的。
如果你有30個下載任務(wù)羔挡,策略1:每三個任務(wù)完成之后再開始三個任務(wù)洁奈。策略2: 同時開啟30個下載任務(wù),30個下載任務(wù)都完成之后session喚起你的app绞灼。蘋果開發(fā)人員的建議是使用策略2的利术,具體可以看下面這個鏈接NSURLSession’s Resume Rate Limiter |Apple Developer Forums

4. 測試調(diào)試

一開始低矮,調(diào)試backgroundSession相關(guān)的代碼會詭異到讓我懷疑人生印叁。建議所有要調(diào)試backgroundSession相關(guān)的業(yè)務(wù),尤其是和suspention/relaunch機制相關(guān)業(yè)務(wù)的同學(xué)仔細(xì)看一下這個官方論壇上的帖子

這里只簡要的說明一下幾個注意點:

盡量使用真機進(jìn)行調(diào)試

一開始用XCode配合模擬器調(diào)試的時候經(jīng)常會遇到NSPosixErrorDomain code 2 file not fuond..這個錯誤轮蜕。這是因為XCode每次運行時會改變app的路徑昨悼,導(dǎo)致downloadTaskByResumeData:時傳入的resumeData指向的路徑失效造成的)。帖子中提到了這個問題跃洛,并且建議使用真機進(jìn)行調(diào)試可以規(guī)避這個問題率触。然而我在使用真機進(jìn)行調(diào)試時也會遇到這個問題。

使用exit()方法調(diào)試app重新啟動相關(guān)的邏輯

使用Xcode debugger進(jìn)行調(diào)試的時候汇竭,debugger會防止app進(jìn)入suspended狀態(tài)葱蝗。并且app進(jìn)入suspended狀態(tài)或者進(jìn)入suspended狀態(tài)然后被系統(tǒng)kill是不會有任何通知或回調(diào)的。所以不要傻傻的鎖上屏幕等著app進(jìn)入suspended狀態(tài)或者被系統(tǒng)skill了细燎。正確的做法是使用exit()方法來退出app两曼。這是app會等同于被系統(tǒng)殺死的狀態(tài)。然后就可以根據(jù)你的需要玻驻,用各種方式啟動app進(jìn)行調(diào)試悼凑,包括但不限于:

  1. 直接用Xcode再run一遍。
  2. 手動啟動app璧瞬。
  3. 使用靜默推送的通知啟動app到后臺户辫。
  4. 設(shè)置scheme中的Background Fetch調(diào)試選項,可以通過xcode把app啟動到后臺彪蓬。(相當(dāng)于從suspended狀態(tài)喚起到后臺)

另外多提一句寸莫。只要在scheme中把launch選項設(shè)置成 wait for executable to be launched捺萌。那么通過方式2/3/4啟動app也可以通過debugger調(diào)試档冬。

6F6B4C44-DA1D-4270-B7DA-F594B2907966.png

使用[session invalidateAndCancel]來讓session恢復(fù)初始狀態(tài)

從任務(wù)管理頁面force quit你的app并不會讓backgroundSession恢復(fù)初始狀態(tài)。如果需要讓backgroundSession回到初始狀態(tài)來進(jìn)行調(diào)試桃纯,那么[backgroundSession invalidateAndCancel]或者刪除app重新安裝才能讓你的session回到初始狀態(tài)酷誓。
ps:[backgroundSession invalidateAndCancel]之后這個session實例對象就不可用了,需要重新創(chuàng)建實例或者直接重新啟動app态坦。

更多調(diào)試相關(guān)的注意事項請看下面的帖子:

總結(jié)

暫時沒想到什么可總結(jié)的盐数,有空的時候準(zhǔn)備補一個demo,就醬伞梯。
PS:整體還是一個比較蛋疼的方案玫氢,如果讓我重新開始寫一個下載框架,我應(yīng)該會拋棄AFNetworking谜诫,在NSURLSession/ NSURLRequest的基礎(chǔ)上去寫漾峡。

5. 參考資料

官方文檔

NSURLSession相關(guān)教程

第三方庫:

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末喻旷,一起剝皮案震驚了整個濱河市生逸,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖槽袄,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件烙无,死亡現(xiàn)場離奇詭異,居然都是意外死亡遍尺,警方通過查閱死者的電腦和手機截酷,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來乾戏,“玉大人合搅,你說我怎么就攤上這事∑缃叮” “怎么了灾部?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵,是天一觀的道長惯退。 經(jīng)常有香客問我赌髓,道長,這世上最難降的妖魔是什么催跪? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任锁蠕,我火速辦了婚禮,結(jié)果婚禮上懊蒸,老公的妹妹穿的比我還像新娘荣倾。我一直安慰自己,他們只是感情好骑丸,可當(dāng)我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布舌仍。 她就那樣靜靜地躺著,像睡著了一般通危。 火紅的嫁衣襯著肌膚如雪铸豁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天菊碟,我揣著相機與錄音节芥,去河邊找鬼。 笑死逆害,一個胖子當(dāng)著我的面吹牛头镊,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播魄幕,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼相艇,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了梅垄?” 一聲冷哼從身側(cè)響起厂捞,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤输玷,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后靡馁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體欲鹏,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年臭墨,在試婚紗的時候發(fā)現(xiàn)自己被綠了赔嚎。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡胧弛,死狀恐怖尤误,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情结缚,我是刑警寧澤损晤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布,位于F島的核電站红竭,受9級特大地震影響尤勋,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜茵宪,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一最冰、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧稀火,春花似錦暖哨、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至服球,卻和暖如春茴恰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背斩熊。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留伐庭,地道東北人粉渠。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像圾另,于是被迫代替她去往敵國和親霸株。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,916評論 2 344

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