摘自《iOS程序员面试笔试宝典》
在移动互联网时代,几乎所有的应用程序都需要使用到网络请求,只有通过网络和外界进行数据交换、数据更新,应用程序才能保持新鲜与活力。网络编程是实时更新应用程序数据的最常用手段之一。而为了编写高效的网络请求模块,开发者必须能够灵活运用多线程的各种操作。
iOS网络编程与多线程基础
iOS中的多线程编程主要可以分为3个层次:NSThread、GCD和NSOperation。另外,由于Objective-C兼容C语言,所以仍然可以使用C语言的POSIX接口来实现多线程,但需要引入相应的头文件:#include
1.NSThread
NSThread是封装程度最小、最轻量级的多线程编程接口,它使用更灵活,但要手动管理线程的生命周期、线程同步和线程加锁等,开销较大。
NSThread的使用比较简单,可以动态创建并初始化NSThread对象,对其进行设置然后启动;也可以通过NSThread的静态方法快速创建并启动新线程;此外NSObject基类对象还提供了隐式快速创建NSThread线程的performSelector系列扩展工具类方法,和一些静态工具接口来控制当前线程以及获取当前线程的一些信息。
下面以在一个UIViewController中为例展示NSThread的使用方法。代码如下:
//NSTread静态工具方法
//1.是否开启了多线程
BOOL isMultiThreaded = [NSThread isMultiThreaded];
//2.获取当前线程
NSThread *currentThread = [NSThread currentThread];
//3.获取主线程
NSThread *mainThread = [NSThread mainThread];
//4.睡眠当前线程
//线程睡眠5s
[NSThread sleepForTimeInterval:5];
//线程睡眠到指定时间,效果同上
[NSThread sleepUntilDate:[NSDate dateWithTimeIntervalSinceNow:5]];
//5.退出当前线程,注意不要用在主线程调用,防止主线程被kill掉
[NSThread exit];
//Thread线程对象的基本创建,target为入口方法所在的对象,selector为线程入口方法
//1.线程实例对象创建与设置
NSThread *newThread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
//设置线程优先级threadPriority(0~1.0),该属性即将被抛弃,将使用qualityOfService代替
//newThread.threadPriority = 1.0;
newThread.qualityOfService = NSQualityOfServiceUserInteractive;
//开启线程
[newThread start];
//2.静态方法快速创建并开启新线程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
[NSThread detachNewThreadWithBlock:^{
NSLog(@"block run...");
}];
//NSObject基类隐式创建线程的一些静态工具方法
//1.在当前线程上执行方法,延迟2s
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
//2.在指定线程上执行方法,不等待当前线程
[self performSelector:@selector(run) onThread:newThread withObject:nil waitUntilDone:NO];
//3.后台异步执行方法
[self performSelectorInBackground:@selector(run) withObject:nil];
//4.在主线程上执行方法
[self performSelectorOnMainThread:@selector(run) withObject:nil waitUntilDone:NO];
2.GCD
GCD(Grand Central Dispatch),又叫大中央调度,它对线程操作进行了封装,加入了很多新的特性,内部进行了效率优化,提供了简洁的C语言接口,使用更加简单高效,也是苹果公司推荐的方式。
这里对GCD进行简单提炼,整理出如下必会内容,可以帮助读者快速掌握使用。
1)同步dispatch_sync与异步dispatch_async任务派发。
2)串行队列与并发队列dispatch_queue_t。
3)dispatch_once_t只执行一次。
4)dispatch_after延后执行。
5)dispatch_group_t组调度。
(1)串行与并发(Serial和Concurrent)
这个概念在创建操作队列的时候有宏定义参数,用来指定创建的是串行队列还是并行队列。串行指队列内任务一个接一个地执行,任务之间要依次等待不可重合,且添加的任务按照先进先出(FIFO)的顺序执行,但并不是指这就是单线程,只是同一个串行队列内的任务需要依次等待排队执行避免出现竞态条件,仍然可以创建多个串行队列并行地执行任务。也就是说,串行队列内是串行的,串行队列之间仍然是可以并行的,同一个串行队列内的任务的执行顺序是确定的(FIFO),且可以创建任意多个串行队列。
并行指同一个队列先后添加的多个任务可以同时并列执行,任务之间不会相互等待,且这些任务的执行顺序和执行过程不可预测。
(2)同步和异步任务派发(Synchronous和Asynchronous)
GCD多线程编程时经常会使用dispatch_async和dispatch_sync函数向指定队列中添加任务块,区别就是同步和异步。
同步指阻塞当前线程,即要等添加的耗时任务块block完成后,函数才能返回,后面的代码才可以继续执行。如果在主线上,那么会发生阻塞,用户会感觉应用不响应,这是要避免的。有时需要使用同步任务的原因是想保证先后添加的任务要按照编写的逻辑顺序依次执行。
异步指将任务添加到队列后函数立刻返回,后面的代码不用等待添加的任务完成返回即可继续执行。异步提交无法确认任务的执行顺序。
通过下面的代码可以比较异步和同步任务的区别。
//1.提交异步任务
NSLog(@"开始提交异步任务");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//耗时任务
[NSThread sleepForTimeInterval:10];
});
NSLog(@"异步任务提交成功!");
//2.提交同步任务
NSLog(@"开始提交同步任务:");
dispatch_sync(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//耗时任务
[NSThread sleepForTimeInterval:10];
});
NSLog(@"同步任务提交成功!");
打印结果为:
2021-07-23 17:20:31.367201+0800 网络与多线程编程[47185:2875294] 开始提交异步任务
2021-07-23 17:20:31.367352+0800 网络与多线程编程[47185:2875294] 异步任务提交成功!
2021-07-23 17:20:31.367443+0800 网络与多线程编程[47185:2875294] 开始提交同步任务:
2021-07-23 17:20:41.368782+0800 网络与多线程编程[47185:2875294] 同步任务提交成功!
通过打印结果的时间可以看出,异步任务提交后立即就执行下一步打印提交成功了,不会阻碍当前线程,提交的任务会在后台去执行;而提交同步任务要等到提交的后台任务结束后才可以继续执行当前线程的下一步。此处在主线程上添加的同步任务就会阻塞主线程,导致后面界面的显示要延迟,影响用户体验。
(3)dispatch_queue_t
GCD队列有两种类型:并发队列和串行队列。它们的区别上面已经提到,具体创建的方法很简单,要提供两个参数,一个是标记该自定义队列的唯一字符串;另一个是指定串行队列还是并发队列的宏参数。
//创建一个并发队列
dispatch_queue_t concurrent_queue = dispatch_queue_create("demo.gcd.concurrent_queue", DISPATCH_QUEUE_CONCURRENT);
//创建一个串行队列
dispatch_queue_t serial_queue = dispatch_queue_create("demo.gcd.serial_queue", DISPATCH_QUEUE_SERIAL);
另外,GCD还提供了几个常用的全局队列以及主队列,其中全局队列本质是并发队列,主队列本质是串行队列,获取方法如下:
//获取主队列(在主线程上执行)
dispatch_queue_t main_queue = dispatch_get_main_queue();
//获取不同优先级的全局队列(优先级从高到低)
dispatch_queue_t queue_high = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
dispatch_queue_t queue_default = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_queue_t queue_low = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
dispatch_queue_t queue_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0);
(4)dispatch_once_t
这个函数控制指定代码只会被执行一次,常用来实现单例模式。这里以单例模式实现的模板代码为例展示dispatch_once_t的用法,其中的实例化语句只会被执行一次。
+(instancetype)sharedInstance {
static dispatch_once_t once = 0;
static id sharedInstance = nil;
dispatch_once(&once, ^{
//只实例化一次
sharedInstance = [[self alloc]init];
});
return sharedInstance;
}
(5)dispatch_after
通过该函数可以让要提交的任务在从提交开始后的指定时间执行,也就是定时延迟执行提交的任务,使用方法很简单。
//定义延迟时间:3s
dispatch_time_t delay = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0 * NSEC_PER_SEC));
dispatch_after(delay, dispatch_get_main_queue(), ^{
//要执行的任务...
});
(6)dispatch_group_t
组调度可以实现等待一组操作都完成后执行后续操作。典型的例子是大图片的下载,例如可以将大图片分成几块同时下载,等各部分都下载完再后续将图片拼接起来,提高下载的效率。使用方法如下:
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, ^{
//操作1
});
dispatch_group_async(group, queue, ^{
//操作2
});
dispatch_group_async(group, queue, ^{
//操作3
});
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
//后续操作...
});
(7)同步代码到主线程
对于UI的更新代码,必须要在主线程上执行才会及时有效,当当前代码不在主线程时,需要将UI更新的部分代码单独同步到主线程。同步的方法有3种,可以使用NSThread类的performSelectorOnMainThread
方法或者NSOperationQueue类的mainQueue
主队列来进行同步,但推荐直接使用GCD方法。
dispatch_async(dispatch_get_main_queue(), ^{
//UI更新代码...
});
3.NSOperation
NSOperation是基于GCD的一个抽象基类,将线程封装成要执行的操作,不需要管理线程的生命周期和同步,但比GCD可控性更强,例如可以加入操作依赖(addDependency)控制操作执行顺序,设置操作队列最大可并发执行的操作个数(setMaxConcurrentOperationCount),取消操作(cancel)等。
NSOperation作为抽象基类不具备封装开发者的操作的功能,需要使用两个它的实体子类:NSBlockOperation和NSInvocationOperation,或者继承NSOperation自定义子类。NSInvocationOperation和NSBlockOperation用法的主要区别是:前者执行指定的方法,后者执行代码块,相对来说后者更加灵活易用。NSOperation操作配置完成后便可调用start
函数在当前线程执行,如果要异步执行避免阻塞当前线程,那么可以加入NSOperationQueue中异步执行。它们的简单用法如下:
- (void)viewDidLoad {
[super viewDidLoad];
//NSInvocationOperation初始化
NSInvocationOperation *invoOperation = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(run) object:nil];
[invoOperation start];
//NSBlockOperation初始化
NSBlockOperation *blkOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperation");
}];
[blkOperation start];
}
- (void)run{
NSLog(@"NSInvocationOperation");
}
另外,NSBlockOperation可以后续继续添加block执行块,操作启动后会在不同线程并发地执行这些执行块。
//NSBlockOperation初始化
NSBlockOperation *blkOperation = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"NSBlockOperationA %@",[NSThread currentThread]);
}];
[blkOperation addExecutionBlock:^{
NSLog(@"NSBlockOperationB %@",[NSThread currentThread]);
}];
[blkOperation addExecutionBlock:^{
NSLog(@"NSBlockOperationC %@",[NSThread currentThread]);
}];
[blkOperation start];
程序的输出结果为:
2021-07-23 18:08:32.750030+0800 网络与多线程编程[48288:2910528] NSBlockOperationB {number = 6, name = (null)}
2021-07-23 18:08:32.750036+0800 网络与多线程编程[48288:2910530] NSBlockOperationA {number = 3, name = (null)}
2021-07-23 18:08:32.750030+0800 网络与多线程编程[48288:2910397] NSBlockOperationC {number = 1, name = main}
另外,NSOperation的可控性比GCD要强,其中一个非常重要的特性是可以设置各操作之间的依赖实现线程同步,例如,规定操作A要在操作B完成之后才能开始执行,称为操作A依赖于操作B。而GCD中任务默认是先进先出的,要实现线程同步需要通过组队列或信号量等方式实现。
//获取主队列(主线程)
NSOperationQueue *queue = [NSOperationQueue mainQueue];
//创建a、b、c操作
NSOperation *c = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"OperationC");
}];
NSOperation *a = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"OperationA");
}];
NSOperation *b = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"OperationB");
}];
//添加操作依赖,c依赖于a和b,这样c一定会在a和b完成后才执行,即顺序为:A、B、C
[c addDependency:a];
[c addDependency:b];
//添加操作a、b、c到操作队列queue(特意将c在a和b之前添加)
[queue addOperation:c];
[queue addOperation:a];
[queue addOperation:b];
NSBlockOperation和NSInvocationOperation可以满足多数情况下的编程需求,如果需求特殊,那么需要继承NSOperation类自定义子类来更加灵活地实现。