iOS多線程之NSOperation<三>

我們在前面兩節分別講了iOS多線程的Pthrea、NSThreadGCD,那么我們關于多線程的學習就剩下最后一個內容,就是NSOperation。
NSOperation NSOperation其實是對GCD的封裝,表示了一個獨立的計算單元。NSOperation本身是一個抽象類,對我們來說并沒有什么實用價值,但是系統幫我們封裝了兩個它的子類,并且我們也可以自己去封裝,它的之類都是用線程安全的方式來建立狀態、優先級、依賴性和取消等的模型。
很多執行任務類型的案例都很好的運用了NSOperation,包括網絡請求,圖像壓縮,自然語言處理或者其他很多需要返回處理后數據的、可重復的、結構化的、相對長時間運行的任務。
前面學習GCD的時候我們知道,GCD有兩個核心概念就是任務和隊列,不能只創建任務,然后置之不理,那樣并沒有什么用處,NSOperation也是一樣的,我們不能僅僅把計算單元做好之后就不管它了,我們還是和GCD一樣把它放進一個隊列中進行調度,這樣我們的計算單元才會運作起來,這時候我們就需要另一個概念:NSOperationQueue。
NSOperationQueue 控制著這些操作的執行,它扮演者任務調度的角色,它總是能在遵循先進先出的原則下‘讓高優先級操作’能先于‘低優先級操作’運行,使它管理的操作能基本。
接下來我們先不著急看NSOperation和NSOperationQueue給了我們什么接口,等我們先學習NSOperation有哪些操作可以使用。

狀態

NSOperation包含了一個十分優雅的狀態機來描述每一個操作的執行。

isReady → isExecuting → isFinished

為了替代不那么清晰的state屬性,狀態直接由上面那些keypath的KVO通知決定,也就是說,當一個操作在準備好被執行的時候,它發送了一個KVO通知給isReady的keypath,讓這個keypath對應的屬性isReady在被訪問的時候返回YES。

每一個屬性對于其他的屬性必須是互相獨立不同的,也就是同時只可能有一個屬性返回YES,從而才能維護一個連續的狀態: - isReady: 返回 YES 表示操作已經準備好被執行, 如果返回NO則說明還有其他沒有先前的相關步驟沒有完成。 - isExecuting: 返回YES表示操作正在執行,反之則沒在執行。 - isFinished : 返回YES表示操作執行成功或者被取消了,NSOperationQueue只有當它管理的所有操作的isFinished屬性全標為YES以后操作才停止出列,也就是隊列停止運行,所以正確實現這個方法對于避免死鎖很關鍵。

取消

早些取消那些沒必要的操作是十分有用的。取消的原因可能包括用戶的明確操作或者某個相關的操作失敗。

與之前的執行狀態類似,當NSOperation的-cancel狀態調用的時候會通過KVO通知isCancelled的keypath來修改isCancelled屬性的返回值,NSOperation需要盡快地清理一些內部細節,而后到達一個合適的最終狀態。這個時候isCancelled和isFinished的值將是YES,而isExecuting的值則為NO。

優先級

不可能所有的操作都是一樣重要,通過以下的順序設置queuePriority屬性可以加快或者推遲操作的執行:

NSOperationQueuePriorityVeryHigh
NSOperationQueuePriorityHigh
NSOperationQueuePriorityNormal
NSOperationQueuePriorityLow
NSOperationQueuePriorityVeryLow

此外,有些操作還可以指定threadPriority的值,它的取值范圍可以從0.0到1.0,1.0代表最高的優先級。鑒于queuePriority屬性決定了操作執行的順序,threadPriority則指定了當操作開始執行以后的CPU計算能力的分配,如果你不知道這是什么,好吧,你可能根本沒必要知道這是什么。

依賴性

根據你應用的復雜度不同,將大任務再分成一系列子任務一般都是很有意義的,而你能通過NSOperation
的依賴性實現。
比如說,對于服務器下載并壓縮一張圖片的整個過程,你可能會將這個整個過程分為兩個操作(可能你還會用到這個網絡子過程再去下載另一張圖片,然后用壓縮子過程去壓縮磁盤上的圖片)。顯然圖片需要等到下載完成之后才能被調整尺寸,所以我們定義網絡子操作是壓縮子操作的依賴,通過代碼來說就是:

[resizingOperation addDependency:networkingOperation];//設置resizingOperation依賴networkingOperation
[operationQueue addOperation:networkingOperation];//操作添加進隊列
[operationQueue addOperation:resizingOperation];//操作添加進隊列

除非一個操作的依賴的isFinished返回YES,不然這個操作不會開始。時時牢記將所有的依賴關系添加到操作隊列很重要,不然會像走路遇到一條大溝,就走不過去了喲。
此外,確保不要意外地創建依賴循環,像A依賴B,B又依賴A,這也會導致杯具的死鎖。

