拨开GCD的面纱

之前我在博客中也说过了,关于GCD和block是很多新手的两大拦路虎,下面谈谈GCD。

GCD是iOS中异步执行任务的技术之一,他可以生成必要的线程并计划执行任务,这样就可以通过管理线程的方式来实现操作,会比较有效率。

先来写个最基本的GCD的用法

    dispatch_async(queue, ^{
        //长时间处理的东西,比如数据的访问等
       dispatch_async(dispatch_get_main_queue(), ^{
          //只在主线程可以执行的处理
       });
    });

对上面代码不清楚的可以去看我这篇博客,最常用基本的:http://www.jianshu.com/p/c25bc3740848

上面的就是在后台线程中执行长时间处理,处理结束时,主线程使用该处理结果结果的源代码,而queue就是队列,而我们开发者要做的就是定义想执行的任务并追加到适当的Dispatch Queue 中,比如下面的代码:

dispatch_async(queue, ^{
     //想执行的任务
    });

该源代码使用Blcok语法"定义想执行的任务",通过dispatch_async函数“追加”赋值在变量queue的“Dispatch Queue”中。仅这样就可使指定的Block在另一线程中执行。

Dispatch Queue
“Dispatch Queue”是什么呢?顾名思义,是执行处理的等待队列。我们通过dispatch_async函数等API,在Block语法中记述想执行的处理并将其追加到Dispatch Queue中。Dispatch Queue按照追加的顺序(先进先出FIFO)执行处理。

而在执行处理时候存在两种Dispatch Queue,一种是等待现在执行中处理的Serial Dispatch Queue,另一种是不等待现在执行中处理的Concurrent Dispatch Queue (你可以理解前者是串行,后者可并行)

举个例子:

dispatch_async(queue, blk0);
dispatch_async(queue, blk1);
dispatch_async(queue, blk2);
dispatch_async(queue, blk3);

当queue为Serial Dispatch Queue(也就是串行),执行的顺序为:blk0,blk1,blk2,blk3,并分别等上一步完成再执行下一步。
当queue为Concurrent Dispatch Queue(可以理解为并行),执行顺序为:blk0,blk1,blk2,blk3,但是当执行第一个的时候,他不会等待是否执行完成,就会立马执行第二个,如此重复。

看到这里有的童鞋是不是有点混了呢?其实吧,上面说的两种(Serial Dispatch Queue,Concurrent Dispatch Queue)都是Dispatch Queue,我们现在回到这个问题上,如何才能得到Dispatch Queue(也就是上面的Serial Dispatch Queue,Concurrent Dispatch Queue),得不到说了那么多也是屁话。

第一种方法是通过GCD的API生成Dispatch Queue。
通过dispatch_queue_create函数可生成Dispatch Queue,下面的代码生成了Serial Dispatch Queue

dispatch_queue_t mySerialDispath = dispatch_queue_create("mySerialDispath", NULL);

生成Serial Dispatch Queue时,像该源代码这样,将第二个参数指定为NULL。而生成Concurrent Dispatch Queue时,代码如下,指定为DISPATCH_QUEUE_CONCURRENT

dispatch_queue_t myConcurrentDispatch = dispatch_queue_create("myConcurrentDispatch", DISPATCH_QUEUE_CONCURRENT);

在这里dispatch_queue_create函数的返回值为表示Dispatch Queue的“dispatch_queue_t类型”。之前的代码中所出现的变量queue均为dispatch_queue_t类型变量。
在这里要说明一个问题,如果你部署的最低目标低于 iOS 6.0 or Mac OS X 10.8,那么需要通过release方法去释放Dispatch Queue,如下

dispatch_release(mySerialDispath)

当然了,现在一般都不用我们自己手动去释放了,系统一般也没那么低了

第二种方法是获取系统标准提供的Dispatch Queue。
实际上不用特意生成Dispatch Queue系统也会给我们提供几个。那就是Main Dispatch Queue和Global Dispatch Queue。
Main Dispatch Queue正如其名称中含有的“Main”一样,是在主线程中执行的Dispatch Queue。因为主线程只有1个,所以Main Dispatch Queue自然就是Serial Dispatch Queue,追加到Main Dispatch Queue的处理在主线程的RunLoop中执行,由于在主线程中执行,因此要将用户界面的界面更新等一些必须在主线程中执行的处理追加到Main Dispatch Queue使用。
另一个Global Dispatch Queue是所有应用程序都能够使用的Concurrent Dispatch Queue。没有必要再通过dispatch_queue_create函数逐个生成Concurrent Dispatch Queue,只要获取Global Dispatch Queue使用即可。
另外,Global Dispatch Queue有4个优先级:High Priority,Default Priority,Low Priority,Background Priority(后台优先级)。
下面举个例子来说明一下上面提到的自己创建和系统提供的Dispatch Queue。

