iOS多線程——你要知道的GCD都在這里

你要知道的iOS多線程N(yùn)SThread、GCD叉存、NSOperation、RunLoop都在這里

轉(zhuǎn)載請(qǐng)注明出處 http://www.reibang.com/p/e9d8a087f6c0

本系列文章主要講解iOS中多線程的使用歼捏,包括:NSThread、GCD瓣履、NSOperation以及RunLoop的使用方法詳解,本系列文章不涉及基礎(chǔ)的線程/進(jìn)程袖迎、同步/異步、阻塞/非阻塞燕锥、串行/并行,這些基礎(chǔ)概念归形,有不明白的讀者還請(qǐng)自行查閱鼻由。本系列文章將分以下幾篇文章進(jìn)行講解连霉,讀者可按需查閱嗡靡。

GCD的使用姿勢(shì)全解

經(jīng)過前一篇文章的學(xué)習(xí),可以發(fā)現(xiàn)直接使用NSThread來編寫多線程程序有不少問題柿祈,線程在執(zhí)行完成后就會(huì)退出,每次執(zhí)行任務(wù)都需要?jiǎng)?chuàng)建一個(gè)線程很浪費(fèi)資源躏嚎,其次是需要我們自行進(jìn)行同步操作,自行管理線程的生命周期卢佣,如果要編寫并發(fā)的代碼或者多核的真正并行處理的代碼就比較復(fù)雜了。

本篇文章將會(huì)介紹一個(gè)抽象層次更高的多線程編寫方式GCD虚茶,GCD全稱Grand Central Dispatch是蘋果提供的一個(gè)多核編程的解決方案仇参,在真正意義上實(shí)現(xiàn)了并行操作婆殿,而不是并發(fā)。

GCD使用線程池模型來執(zhí)行用戶提交的任務(wù)婆芦,所以它比較節(jié)約資源,不需要為每個(gè)任務(wù)都重新創(chuàng)建一個(gè)新的線程消约,GCD不需要自行編寫并行代碼,而是自動(dòng)進(jìn)行多核的并行計(jì)算荆陆,自動(dòng)管理線程的生命周期,如:使用線程池管理線程的創(chuàng)建和銷毀被啼,線程的調(diào)度,任務(wù)的調(diào)度等浓体,用戶只需要編寫任務(wù)代碼并提交即可。

GCD中有兩個(gè)比較重要的概念:任務(wù)和隊(duì)列命浴。

GCD的任務(wù)

任務(wù)顧名思義就是我們需要執(zhí)行的代碼塊,可以是一個(gè)方法也可以是一個(gè)block媳溺,就是我們需要線程為我們完成的工作,編寫完成的任務(wù)只需提交給GCD的隊(duì)列悬蔽,即可自動(dòng)幫我們完成任務(wù)的調(diào)度,以及線程的調(diào)度蝎困,可以很方便的以多線程的方式執(zhí)行倍啥。

GCD的隊(duì)列

隊(duì)列用于管理用戶提交的任務(wù)禾乘,GCD的隊(duì)列有兩種形式虽缕,串行隊(duì)列和并發(fā)隊(duì)列:

  • 串行隊(duì)列: GCD底層只維護(hù)一個(gè)線程,任務(wù)只能串行依次執(zhí)行。
  • 并發(fā)隊(duì)列: GCD底層使用線程池維護(hù)多個(gè)線程弟塞,任務(wù)可并發(fā)執(zhí)行。

不論是串行隊(duì)列還是并發(fā)隊(duì)列都使用FIFO 先進(jìn)先出的方式來管理用戶提交的任務(wù)决记。

對(duì)于串行隊(duì)列來說,GCD每次從串行隊(duì)列的隊(duì)首取一個(gè)任務(wù)交給唯一的一個(gè)線程來處理系宫,直到前一個(gè)任務(wù)完成后建车,才繼續(xù)從隊(duì)列中取下一個(gè)任務(wù)來執(zhí)行扩借,因此缤至,串行隊(duì)列中的任務(wù)執(zhí)行嚴(yán)格按照提交順序,并且后一個(gè)任務(wù)必須等前一個(gè)任務(wù)執(zhí)行完成后才可以執(zhí)行领斥。

對(duì)于并發(fā)隊(duì)列來說,GCD每次從并發(fā)隊(duì)列的隊(duì)首取一個(gè)任務(wù)月洛,并將這個(gè)任務(wù)按照任務(wù)調(diào)度分發(fā)給多個(gè)線程中的某一個(gè)線程,此時(shí)不需要等待其完成嚼黔,如果隊(duì)列中還有其他任務(wù)繼續(xù)從隊(duì)列中取出并分發(fā)給某一個(gè)線程來執(zhí)行,由于底層由線程池管理多個(gè)線程唬涧,每個(gè)任務(wù)的時(shí)間復(fù)雜度不同再加上線程調(diào)度的影響,后提交的任務(wù)可能先執(zhí)行完成捧搞。但對(duì)于單個(gè)線程來說,只能按順序執(zhí)行,比如某個(gè)線程被安排了多個(gè)任務(wù)陌僵,那這個(gè)線程就只能按提交順序依次執(zhí)行任務(wù)。

所以碗短,我們?cè)谑褂?code>GCD時(shí)也就很簡單了,只需要?jiǎng)?chuàng)建或獲取系統(tǒng)隊(duì)列、編寫任務(wù)并提交任務(wù)到隊(duì)列即可纲堵。首先舉一個(gè)下載圖片的栗子,這個(gè)栗子和第一篇講解NSThread的栗子一樣席函,但是使用GCD來實(shí)現(xiàn):