completionBlock

有一個在iOS 4和Snow Leopard新加入的十分有用的功能就是completionBlock屬性。

每當一個NSOperation執行完畢,它就會調用它的completionBlock屬性一次,這提供了一個非常好的方式讓你能在視圖控制器(View Controller)里或者模型(Model)里加入自己更多自己的代碼邏輯。比如說,你可以在一個網絡請求操作的completionBlock來處理操作執行完以后從服務器下載下來的數據。

對于現在Objective-C程序員必須掌握的工具中,NSOperation依然是最基本的一個。盡管GCD對于內嵌異步操作十分理想,NSOperation依舊提供更復雜、面向對象的計算模型,它對于涉及到各種類型數據、需要重復處理的任務又是更加理想的。在你的下一個項目里使用它吧,讓它及帶給用戶歡樂,你自己也會很開心的。

之前了解過NSOperation的同學們可能看出來了,沒錯,上面就是翻譯Mattt Thompson的,學過iOS的應該都知道這位大神。

下面我們開始學NSOperation和NSOperationQueue給我們提供的接口,然后針對常用接口的用法示例。

執行操作
//如果你子類化NSOperation類,你要重寫start方法,如果你直接沒有子類化,直接使用start,默認是在主線程執行的。
- (void)start;

//如果你子類化NSOperation類,你可以重寫main方法,并且實現它。如果你這樣做,你就沒有必要調用super。
//你絕不能直接調用mian方法,你應該通過調用start方法啟動你的線程。
//如果直接調用好像是會在主線程執行
- (void)main;

//在NSOperation的任務完成之后要執行的方法塊
@property(copy) void (^completionBlock)(void);
獲取Operation狀態
//Operation是否被取消,getter方法用isCancelled
  @property (readonly, getter=isCancelled) BOOL cancelled;

//取消一個Operation
- (void)cancel;

//Operation是否在執行中,getter方法用isExecuting
@property (readonly, getter=isExecuting) BOOL executing;

//Operation操作是否完成,getter方法用isFinished
@property (readonly, getter=isFinished) BOOL finished;

//Operation操作是否是異步的,getter方法用isConcurrent,不建議使用,被下邊的asynchronous方法取代了
@property (readonly, getter=isConcurrent) BOOL concurrent;

//同concurrent
@property (readonly, getter=isAsynchronous) BOOL asynchronous NS_AVAILABLE(10_8, 7_0);

//Operation操作是否已經準備就緒,getter方法用isFinished
@property (readonly, getter=isReady) BOOL ready;
依賴關系

依賴關系可以有效的安排操作的順序

//添加依賴,就是在你添加的依賴Operation完成之后當前Operation才能開始執行
- (void)addDependency:(NSOperation *)op;

//刪除一個之前添加的依賴Operation
- (void)removeDependency:(NSOperation *)op;

@property (readonly, copy) NSArray<NSOperation *> *dependencies;
優先級
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
    NSOperationQueuePriorityVeryLow = -8L,
    NSOperationQueuePriorityLow = -4L,
    NSOperationQueuePriorityNormal = 0,
    NSOperationQueuePriorityHigh = 4,
    NSOperationQueuePriorityVeryHigh = 8
}; //優先級
//NSOperationQueue的優先級,對應上面的枚舉
@property NSOperationQueuePriority queuePriority;

//這是線程優先級,已經被棄用了
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);

//這個東西在NSThread的時候已經說過了
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);

在NSThread的時候講的NSQualityOfService,想看就看一下,在最后。

等待NSOperation完成
//知道operation完成之前,后面的代碼都不會在執行,
//所以為了避免死鎖,在operation啟動之前不要調用該方法
- (void)waitUntilFinished;
NSOperationQueue
//添加一個操作
- (void)addOperation:(NSOperation *)op;

//添加一組Operation并且等到數組中的操作全部完成才會繼續執行
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);

//快速添加一個operation任務
- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);

//隊列中的operations
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;

//操作的數量
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);

//最大并發數
@property NSInteger maxConcurrentOperationCount;

//隊列掛起,getter方法為isSuspended
@property (getter=isSuspended) BOOL suspended;

//隊列名字
@property (nullable, copy) NSString *name NS_AVAILABLE(10_6, 4_0);

//說過了,去看NSThread最那里有寫http://www.lxweimin.com/p/b1962d8543ca
@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0);

//正在執行的操作的調度隊列,默認值是nil,只有在隊列中沒有正在執行或排隊的操作才能被設置這個屬性,否則跑出NSInvalidArgumentException異常。
@property (nullable, assign /* actually retain */) dispatch_queue_t underlyingQueue NS_AVAILABLE(10_10, 8_0);