1.Serial Dispatch Queue:

在viewdidload中运行下面代码

    dispatch_queue_t mySerialDispathOne = dispatch_queue_create("mySerialDispath", NULL);
    dispatch_async(mySerialDispathOne, ^{
        NSLog(@"1111111");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第一个主线程调用");
        });
    });
    
    dispatch_queue_t mySerialDispathTwo = dispatch_queue_create("mySerialDispath", NULL);
    dispatch_async(mySerialDispathTwo, ^{
        NSLog(@"2222222");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第二个主线程调用");
        });
    });
    
    dispatch_queue_t mySerialDispathThree = dispatch_queue_create("mySerialDispath", NULL);
    dispatch_async(mySerialDispathThree, ^{
        NSLog(@"33333333");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第三个主线程调用");
        });
    });

我运行了一次,打印如下:

Paste_Image.png

在这里我创建了三个串行的,但是都是不同队列的,然后打印出来却是随机的,说明多个Serial Dispatch Queue可并行执行。然后问题来了,如果我们创建了多个串行的,就会导致大量消耗内存,降低系统的响应功能。

2.Concurrent Dispatch Queue

    dispatch_queue_t myConcurrentDispatch = dispatch_queue_create("myConcurrentDispatch", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(myConcurrentDispatch, ^{
        NSLog(@"11111");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第一个主线程调用");
        });
    });
    
    dispatch_queue_t myConcurrentDispatchTwo = dispatch_queue_create("myConcurrentDispatch", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(myConcurrentDispatchTwo, ^{
        NSLog(@"222222");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第二个主线程调用");
        });
    });
    
    
    dispatch_queue_t myConcurrentDispatchThree = dispatch_queue_create("myConcurrentDispatch", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(myConcurrentDispatchThree, ^{
        NSLog(@"333333");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第三个主线程调用");
        });
    });

运行后,结果也是随机的。

3.Global Dispatch Queue

代码如下:

    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
        NSLog(@"低级顺序");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"low");
        });
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
        NSLog(@"高级顺序");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"high");
        });
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        NSLog(@"默认顺序");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"default");
        });
    });
    dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
        NSLog(@"后台顺序");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"background");
        });
    });

注意一下:优先级只是表示他们执行的优先级,DISPATCH_QUEUE_PRIORITY_HIGH表示优先级最高,最先执行,但并不代表他第一个执行完。

4.dispatch_after

用dispatch_after可以实现延迟执行的情况

    NSLog(@"1234");
    dispatch_time_t time = dispatch_time(DISPATCH_TIME_NOW, 5ull * NSEC_PER_SEC);
    dispatch_after(time, dispatch_get_main_queue(), ^{
        NSLog(@"waiter at least three seconds");
    });

5.Dispatch Group

说这个函数之前,我们先来看个例子,多个Concurrent Dispatch Queue运行,然后最后在主线程中也运行一条代码。

    dispatch_queue_t myConcurrentDispatch = dispatch_queue_create("myConcurrentDispatch", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(myConcurrentDispatch, ^{
        NSLog(@"11111");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第一个主线程调用");
        });
    });
    
    dispatch_queue_t myConcurrentDispatchTwo = dispatch_queue_create("myConcurrentDispatch", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(myConcurrentDispatchTwo, ^{
        NSLog(@"222222");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第二个主线程调用");
        });
    });
    
    
    dispatch_queue_t myConcurrentDispatchThree = dispatch_queue_create("myConcurrentDispatch", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(myConcurrentDispatchThree, ^{
        NSLog(@"333333");
        dispatch_async(dispatch_get_main_queue(), ^{
            NSLog(@"第三个主线程调用");
        });
    });
    
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"这个啥时候出现呢");
    });

运行结果如下:


Paste_Image.png

我们发现最后一个写的主线程中的,并不是在最后执行 (当然了,如果用Serial Dispatch Queue是可以串行一个一个等待完成的)
看如下代码:

    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        NSLog(@"1111111");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"2222222");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"33333333");
    });
    
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"这个肯定是最后输出");
    });

运行的结果必然是最后一个输出,这就是使用Dispatch Group的原因。
在追加到Dispatch Group中的处理全部执行结束时,该源代码中使用的dispatch_group_notify函数将执行的Block追加到Dispatch Group中,将第一个参数指定为要监视的Dispatch Group。在追加到该Dispatch Group的全部处理执行完毕后,将第三个参数的Block追加到第二个参数的Dispatch Queue中。在dispatch_group_notify函数中不管指定什么样的Dispatch Queue,属于Dispatch Group的全部处理在追加指定的Block时都已执行完毕。