//獲取一個(gè)優(yōu)先級(jí)默認(rèn)的全局并發(fā)隊(duì)列,并以異步的方式提交任務(wù)執(zhí)行
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        //下載圖片
        UIImage *image = [[UIImage alloc] initWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1509003055&di=ef9641b620fc103323df445bf796cb13&imgtype=jpg&er=1&src=http%3A%2F%2Fwscont2.apps.microsoft.com%2Fwinstore%2F1x%2Fea9a3c59-bb26-4086-b823-4a4869ffd9f2%2FScreenshot.398115.100000.jpg"]]];
        //獲取主隊(duì)列茂附,在主線程中更新UI督弓,并以異步方式提交任務(wù)
        dispatch_async(dispatch_get_main_queue(), ^{
            self.imageView.image = image;
        });
        
    });

上面的栗子非常簡單营曼,不需要我們手動(dòng)創(chuàng)建線程即可實(shí)現(xiàn)多線程的并發(fā)編程愚隧,如果現(xiàn)在還看不懂沒關(guān)系,學(xué)完本章內(nèi)容你一定會(huì)懂狂塘。

首先看一下GCD為我們提供了哪些創(chuàng)建隊(duì)列或獲取系統(tǒng)隊(duì)列的方法:

//獲取當(dāng)前執(zhí)行該方法的隊(duì)列,被廢棄了睹耐,最好不要使用
dispatch_queue_t dispatch_get_current_queue(void);

/*
獲取主隊(duì)列,即與主線程相關(guān)聯(lián)的隊(duì)列
如果需要提交任務(wù)到主線程使用該方法獲取主線程的主隊(duì)列即可
主隊(duì)列是串行隊(duì)列因?yàn)橹痪S護(hù)主線程一個(gè)線程
*/
dispatch_queue_t dispatch_get_main_queue(void);

/*
獲取一個(gè)全局的并發(fā)隊(duì)列
identifier指定該隊(duì)列的優(yōu)先級(jí)可選值有:
    DISPATCH_QUEUE_PRIORITY_HIGH 2
    DISPATCH_QUEUE_PRIORITY_DEFAULT 0
    DISPATCH_QUEUE_PRIORITY_LOW (-2)
    DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN
flags未用到傳個(gè)0得了
*/
dispatch_queue_t dispatch_get_global_queue(long identifier, unsigned long flags);

/*
創(chuàng)建一個(gè)隊(duì)列
label 隊(duì)列的名稱
attr 隊(duì)列的屬性可選值有:
    DISPATCH_QUEUE_SERIAL 創(chuàng)建一個(gè)串行隊(duì)列
    DISPATCH_QUEUE_CONCURRENT 創(chuàng)建一個(gè)并發(fā)隊(duì)列
通過這種方式可以自己維護(hù)一個(gè)隊(duì)列
*/
dispatch_queue_t dispatch_queue_create(const char *_Nullable label, dispatch_queue_attr_t _Nullable attr);

具體獲取相關(guān)隊(duì)列的方法如下:

//獲取串行主隊(duì)列
dispatch_queue_t mainQueue = dispatch_get_main_queue();

//獲取一個(gè)默認(rèn)優(yōu)先級(jí)的并發(fā)隊(duì)列
dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT);

//自定義創(chuàng)建一個(gè)名稱為myConcurrentQueue的并發(fā)隊(duì)列
dispatch_queue_t myConcurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);

隊(duì)列創(chuàng)建完成以后就可以編寫任務(wù)并提交了响委,接下來將介紹兩種提交執(zhí)行了,接下來介紹兩種執(zhí)行方式赘风,同步執(zhí)行和異步執(zhí)行。

  • 同步執(zhí)行: 阻塞當(dāng)前線程邀窃,直到任務(wù)執(zhí)行完成當(dāng)前線程才可繼續(xù)執(zhí)行
  • 異步執(zhí)行: 不阻塞當(dāng)前線程假哎,可能使用其他線程來執(zhí)行任務(wù)瞬捕,不需要等待任務(wù)完成當(dāng)前線程即可立即繼續(xù)執(zhí)行

關(guān)于同步/異步舵抹,阻塞/非阻塞建議看UNIX網(wǎng)絡(luò)編程 卷一有詳細(xì)的解釋,此處不再贅述了惧蛹。

看一下GCD提交執(zhí)行任務(wù)的具體方法:

/*
以異步方式執(zhí)行任務(wù)刑枝,不阻塞當(dāng)前線程
queue 管理任務(wù)的隊(duì)列,任務(wù)最終交由該隊(duì)列來執(zhí)行
block block形式的任務(wù)装畅,該block返回值、形參都為void
*/
void dispatch_async(dispatch_queue_t queue, dispatch_block_t block);

/*
同上
context 是一個(gè)void*的指針沧烈,作為work的第一個(gè)形參
work 是一個(gè)函數(shù)指針掠兄,指向返回值為void 形參為void*的函數(shù),且形參不能為NULL掺出,也就是說context一定要傳
使用起來不方便徽千,一般不怎么用,需要使用C函數(shù)汤锨,也可以使用OC方法通過傳遞IMP來執(zhí)行但是會(huì)有編譯警告
*/
void dispatch_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式執(zhí)行任務(wù),阻塞當(dāng)前線程闲礼,必須等待任務(wù)完成當(dāng)前線程才可繼續(xù)執(zhí)行
*/
void dispatch_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

//同上
void dispatch_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式提交任務(wù)牍汹,并重復(fù)執(zhí)行iterations次
iterations 迭代執(zhí)行次數(shù)
queue 管理任務(wù)的隊(duì)列,任務(wù)最終交由該隊(duì)列來執(zhí)行
block block形式的任務(wù)柬泽,該block返回值為void形參為iterations迭代次數(shù)
*/
void dispatch_apply(size_t iterations, dispatch_queue_t queue,  DISPATCH_NOESCAPE void (^block)(size_t));