//取消隊列中的所有操作,正在執行的操作不取消
- (void)cancelAllOperations;

//和Operations的waitUntilFinished方法一樣,這個是等待隊列中所有的操作完成
- (void)waitUntilAllOperationsAreFinished;

//獲取當前的操作隊列,只讀屬性
@property (class, readonly, strong, nullable) NSOperationQueue *currentQueue NS_AVAILABLE(10_6, 4_0);

//獲取主隊列,只讀屬性
@property (class, readonly, strong) NSOperationQueue *mainQueue NS_AVAILABLE(10_6, 4_0);

前面說了系統幫我們封裝好了NSOperation的兩個子類,下面我們就用這兩個子類實現一下NSOperation的常用方法。
終于說到了NSOperation的子類,看一下NSInvocationOperation和NSBlockOperation兩個的基本用法。
首先是不配合NSOperationQueue,看一下什么效果

NSLog(@"----");
    NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil]; //創建一個NSInvocationOperation 類型的Operation,綁定funcation方法
    [invocation start];
    NSLog(@"----");
    NSBlockOperation *block = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1]; //線程睡眠
        NSLog(@"---NSBlockOperation--%@", [NSThread currentThread]);
    }]; //創建一個NSBlockOperation 類型的Operation,添加塊方法
    [block start];

看一下輸出結果

2017-08-25 15:27:25.785 NSOperation[2944:455806] ----
2017-08-25 15:27:26.787 NSOperation[2944:455806] ---invocation---<NSThread: 0x60800007bf40>{number = 1, name = main}
2017-08-25 15:27:26.788 NSOperation[2944:455806] ----
2017-08-25 15:27:27.789 NSOperation[2944:455806] ---NSBlockOperation--<NSThread: 0x60800007bf40>{number = 1, name = main}

從輸出結果看都是在主線程執行中同步執行的,所以不配合NSOperationQueue并沒有什么意義。

NSInvocationOperation
- (void)invocation {
    NSInvocationOperation *invocation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
    NSInvocationOperation *invocation2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
    NSInvocationOperation *invocation3 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(funcation) object:nil];
    //1.創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:invocation];
    [queue addOperation:invocation2];
    [queue addOperation:invocation3];
}

看一下輸出結果

2017-08-25 15:41:34.088 NSOperation[2989:469234] ---invocation---<NSThread: 0x60000007d440>{number = 4, name = (null)}
2017-08-25 15:41:34.088 NSOperation[2989:469216] ---invocation---<NSThread: 0x608000260800>{number = 5, name = (null)}
2017-08-25 15:41:34.088 NSOperation[2989:469218] ---invocation---<NSThread: 0x6080002607c0>{number = 3, name = (null)}

輸出結果顯示每個Operation都開辟了新線程,并且異步執行,那NSBlockOperation會不會是一樣的呢,來看一下吧

- (void)block {
    NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"1----%@",[NSThread currentThread]);
    }];
    NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"2----%@",[NSThread currentThread]);
    }];
    NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"3----%@",[NSThread currentThread]);
    }];
    //創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];
}

一起看一下輸出結果

2017-08-25 15:49:41.484 NSOperation[3013:475949] 1----<NSThread: 0x60000007ea80>{number = 5, name = (null)}
2017-08-25 15:49:41.484 NSOperation[3013:475951] 2----<NSThread: 0x608000070740>{number = 4, name = (null)}
2017-08-25 15:49:41.484 NSOperation[3013:475975] 3----<NSThread: 0x60000007ea00>{number = 3, name = (null)}

和NSInvocationOperation的結果一樣呢,那接下來我們看一下還有什么好用的方法吧。
我們看到NSBlockOperation還有個addExecutionBlock方法,這是干嘛的呢?

- (void)block {
    NSBlockOperation *block1 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"1----%@",[NSThread currentThread]);
    }];
    NSBlockOperation *block2 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"2----%@",[NSThread currentThread]);
    }];
    [block2 addExecutionBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"4---%@", [NSThread currentThread]);
    }];
    NSBlockOperation *block3 = [NSBlockOperation blockOperationWithBlock:^{
        [NSThread sleepForTimeInterval:1];
        NSLog(@"3----%@",[NSThread currentThread]);
    }];
    //創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];
}

先來看下打印結果再說

2017-08-25 16:10:35.735 NSOperation[3114:492996] 3----<NSThread: 0x608000071180>{number = 5, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:493009] 2----<NSThread: 0x6000000711c0>{number = 4, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:493008] 1----<NSThread: 0x60800006a500>{number = 3, name = (null)}
2017-08-25 16:10:35.735 NSOperation[3114:492993] 4---<NSThread: 0x600000071280>{number = 6, name = (null)}