另外,在Dispatch Group中也可以使用dispatch_group_wait函数仅等待全部处理执行结束。

dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_group_t group = dispatch_group_create();
    dispatch_group_async(group, queue, ^{
        NSLog(@"1111111");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"2222222");
    });
    dispatch_group_async(group, queue, ^{
        NSLog(@"33333333");
    });
    dispatch_group_wait(group, DISPATCH_TIME_FOREVER); //等待
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"这个肯定最后执行");
    });

dispatch_group_wait函数的第二个参数指定为等待的时间(超时)。它属于dispatch_queue_t类型的值。该源代码使用DISPATCH_TIME_FOREVER,意味着永久等待。只要属于Dispatch Group的处理执行未结束,就会一直等待,中途不能取消。
指定DISPATCH_TIME_NOW,则不用等待即可判定Dispatch Group的处理是否执行结束。

dispatch_group_wait(group, DISPATCH_TIME_NOW);

这样的话,在主线程的RunLoop的每次循环中,可检查执行是否结束 ,从而不耗费多余的等待时间,虽然这样也可以,但一般在这种情形下,还是推荐用dispatch_group_notify函数追加结束处理到Main Dispatch Queue中,这是因为dispatch_group_notify函数可以简化源代码。

6.dispatch_barrier_async

在访问数据库或者文件时候,向之前说的,使用Serial Dispatch Queue可避免数据竞争的问题。
写入处理确实不可与其他的写入处理以及包含读取处理的其他某些处理并行执行。但是如果读取处理只是与读取处理并行执行,那么多个并行执行就不会发生问题。
也就是说,为了高效率地进行访问,读取处理追加到Concurrent Dispatch Queue中,写入处理在任一个读取处理没有执行的状态下(在写入处理结束之前,读取处理不可执行)。
在这里引出dispatch_barrier_async函数,它可以起到中间人的身份,让之前的任务处理完,先处理dispatch_barrier_async函数中的任务,再执行后面的。

 //创建一个txt文件
    NSArray *paths=NSSearchPathForDirectoriesInDomains(NSDocumentDirectory,NSUserDomainMask,YES);
    NSString *path=[paths    objectAtIndex:0];
    NSLog(@"path = %@",path);
    NSString *filename=[path stringByAppendingPathComponent:@"junjieTest.txt"];
    NSFileManager* fm = [NSFileManager defaultManager];
    [fm createFileAtPath:filename contents:nil attributes:nil];
    
    //创建一个文字什么的,写到txt文件里
    NSString *str = @"111111";
    [str writeToFile:filename atomically:YES encoding:NSUTF8StringEncoding error:nil];
    NSString *myStr = [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil];
    NSLog(@"myStr == %@", myStr);
    
    dispatch_queue_t queue = dispatch_queue_create("concurrent", DISPATCH_QUEUE_CONCURRENT);
    dispatch_async(queue, ^{
        NSLog(@"one == %@", [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil]);
    });
    dispatch_async(queue, ^{
        NSLog(@"two == %@", [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil]);
    });
    dispatch_async(queue, ^{
        NSLog(@"three = %@", [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil]);
    });
    dispatch_async(queue, ^{
        NSLog(@"four == %@", [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil]);
    });
    //中间人
    dispatch_barrier_async(queue, ^{
        [@"333333" writeToFile:filename atomically:YES encoding:NSUTF8StringEncoding error:nil];
        NSLog(@"tttttttttt == %@",[NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil] );
    });
    
    //写在后面执行的
    dispatch_async(queue, ^{
        NSLog(@"five == %@", [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"six == %@", [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"seven == %@", [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil]);
    });
    
    dispatch_async(queue, ^{
        NSLog(@"eight == %@", [NSString stringWithContentsOfFile:filename encoding:NSUTF8StringEncoding error:nil]);
    });

打印结果如下:


Paste_Image.png

dispatch_barrier_async函数可以实现高效率的数据库访问和文件访问。

7.dispatch_sync

dispatch_sync函数,意味着同步,也就是将制定的block同步追加到指定的Dispatch Queue中,在追加Block结束之前,dispatch_sync函数会一直等待,也可以说是简易版的dispatch_group_wait函数。
正因为dispatch_sync函数简单,所以容易造成死锁。
如下:

dispatch_sync(dispatch_get_main_queue(), ^{
         NSLog(@"dsggds");
     });

该源代码在Main Dispatch Queue即主线程中执行指定的block,并等待其执行结束。而其实主线程中正在执行这些源代码,所以无法追加到Main Dispatch Queue的block。

8.dispatch_apply

该函数按指定的次数将指定的block追加到指定的Dispatch Queue中,并等待全部处理执行结束。

    dispatch_apply(10, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t index) {
        NSLog(@"index == %lu", index);
    });

打印结果如下:


Paste_Image.png

第一个参数为重复次数,第二个参数为追加对象的Dispatch Queue,第三个参数为追加的处理,第三个参数的block为带有参数的block,这是为了按第一个参数重复追加到block并区分各个block而使用。例如要对NSArray类对象的所有元素执行处理时,不必一个一个编写for循环部分。如下:

    NSArray *arr = @[@"1", @"2",@"3",@"4",@"5"];
    dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    dispatch_apply([arr count], queue, ^(size_t index) {
        NSLog(@"%lu == %@", index, [arr objectAtIndex:index]);
    });

另外,dipatch_apply函数也与dispatch_sync函数相同,会等待处理执行结束,因此推荐在dispatch_async函数中非同步地执行dipatch_apply函数。

9.dispatch_suspend / dispatch_resume

当追加大量处理到Dispatch Queue时,在追加处理的过程中,有时希望不执行已追加的处理。
在这种情况下,只要挂起Dispatch Queue即可。当可以执行时再恢复。
dispatch_suspend函数挂起指定的Dispatch Queue。

dispatch_suspend(queue);

dispatch_resume函数恢复指定的Dispatch Queue。

dispatch_resume(queue);

这些函数对已经执行的处理没有影响。挂起后,追加到Dispatch Queue中但尚未执行的处理在此之后停止执行。而恢复则使得这些处理能够继续执行。代码如下:

    dispatch_queue_t serialQueue = dispatch_queue_create("serialQueue", NULL);
    dispatch_async(serialQueue, ^{
            for (int j = 0; j < 10; j++) {
                NSLog(@"jjj == %d", j);
                if (j == 2) {
                    dispatch_suspend(serialQueue);
                    NSLog(@"uuuuuuuuuuuuuuuuuuu分割线");
                }
                if (j == 7) {
                    dispatch_resume(serialQueue);
                }
            }
    });

打印如下:


Paste_Image.png

另外说一下,这两个函数应该是成对出现的,要不然程序会crash,不信你试试(反正我试过)

10.dispatch_once

写过单列的童鞋对它应该很熟悉了。
dispatch_once函数是保证在应用程序执行中只执行一次指定处理的API。如下:

    static dispatch_once_t pred;
    dispatch_once(&pred, ^{
       //初始化
    });

可以保证只执行一次初始化处理。

除了上面说的,还有诸如Dispatch I/O提高文件读取速度等,有需要的可以自己研究研究(这个我也没怎么研究,好像是将文件大小分割从而进行读取)

当然GCD还有很多更深层次的运用,比如Dispatch Source,这里也不说了(我也不会。。),有兴趣的可以自己去网上研究研究,我们掌握常见的Dispatch Queue就行了。

花了几天时间断断续续也写差不多了,那GCD就给大家介绍到这里。如果你喜欢我写的文章,或者觉得对你的了解有帮助,就点个赞或者关注吧~谢谢。
(参考书籍:Objective-C高级编程)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 196,165评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,503评论 2 373
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 143,295评论 0 325
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,589评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,439评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,342评论 1 273
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,749评论 3 387
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,397评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,700评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,740评论 2 313
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,523评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,364评论 3 314
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,755评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,024评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,297评论 1 251
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,721评论 2 342
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,918评论 2 336

推荐阅读更多精彩内容

  • 我们知道在iOS开发中,一共有四种多线程技术:pthread,NSThread,GCD,NSOperation: ...
    请叫我周小帅阅读 1,475评论 0 1
  • Dispatch Queue 首先回顾一下苹果GCD的说明 开发者要做的只是定义想执行的任务并追加到适当的Disp...
    面试小集阅读 1,495评论 0 4
  • Grand Central Dispatch(GCD)概要 我的博客链接 什么是GCD? 苹果官方这么描述的:Gr...
    换个名字再说阅读 1,269评论 4 7
  • 简介 GCD(Grand Central Dispatch)是在macOS10.6提出来的,后来在iOS4.0被引...
    sunmumu1222阅读 838评论 0 2
  • 今天上午决定在家办公,因为两天前承诺最迟今天下午4点钟要交一个作业,不能食言。开工前抽了一张牌,翻开一看是...
    鱼儿加加阅读 222评论 1 2