//同上
void dispatch_apply_f(size_t iterations, dispatch_queue_t queue, void *_Nullable context, void (*work)(void *_Nullable, size_t));

/*
以異步方式提交任務(wù)慎菲,在when時(shí)間點(diǎn)提交任務(wù)
queue 管理任務(wù)的隊(duì)列,任務(wù)最終交由該隊(duì)列來執(zhí)行
block block形式的任務(wù)锨并,該block返回值露该、形參都為void
*/
void dispatch_after(dispatch_time_t when, dispatch_queue_t queue, dispatch_block_t block);

//同上
void dispatch_after_f(dispatch_time_t when, dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以異步方式提交任務(wù),會(huì)阻塞queue隊(duì)列第煮,但不阻塞當(dāng)前線程
queue 管理任務(wù)的隊(duì)列解幼,任務(wù)最終交由該隊(duì)列來執(zhí)行
需要說明的是,即時(shí)使用并發(fā)隊(duì)列包警,該隊(duì)列也會(huì)被阻塞撵摆,前一個(gè)任務(wù)執(zhí)行完成才能執(zhí)行下一個(gè)任務(wù)
block block形式的任務(wù),該block返回值害晦、形參都為void
*/
void dispatch_barrier_async(dispatch_queue_t queue, dispatch_block_t block);

//同上
void dispatch_barrier_async_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
以同步方式提交任務(wù)特铝,會(huì)阻塞queue隊(duì)列,也會(huì)阻塞當(dāng)前線程
queue 管理任務(wù)的隊(duì)列壹瘟,任務(wù)最終交由該隊(duì)列來執(zhí)行
同樣的鲫剿,即時(shí)是并發(fā)隊(duì)列該隊(duì)列也會(huì)被阻塞,需要等待前一個(gè)任務(wù)完成稻轨,同時(shí)線程也會(huì)阻塞
block block形式的任務(wù)灵莲,該block返回值、形參都為void
*/
void dispatch_barrier_sync(dispatch_queue_t queue, DISPATCH_NOESCAPE dispatch_block_t block);

//同上
void dispatch_barrier_sync_f(dispatch_queue_t queue, void *_Nullable context, dispatch_function_t work);

/*
底層線程池控制block任務(wù)在整個(gè)應(yīng)用的生命周期內(nèi)只執(zhí)行一次
predicate 實(shí)際為long類型澄者,用于判斷是否執(zhí)行過
block block形式的任務(wù)笆呆,該block返回值、形參都為void
該方法常用于實(shí)現(xiàn)單例類粱挡,以及結(jié)合RunLoop創(chuàng)建一個(gè)常駐內(nèi)存的線程
*/
void dispatch_once(dispatch_once_t *predicate, dispatch_block_t block);

猛的一看常用的方法就有十二種呢赠幕,但是可以發(fā)現(xiàn)每一類方法都提供了block任務(wù)和function任務(wù)兩種形式,所以常用的也就七種询筏,只是對(duì)應(yīng)了block版本和函數(shù)版本榕堰。接下來先介紹最常用的同步執(zhí)行和異步執(zhí)行,其他的方法后文會(huì)講嫌套。

單拎出來同步/異步很好理解逆屡,但是結(jié)合了串行隊(duì)列和并發(fā)隊(duì)列以后情況就有點(diǎn)復(fù)雜了,同步/異步執(zhí)行和串行/并發(fā)隊(duì)列兩兩組合就有四種組合方式踱讨,接下來我們一一查看相關(guān)栗子:

- (void)viewWillAppear:(BOOL)animated
{
    //手動(dòng)創(chuàng)建了一個(gè)并發(fā)隊(duì)列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //也可以獲取全局的并發(fā)隊(duì)列魏蔗,效果一樣
    //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //異步提交一個(gè)任務(wù)到異步隊(duì)列
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    //異步提交一個(gè)任務(wù)到異步隊(duì)列
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    //異步提交一個(gè)任務(wù)到異步隊(duì)列
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //使用傳遞函數(shù)指針的方式有點(diǎn)復(fù)雜,以后的栗子不再贅述
    int context = 0;
    dispatch_async_f(concurrentQueue, &context, cFuncTask);
    //也可以使用OC方法痹筛,傳入IMP莺治,但會(huì)有警告
    //dispatch_async_f(concurrentQueue, &context, [self methodForSelector:@selector(ocFuncTask:)]);
}

//該函數(shù)是C函數(shù)
void cFuncTask(void* context)
{
    for (int i = 0; i < 500; i++)
    {
        NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
    }
}
//OC方法
- (void)ocFuncTask:(void*) context
{
    for (int i = 0; i < 500; i++)
    {
        NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
    }
}

上述代碼輸出的東西比較多,現(xiàn)在的手機(jī)CPU都很強(qiáng)大帚稠,可能一個(gè)時(shí)間片就一次性處理完了谣旁,就看不到并發(fā)的輸出結(jié)果了,所以這里輸出的次數(shù)比較多滋早,最終的結(jié)果就是Task1-4亂序輸出榄审,摘取四個(gè)輸出結(jié)果如下:

Task4 <NSThread: 0x1c04776c0>{number = 7, name = (null)} 88
Task3 <NSThread: 0x1c04770c0>{number = 6, name = (null)} 63
Task1 <NSThread: 0x1c0474980>{number = 4, name = (null)} 99
Task2 <NSThread: 0x1c427c5c0>{number = 5, name = (null)} 0

可以發(fā)現(xiàn)每一個(gè)任務(wù)都是用了不同的線程來執(zhí)行,所以通過異步提交任務(wù)到一個(gè)并發(fā)隊(duì)列是真正實(shí)現(xiàn)了并發(fā)執(zhí)行杆麸。

接下來看一下異步提交到一個(gè)串行隊(duì)列搁进,栗子如下:

- (void)viewWillAppear:(BOOL)animated
{
    //創(chuàng)建一個(gè)串行隊(duì)列
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
    //這里也可以用主隊(duì)列,因?yàn)橹麝?duì)列也是串行隊(duì)列
    //dispatch_queue_t serialQueue = dispatch_get_main_queue();

    //異步提交一個(gè)任務(wù)到串行隊(duì)列
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    //異步提交一個(gè)任務(wù)到串行隊(duì)列
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    //異步提交一個(gè)任務(wù)到串行隊(duì)列
    dispatch_async(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
}

執(zhí)行程序后可以發(fā)現(xiàn)角溃,不論輸出再多次都是按照Task1-3順序輸出拷获,也就是后一個(gè)任務(wù)必須在前一個(gè)任務(wù)完成后才能執(zhí)行,摘取三個(gè)輸出結(jié)果如下:

Task1 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0
Task2 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0
Task3 <NSThread: 0x1c0462280>{number = 4, name = (null)} 0

通過結(jié)果不難發(fā)現(xiàn)三個(gè)任務(wù)使用的是同一個(gè)線程减细,因?yàn)榇嘘?duì)列的底層只維護(hù)一個(gè)線程匆瓜,所以三個(gè)任務(wù)只能使用同一個(gè)線程來執(zhí)行,而且單個(gè)線程的執(zhí)行是串行的未蝌,所以才會(huì)造成上述輸出結(jié)果驮吱,這里使用異步執(zhí)行沒有阻塞當(dāng)前線程。