從結果來看似乎和新建一個NSBlockOperation效果一樣啊,都是異步執行。
看看隊列有中有什么好用的方法吧

//創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
[queue setMaxConcurrentOperationCount:2];
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];

輸出結果:
2017-08-25 15:53:02.280 NSOperation[3034:479336] 1----<NSThread: 0x60800007b7c0>{number = 3, name = (null)}
2017-08-25 15:53:02.280 NSOperation[3034:479338] 2----<NSThread: 0x600000263d00>{number = 4, name = (null)}
2017-08-25 15:53:03.353 NSOperation[3034:479335] 3----<NSThread: 0x60800007b740>{number = 5, name = (null)}

我們在剛剛的NSBlockOperation測試的基礎上加上了一句[queue setMaxConcurrentOperationCount:2],結果變得不一樣了呢,3要比1和2晚一秒鐘哦,剛好是3的睡眠時間,那就說明3是在1和2完成之后開始的,這就是最大并發量的作用,設置的數字就是它允許開辟的最大操作數數。
最后,我們發現除了剛開始在線程中任務順序執行外,我們一直沒講串行,因為NSOperationQueue是并行隊列,我們想要串行就把最大并發量設置為1就可以了,是不是簡單多了,不需要再怕不小心把串行并行寫錯了。
前面有句話看清楚哦,是最大任務數,不是線程數,看看下面這種情況你就清楚其中的區別了

//創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue setMaxConcurrentOperationCount:1];//設置最大并發量
//    [block1 addDependency:block3];//設置block1依賴block3
    block1.queuePriority = NSOperationQueuePriorityHigh;
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];

輸出結果:
2017-08-25 16:44:21.361 NSOperation[3366:527290] 1----<NSThread: 0x6000002644c0>{number = 3, name = (null)}
2017-08-25 16:44:22.435 NSOperation[3366:527290] 4---<NSThread: 0x6000002644c0>{number = 3, name = (null)}
2017-08-25 16:44:22.435 NSOperation[3366:527274] 2----<NSThread: 0x600000267c40>{number = 4, name = (null)}
2017-08-25 16:44:23.482 NSOperation[3366:527274] 3----<NSThread: 0x600000267c40>{number = 4, name = (null)}

所以看時間我們能看到2和4是同一時間執行的,所以setMaxConcurrentOperationCount只能控制人物數量。

前面說了好多遍依賴關系,看一下什么是依賴關系吧

//創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
    [queue setMaxConcurrentOperationCount:2];//設置最大并發量
    [block1 addDependency:block3];//設置block1依賴block3
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];

輸出結果:
2017-08-25 16:00:12.378 NSOperation[3058:484669] 2----<NSThread: 0x608000267d40>{number = 3, name = (null)}
2017-08-25 16:00:12.378 NSOperation[3058:484671] 3----<NSThread: 0x608000267d80>{number = 4, name = (null)}
2017-08-25 16:00:13.380 NSOperation[3058:484668] 1----<NSThread: 0x60000007ae80>{number = 5, name = (null)}

誒,隊列不是先進先出的嗎?為什么線程不夠的時候不是1和2先執行呢?反而3先執行了。噢,我們設置了1依賴3,所以當cpu要殺1的時候,1說“不行,要殺我,先殺3”,所以先執行了2和3,最后執行1。
也可以不初始化NSBlockOperation,直接在隊列中添加block操作哦。

//創建隊列
    NSOperationQueue *queue = [[NSOperationQueue alloc]init];
//    [queue setMaxConcurrentOperationCount:1];//設置最大并發量
//    [block1 addDependency:block3];//設置block1依賴block3
    block1.queuePriority = NSOperationQueuePriorityHigh;
    [queue addOperation:block1];
    [queue addOperation:block2];
    [queue addOperation:block3];
    
    [queue addOperationWithBlock:^{
        sleep(1);
        NSLog(@"5----%@",[NSThread currentThread]);
    }];

輸出結果:
2017-08-25 16:41:30.151 NSOperation[3346:524342] 5----<NSThread: 0x60000007c980>{number = 3, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524343] 1----<NSThread: 0x60000007cb80>{number = 4, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524345] 2----<NSThread: 0x608000071340>{number = 5, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524360] 3----<NSThread: 0x600000075c00>{number = 6, name = (null)}
2017-08-25 16:41:30.222 NSOperation[3346:524359] 4---<NSThread: 0x60800007c780>{number = 7, name = (null)}

看結果和用NSBlockOperation沒什么區別哦

最后的最后,關于iOS多線程就到這里了,希望對后學者能有一些幫助,那將是我的榮幸。

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

推薦閱讀更多精彩內容