異步執(zhí)行的栗子實(shí)驗(yàn)完了萧吠,可以發(fā)現(xiàn)左冬,異步執(zhí)行僅僅不會(huì)阻塞當(dāng)前線程,但是否是并發(fā)執(zhí)行需要依靠傳入的隊(duì)列纸型,如果傳遞的是串行隊(duì)列就是串行執(zhí)行拇砰,傳入的是并發(fā)隊(duì)列就是并發(fā)執(zhí)行梅忌,接下來看一下同步執(zhí)行的實(shí)驗(yàn)。

- (void)viewWillAppear:(BOOL)animated
{
    /*
    創(chuàng)建一個(gè)串行隊(duì)列
    這里不可以使用主隊(duì)列了除破,因?yàn)閳?zhí)行該方法的是主線程牧氮,如果使用同步執(zhí)行提交到主隊(duì)列會(huì)造成死鎖,后文會(huì)有具體講解
    */
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);
    
    //同步提交一個(gè)任務(wù)到串行隊(duì)列
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //同步提交一個(gè)任務(wù)到串行隊(duì)列
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //同步提交一個(gè)任務(wù)到串行隊(duì)列
    dispatch_sync(serialQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
}

執(zhí)行程序后可以發(fā)現(xiàn)瑰枫,不論輸出再多次都是按照Task1-3順序輸出踱葛,也就是后一個(gè)任務(wù)必須在前一個(gè)任務(wù)完成后才能執(zhí)行,但這里的順序執(zhí)行和前一個(gè)異步提交到串行隊(duì)列不同光坝,異步提交不會(huì)造成線程阻塞尸诽,所以三個(gè)任務(wù)都被提交到了串行隊(duì)列中,但是由于線程的執(zhí)行是按順序的盯另,所以三個(gè)任務(wù)按次序依次執(zhí)行性含。而這里是使用同步提交到串行隊(duì)列去執(zhí)行任務(wù),當(dāng)?shù)谝粋€(gè)dispatch_sync方法執(zhí)行后會(huì)阻塞當(dāng)前線程鸳惯,必須得等第一個(gè)任務(wù)完成后才能繼續(xù)胶滋,所以這里的執(zhí)行順序是提交第一個(gè)任務(wù)后就開始執(zhí)行而且得等到第一個(gè)任務(wù)完成后再去執(zhí)行第二個(gè)dispatch_sync方法用于提交第二個(gè)任務(wù),以此類推悲敷。雖然結(jié)果是一致的究恤,但執(zhí)行順序是有差別的,需要注意后德,摘取三個(gè)輸出結(jié)果如下:

Task1 <NSThread: 0x1c0072c80>{number = 1, name = main} 0
Task2 <NSThread: 0x1c0072c80>{number = 1, name = main} 0
Task3 <NSThread: 0x1c0072c80>{number = 1, name = main} 0

可以發(fā)現(xiàn)三個(gè)任務(wù)使用了同一個(gè)線程來執(zhí)行部宿,但是這個(gè)線程有點(diǎn)特殊,它是主線程瓢湃,由于viewWillAppear:方法是在主線程中執(zhí)行的理张,所以這里也就直接使用了主線程。

接下來進(jìn)行最后一個(gè)實(shí)驗(yàn)绵患,同步提交到并發(fā)隊(duì)列執(zhí)行:

- (void)viewWillAppear:(BOOL)animated
{
    //手動(dòng)創(chuàng)建了一個(gè)并發(fā)隊(duì)列
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    //也可以獲取全局的并發(fā)隊(duì)列茬射,效果一樣
    //dispatch_queue_t concurrentQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    
    //同步提交一個(gè)任務(wù)到并發(fā)隊(duì)列
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //同步提交一個(gè)任務(wù)到并發(fā)隊(duì)列
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    //同步提交一個(gè)任務(wù)到并發(fā)隊(duì)列
    dispatch_sync(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
}

執(zhí)行程序后可以發(fā)現(xiàn)碑诉,不論輸出多少次都是按Task1-3順序輸出,相信大家應(yīng)該明白是為什么了,因?yàn)橥教峤蛔枞?dāng)前線程屹徘,第一個(gè)dispatch_sync提交的任務(wù)完成以后當(dāng)前線程才能去執(zhí)行第二個(gè)dispatch_sync方法然后執(zhí)行第二個(gè)任務(wù)糊肤。所以资昧,即時(shí)是并發(fā)隊(duì)列居夹,采用同步提交也沒什么卵用了。摘取三個(gè)輸出如下:

Task1 <NSThread: 0x1c0263200>{number = 1, name = main} 0
Task2 <NSThread: 0x1c0263200>{number = 1, name = main} 0
Task3 <NSThread: 0x1c0263200>{number = 1, name = main} 0

從上面的輸出可以看出管行,三個(gè)任務(wù)都是用主線程來執(zhí)行厨埋,按照并發(fā)隊(duì)列的特性,這里的三個(gè)任務(wù)完全可能由不同的三個(gè)線程來執(zhí)行捐顷,但由于viewWillAppear:方法是主線程執(zhí)行的荡陷,而且主線程又被阻塞了雨效,底層可能因此選擇了主線程來執(zhí)行,多運(yùn)行幾次就會(huì)發(fā)現(xiàn)也有可能使用其他線程來執(zhí)行废赞。

到此為止设易,四個(gè)實(shí)驗(yàn)都結(jié)束了,沒有單獨(dú)把主隊(duì)列拿出來做實(shí)驗(yàn)蛹头,因?yàn)橹麝?duì)列本質(zhì)還是一個(gè)串行隊(duì)列,其實(shí)驗(yàn)結(jié)果和串行隊(duì)列是一樣的戏溺。通過四組實(shí)驗(yàn)不難發(fā)現(xiàn)渣蜗,想要實(shí)現(xiàn)并發(fā)只能通過異步提交到并發(fā)隊(duì)列來執(zhí)行任務(wù),實(shí)驗(yàn)分析如下表:

type Serial串行隊(duì)列 Concurrent并發(fā)隊(duì)列
async異步執(zhí)行 不阻塞當(dāng)前線程旷祸,使用其他線程串行執(zhí)行任務(wù)耕拷,只有一個(gè)線程用于執(zhí)行任務(wù) 不阻塞當(dāng)前線程,并發(fā)執(zhí)行任務(wù)托享,使用多個(gè)線程執(zhí)行任務(wù)
sync同步執(zhí)行 阻塞當(dāng)前線程骚烧,使用同一線程串行執(zhí)行任務(wù),只有一個(gè)線程用于執(zhí)行任務(wù) 阻塞當(dāng)前線程闰围,可能使用同一線程串行執(zhí)行任務(wù)

所以赃绊,針對(duì)異步執(zhí)行/同步執(zhí)行和串行隊(duì)列/并發(fā)隊(duì)列,只需要掌握其關(guān)鍵就可以了羡榴,同步/異步的區(qū)別在于是否阻塞線程碧查,串行/并發(fā)隊(duì)列的區(qū)別在于有多少個(gè)線程參與執(zhí)行任務(wù)。即時(shí)存在嵌套結(jié)構(gòu)也能夠很好理解了校仑。舉一個(gè)嵌套結(jié)構(gòu)的例子:

- (void)viewWillAppear:(BOOL)animated
{

    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    dispatch_queue_t serialQueue = dispatch_queue_create("mySerialQueue", DISPATCH_QUEUE_SERIAL);

    dispatch_sync(serialQueue, ^{
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 500; i++)
            {
                NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
            }
        });
        
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 500; i++)
            {
                NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
            }
        });
        
        dispatch_async(concurrentQueue, ^{
            for (int i = 0; i < 500; i++)
            {
                NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
            }
        });
        
        for (int i = 0; i < 100; i++)
        {
            NSLog(@"Complete.");
        }
    });    
}

外層dispatch不論使用串行隊(duì)列還是并發(fā)隊(duì)列忠售,由于只有一個(gè)任務(wù),只會(huì)有一個(gè)線程來執(zhí)行這個(gè)block塊的內(nèi)容迄沫,而同步和異步的區(qū)別就在于是否會(huì)阻塞當(dāng)前線程稻扬,接下來看block塊的內(nèi)容,采用了三個(gè)異步提交到并發(fā)隊(duì)列羊瘩,所以并發(fā)隊(duì)列里就有了三個(gè)不同的任務(wù)泰佳,就可以真正執(zhí)行并發(fā),由于都是異步提交沒有阻塞當(dāng)前線程尘吗,所以輸出Complete的代碼也會(huì)摻雜在Task1-3中亂序輸出乐纸。

dispatch_apply

通過上面的講解就已經(jīng)很清楚的了解了GCD同步/異步提交到串行/并發(fā)隊(duì)列的執(zhí)行過程了。接下來再繼續(xù)講解幾個(gè)常用的方法摇予,再舉個(gè)栗子:

- (void)viewWillAppear:(BOOL)animated
{
    //執(zhí)行該方法的是主線程汽绢,不能傳入主隊(duì)列否則會(huì)死鎖,后文會(huì)講解
    dispatch_apply(20000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
        NSLog(@"Task %@ %ld", [NSThread currentThread], t);
    });
}

dispatch_apply方法是以同步方式提交執(zhí)行任務(wù)侧戴,這里傳入了一個(gè)全局的并發(fā)隊(duì)列宁昭,因此講道理重復(fù)執(zhí)行任務(wù)時(shí)就應(yīng)該有多個(gè)線程并發(fā)執(zhí)行跌宛,但是不管我迭代多少次運(yùn)行多少次都只有一個(gè)輸出是其他線程輸出的,剩余的都是同一個(gè)線程輸出积仗,有懂的讀者可以留言講解一下疆拘。如果傳入的是串行隊(duì)列,那么迭代就是按照順序依次執(zhí)行寂曹。

dispatch_after

再看一個(gè)栗子:

- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"Before");
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, NSEC_PER_SEC * 5), dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"In %@", [NSThread currentThread]);
    });
    NSLog(@"After");
}

dispatch_after是在when時(shí)間點(diǎn)異步提交任務(wù)哎迄,所以不會(huì)阻塞當(dāng)前線程,這里設(shè)置的時(shí)間點(diǎn)是當(dāng)前時(shí)間的5s后隆圆,觀察輸出:

2017-10-19 17:44:19.274305+0800 StudyOCTest[25385:13635522] Before
2017-10-19 17:44:19.274365+0800 StudyOCTest[25385:13635522] After
2017-10-19 17:44:24.745273+0800 StudyOCTest[25385:13635542] In <NSThread: 0x1c027ae40>{number = 3, name = (null)}

可以看出這個(gè)5s并不是精確的5s漱挚,因?yàn)樵摲椒ㄊ窃?code>when時(shí)間點(diǎn)到達(dá)的時(shí)候去提交任務(wù)到隊(duì)列,所以是延遲提交渺氧,而不是延遲執(zhí)行旨涝,隊(duì)列什么時(shí)候安排線程去執(zhí)行是未知的,所以不要用這個(gè)方法去實(shí)現(xiàn)定時(shí)器這樣的功能侣背。

dispatch_barrier _ (a)sync

該方法用于阻塞隊(duì)列白华,舉個(gè)栗子如下:

- (void)viewWillAppear:(BOOL)animated
{
    dispatch_queue_t concurrentQueue = dispatch_queue_create("myConcurrentQueue", DISPATCH_QUEUE_CONCURRENT);
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task0 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_barrier_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_async(concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task4 %@ %d", [NSThread currentThread], i);
        }
    });
}

上面的輸出是按照Task0 Task1并發(fā)執(zhí)行,Task2等待Task0 Task1執(zhí)行完成后單獨(dú)執(zhí)行贩耐, 最后Task3 Task4等待Task2執(zhí)行完成后開始并發(fā)執(zhí)行 弧腥。

這里需要講解一下阻塞隊(duì)列的概念,前文講過不論是并發(fā)隊(duì)列還是串行隊(duì)列都是使用FIFO 先進(jìn)先出的方式管理的潮太,隊(duì)列會(huì)從隊(duì)首獲取要執(zhí)行的任務(wù)并交由對(duì)應(yīng)線程處理鸟赫,串行隊(duì)列只有一個(gè)線程所以是順序執(zhí)行,并發(fā)隊(duì)列有多個(gè)線程消别,但獲取任務(wù)依舊是FIFO按順序獲取抛蚤,只是執(zhí)行時(shí)有多個(gè)線程。阻塞線程即寻狂,獲取一個(gè)任務(wù)后岁经,這個(gè)任務(wù)必須要執(zhí)行完成才能獲取下一個(gè)任務(wù),所以不管是并發(fā)還是串行隊(duì)列蛇券,都得等前一個(gè)任務(wù)完成了才能從隊(duì)列中獲取下一個(gè)任務(wù)缀壤,這樣就不難理解輸出結(jié)果了,上述栗子改成串行隊(duì)列結(jié)果也是一樣的纠亚,如果使用同步提交效果也是一樣的塘慕,讀者可以自行嘗試,篇幅問題不再贅述了蒂胞。

dispatch_barrier_async方法常與并發(fā)隊(duì)列共用图呢,前一段任務(wù)使用dispatch_async異步并發(fā)執(zhí)行,然后插入一個(gè)dispatch_barrier_async執(zhí)行一個(gè)中間任務(wù),這個(gè)中間任務(wù)必須要等待前面的并發(fā)任務(wù)執(zhí)行完成后才能開始執(zhí)行蛤织,接著這個(gè)中間任務(wù)完成后赴叹,繼續(xù)異步并發(fā)執(zhí)行接下來的任務(wù)。

dispatch_once

該方法能夠保證在應(yīng)用的生命周期內(nèi)只執(zhí)行一次提交的任務(wù)指蚜,所以常用于單例類的創(chuàng)建乞巧,舉個(gè)單例類的栗子如下:

@interface MyUtil: NSObject <NSCopying>

+ (instancetype)sharedUtil;

@end

@implementation MyUtil

static MyUtil *staticMyUtil = nil;

+ (instancetype)sharedUtil
{
    //保證初始化創(chuàng)建只執(zhí)行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        staticMyUtil = [[MyUtil alloc] init];
    });
    return staticMyUtil;
}

//防止通過alloc或new直接創(chuàng)建對(duì)象
+ (instancetype)allocWithZone:(struct _NSZone *)zone
{
    //保證alloc函數(shù)只執(zhí)行一次
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        staticMyUtil = [super allocWithZone:zone];
    });
    return staticMyUtil;
}

//實(shí)現(xiàn)NSCopying協(xié)議的方法,防止通過copy獲取副本對(duì)象
- (instancetype)copyWithZone:(NSZone *)zone
{
    return staticMyUtil;
}

@end

dispatch_once函數(shù)需要傳入一個(gè)long類型的predicate摊鸡,這個(gè)值必須是獨(dú)一無二的绽媒,使用靜態(tài)變量的地址最合適不過了,MyUtil實(shí)現(xiàn)了NSCopying協(xié)議的copyWithZone:方法免猾,防止通過copy方法獲取副本對(duì)象是辕。

當(dāng)使用alloc&&init方法初始化時(shí),先調(diào)用allocWithZone:方法來分配存儲(chǔ)空間掸刊,如果再次使用sharedUtil方法來獲取的話,由于沒有執(zhí)行過赢乓,會(huì)執(zhí)行到dispatch_once內(nèi)部block忧侧,此時(shí)會(huì)再去執(zhí)行allocWithZone:方法,但該方法內(nèi)部dispatch_once已經(jīng)執(zhí)行過了會(huì)直接返回staticMyUtil牌芋,反過來調(diào)用是一樣的道理蚓炬,通過這樣的方式就可以實(shí)現(xiàn)真正的單例了。

dispatch_ group_ t

dispatch_group_t是一個(gè)比較實(shí)用的方法躺屁,通過構(gòu)造一個(gè)組的形式肯夏,將各個(gè)同步或異步提交任務(wù)都加入到同一個(gè)組中,當(dāng)所有任務(wù)都完成后會(huì)收到通知犀暑,用于進(jìn)一步處理驯击,通過這樣的方式就可以實(shí)現(xiàn)多線程下載,當(dāng)下載完成后就可以通知用戶了耐亏,舉個(gè)簡單的栗子如下:

- (void)viewWillAppear:(BOOL)animated
{
    dispatch_group_t group = dispatch_group_create();
    
    dispatch_group_async(group, concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task1 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_group_async(group, dispatch_get_main_queue(), ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task2 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_group_async(group, concurrentQueue, ^{
        for (int i = 0; i < 500; i++)
        {
            NSLog(@"Task3 %@ %d", [NSThread currentThread], i);
        }
    });
    
    dispatch_group_notify(group, concurrentQueue, ^{
        NSLog(@"All Task Complete");
    });
}

像一個(gè)組中添加了三個(gè)異步的任務(wù)徊都,最終三個(gè)任務(wù)完成后可以收到通知執(zhí)行回調(diào)的block,上面的輸出為广辰,All Task Complete在前面三個(gè)輸出都結(jié)束后才會(huì)輸出暇矫。

防止GCD產(chǎn)生死鎖

接下來將講解一下GCD使用時(shí)可能會(huì)產(chǎn)生死鎖的情況,首先舉一個(gè)比較簡單的栗子:

- (void)viewWillAppear:(BOOL)animated
{
    NSLog(@"Before");
    
    dispatch_sync(dispatch_get_main_queue(), ^{
        NSLog(@"In");
    });
    
    NSLog(@"After");
}

上述代碼就會(huì)產(chǎn)生死鎖择吊,分析下原因李根,首先,viewWillAppear:方法是在主線程中執(zhí)行的几睛,接著調(diào)用dispatch_sync方法房轿,該方法會(huì)阻塞當(dāng)前線程,也就是會(huì)阻塞主線程,主線程被阻塞是為了等待任務(wù)的完成冀续,然后該代碼將任務(wù)添加到了主隊(duì)列琼讽,主隊(duì)列會(huì)將任務(wù)交給主線程執(zhí)行,但此時(shí)主線程阻塞了洪唐,任務(wù)添加進(jìn)了主線程得不到運(yùn)行钻蹬,而主線程在等待任務(wù)的執(zhí)行,因此就造成了死鎖凭需。

這個(gè)栗子一般人寫不出來這樣的代碼问欠,僅僅是為了講解什么情況下會(huì)造成死鎖,即粒蜈,線程被阻塞需要等到任務(wù)執(zhí)行完成顺献,而任務(wù)由于線程阻塞得不到執(zhí)行。前文舉的幾個(gè)串行隊(duì)列的栗子很多是不能使用主隊(duì)列的枯怖,原因也正在此注整。

再舉個(gè)栗子:

dispatch_apply(1, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
    dispatch_apply(2000, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^(size_t t) {
        NSLog(@"===== %@ %ld", [NSThread currentThread], t);
    });
 }); 

上述栗子也會(huì)造成死鎖,因?yàn)椋?code>dispatch_apply同樣會(huì)阻塞當(dāng)前線程度硝,它需要等待內(nèi)部的dispatch_apply執(zhí)行完成肿轨,內(nèi)部的需要等待外部的線程來執(zhí)行它,產(chǎn)生了死鎖蕊程。

可以看出死鎖產(chǎn)生的條件一般都發(fā)生在同步執(zhí)行方法中椒袍,所以,在使用同步執(zhí)行方法時(shí)要避免任務(wù)再次派發(fā)到同一個(gè)線程中藻茂。

實(shí)現(xiàn)定時(shí)器的三種方法

定時(shí)器在開發(fā)中是比較常見的需求驹暑,常用的其實(shí)有三種方法:NSTimerGCD以及CADisplayLink辨赐,CADisplayLink是其中精度最高的优俘,因?yàn)樗噲D與屏幕刷新率保持一致,由于涉及的內(nèi)容比較多本小結(jié)只介紹基本使用方法掀序,直接看栗子:

- (void)viewWillAppear:(BOOL)animated
{
    //倒計(jì)時(shí)次數(shù)
    __block int count = 10;
    //間隔1s執(zhí)行一次
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        //如果還在倒計(jì)時(shí)次數(shù)內(nèi)
        if (count > 0)
        {
            //執(zhí)行相關(guān)工作兼吓,如果有UI更新的操作需要放到主線程
            dispatch_async(dispatch_get_main_queue(), ^{
        
            });
            //次數(shù)--
            count --;
        }
        else
        {   
            //次數(shù)到達(dá),取消定時(shí)器
            [timer invalidate];
        }
    }];
    //加入到RunLoop中森枪,使用NSRunLoopCommonModes在滑動(dòng)時(shí)也可以繼續(xù)執(zhí)行
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

上面的栗子比較簡單视搏,使用了block的形式實(shí)現(xiàn),也可以使用方法的形式執(zhí)行:

- (void)viewWillAppear:(BOOL)animated
{
    self.count = 10;
    
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate date] interval:1 target:self selector:@selector(countDown:) userInfo:nil repeats:YES];
    [[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

- (void)countDown:(NSTimer*)timer
{
    if (self.count > 0)
    {
        //執(zhí)行相關(guān)工作县袱,如果有UI更新的操作需要放到主線程
        dispatch_async(dispatch_get_main_queue(), ^{
        
        });
        self.count --;
    }
    else
    {
        //取消定時(shí)器
        [timer invalidate];
    }
}

以上比較重要的就是引用循環(huán)的問題浑娜,創(chuàng)建NSTimer傳入的target對(duì)象,NSTimer會(huì)持有強(qiáng)引用式散,所以在重復(fù)執(zhí)行NSTimer時(shí)一定要在任務(wù)結(jié)束后調(diào)用invalidate方法取消定時(shí)器打破引用循環(huán)筋遭,如果只執(zhí)行一次可以不需要。

接下來看一下GCD如何實(shí)現(xiàn):

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:YES];
    //執(zhí)行次數(shù)
    __block int count = 10;
    //獲取一個(gè)全局并發(fā)隊(duì)列
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    //這里不能使用局部變量,因?yàn)楫?dāng)viewDidAppear函數(shù)返回后timer就會(huì)被釋放
    _timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //設(shè)置timer的執(zhí)行時(shí)間和間隔時(shí)間漓滔,設(shè)置每秒執(zhí)行一次
    dispatch_source_set_timer(_timer, dispatch_time(DISPATCH_TIME_NOW, 0), 1 * NSEC_PER_SEC, 0); 
    //設(shè)置timer執(zhí)行事件的block塊
    dispatch_source_set_event_handler(_timer, ^{
        if (count > 0)
        {
            //要執(zhí)行的任務(wù)编饺,更新UI需要放到主線程
            dispatch_async(dispatch_get_main_queue(), ^{
                
            });
            count --;
        }
        else
        {
            //執(zhí)行次數(shù)達(dá)到預(yù)期就取消timer
            dispatch_source_cancel(_timer);
        }
    });
    //啟動(dòng)timer
    dispatch_resume(_timer);
}

上面代碼比較簡單,也可以傳入一個(gè)C函數(shù)而不使用塊响驴,具體不再贅述了透且,有興趣的讀者可以自行實(shí)驗(yàn)。

最后看一下CADisplayLink的栗子:

- (void)viewDidAppear:(BOOL)animated
{
    [super viewDidAppear:YES];
    
    self.count = 10;
    //CADisplayLink只有這一個(gè)構(gòu)造方法
    CADisplayLink *timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(countDown:)];
    //每秒對(duì)多少幀感興趣豁鲤,也就是每秒要執(zhí)行多少次回調(diào)方法
    timer.preferredFramesPerSecond = 1;
    //必須要添加進(jìn)RunLoop才開始執(zhí)行
    [timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSRunLoopCommonModes];

}

- (void)countDown:(CADisplayLink*)timer
{
    if (self.count > 0)
    {
        //執(zhí)行相關(guān)工作秽誊,如果有UI更新的操作需要放到主線程
        dispatch_async(dispatch_get_main_queue(), ^{
            
        });
        self.count --;
    }
    else
    {
        //取消定時(shí)器
        [timer invalidate];
    }
}

上面栗子不再贅述了,具體細(xì)節(jié)可以自行查閱琳骡。

備注

由于作者水平有限锅论,難免出現(xiàn)紕漏,如有問題還請(qǐng)不吝賜教楣号。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末最易,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子炫狱,更是在濱河造成了極大的恐慌藻懒,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,657評(píng)論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件毕荐,死亡現(xiàn)場(chǎng)離奇詭異束析,居然都是意外死亡艳馒,警方通過查閱死者的電腦和手機(jī)憎亚,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評(píng)論 3 394
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來弄慰,“玉大人第美,你說我怎么就攤上這事÷剿” “怎么了什往?”我有些...
    開封第一講書人閱讀 164,057評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長慌闭。 經(jīng)常有香客問我别威,道長,這世上最難降的妖魔是什么驴剔? 我笑而不...
    開封第一講書人閱讀 58,509評(píng)論 1 293
  • 正文 為了忘掉前任省古,我火速辦了婚禮,結(jié)果婚禮上丧失,老公的妹妹穿的比我還像新娘豺妓。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評(píng)論 6 392
  • 文/花漫 我一把揭開白布琳拭。 她就那樣靜靜地躺著训堆,像睡著了一般。 火紅的嫁衣襯著肌膚如雪白嘁。 梳的紋絲不亂的頭發(fā)上坑鱼,一...
    開封第一講書人閱讀 51,443評(píng)論 1 302
  • 那天,我揣著相機(jī)與錄音权薯,去河邊找鬼姑躲。 笑死,一個(gè)胖子當(dāng)著我的面吹牛盟蚣,可吹牛的內(nèi)容都是我干的黍析。 我是一名探鬼主播,決...
    沈念sama閱讀 40,251評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼屎开,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼阐枣!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起奄抽,我...
    開封第一講書人閱讀 39,129評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤蔼两,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后逞度,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體额划,經(jīng)...
    沈念sama閱讀 45,561評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評(píng)論 3 335
  • 正文 我和宋清朗相戀三年档泽,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俊戳。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,902評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡馆匿,死狀恐怖抑胎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情渐北,我是刑警寧澤阿逃,帶...
    沈念sama閱讀 35,621評(píng)論 5 345
  • 正文 年R本政府宣布,位于F島的核電站赃蛛,受9級(jí)特大地震影響恃锉,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜呕臂,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評(píng)論 3 328
  • 文/蒙蒙 一破托、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧诵闭,春花似錦炼团、人聲如沸澎嚣。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽易桃。三九已至,卻和暖如春锌俱,著一層夾襖步出監(jiān)牢的瞬間晤郑,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評(píng)論 1 269
  • 我被黑心中介騙來泰國打工贸宏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留造寝,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,025評(píng)論 2 370
  • 正文 我出身青樓吭练,卻偏偏與公主長得像诫龙,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子鲫咽,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評(píng)論 2 354

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