简单谈谈iOS多线程之间的通信方式
一、进程与线程
1.1 进程
进程是系统进行资源分配和调度的基本单位,在iOS
上,一个App
运行起来的实例就是一个进程,每个进程在内存中都有自己独立的地址段。
1.2 线程
线程是进程的基本执行单元,进程中的所有任务都在线程中执行,因此,一个进程中至少要有一个线程。iOS
程序启动后会默认开启一个主线程,也叫UI
线程。
1.3 进程与线程的关系
- 地址空间:同一进程中的地址空间可以被本进程中的多个线程共享,但进程与进程之间的地址空间是独立的
- 资源拥有:同一进程中的资源可以被本进程中的所有线程共享,如内存、I/O、CUP等等,但进程与进程之间的资源是相互独立的
- 一个进程中的任一线程崩溃后,都会导致整个进程崩溃,但进程奔溃后不会影响另一个进程
- 进程可以看做是线程的容器,每个进程都有一个程序运行的入口,但线程不能独立运行,必须依存于进程
1.4 线程与Runloop
的关系
- 线程与
Runloop
是一一对应的,一个Runloop
对应一个核心线程,为什么说是核心,因为Runloop
是可以嵌套的,但核心的只有一个,他们的对应关系保存在一个全局字典里 Runloop
是来管理线程的,线程执行完任务时会进入休眠状态,有任务进来时会被唤醒开始执行任务(事件驱动)
Runloop
在第一次获取时被创建,线程结束时被销毁- 主线程的
Runloop
在程序启动时就默认创建好了
- 子线程的
Runloop
是懒加载的,只有在使用时才被创建,因此在子线程中使用NSTimer
时要注意确保子线程的Runloop
已创建,否则NSTimer
不会生效。
二、多线程
2.1 概念及原理
一个进程中可以并发多个线程同时执行各自的任务,叫做多线程。分时操作系统会把CPU
的时间划分为长短基本相同的时间区间,叫“时间片”,在一个时间片内,CPU
只能处理一个线程中的一个任务,对于一个单核CPU
来说,在不同的时间片来执行不同线程中的任务,就形成了多个任务在同时执行的“假象”:

上图中,CPU
在时间片t3
开始执行线程3中的任务,但任务还没执行完,来到了t4
,开始执行线程4中的任务,在t4
这个时间片内就执行完了线程4的任务,到t5
时接着执行线程3的任务。
现在都是多核CPU
,每个核心都可以单独处理任务,实现“真正”的多线程,但是一个App
动辄几十个并发线程,那么每个核心仍然以上述原理实现多线程。
2.2 iOS
中的几种多线程
在iOS
中,有下列几种多线程的使用方式:
pthread
:即POSIX Thread
,缩写称为Pthread
,是线程的POSIX
标准,是一套通用的多线程API
,可以在Unix/Linux/Windows
等平台跨平台使用。iOS
中基本不使用。
NSThread
:苹果封装的面向对象的线程类,可以直接操作线程,比起GCD
,NSThread
效率更高,由程序员自行创建,当线程中的任务执行完毕后,线程会自动退出,程序员也可手动管理线程的生命周期。使用频率较低。
GCD
:全称Grand Central Dispatch
,由C
语言实现,是苹果为多核的并行运算提出的解决方案,CGD
会自动利用更多的CPU
内核,自动管理线程的生命周期,程序员只需要告诉GCD
需要执行的任务,无需编写任何管理线程的代码。GCD
也是iOS
使用频率最高的多线程技术。
NSOperation
:基于GCD
封装的面向对象的多线程技术,常配合NSOperationQueue
使用,使用频率较高。
三、线程池
- 线程池(Thread Pool)
顾名思义就是一个管理多个线程生命周期的池子。iOS
开发中不会直接接触到线程池,这是因为GCD
已经包含了线程池的管理,我们只需要通过GCD
获取线程来执行任务即可。
- 线程的生命周期
一个线程的生命周期包括创建
--就绪
--运行
--死亡
这四个阶段,我们可以通过阻塞、退出等来控制线程的生命周期。
四、线程间的通讯
4.1 几种线程间的通讯方式
在面试中,经常被面试官问到线程间是如何通讯的,很多童鞋会回答在子线程获取数据,切换回主线程刷新UI
,那么请你回家等消息。苹果的官方文档给我们列出了线程间通讯的几种方式:

上图的表格是按照技术复杂度由低到高顺序排列的,其中后两种只能在OS X
中使用。
Direct messaging
:这是大家非常熟悉的-performSelector:
系列。
Global variables...
:直接通过全局变量、共享内存等方式,但这种方式会造成资源抢夺,涉及到线程安全问题。
Conditions
:一种特殊的锁--条件锁,当使用条件锁使一个线程等待(wait
)时,该线程会被阻塞并进入休眠状态,在另一个线程中对同一个条件锁发送信号(single
),则等待中的线程会被唤醒继续执行任务。
Run loop sources
:通过自定义Run loop sources
来实现,后面的文章会单独研究Run loop
。
Ports and sockets
:通过端口和套接字来实现线程间通讯。
4.2 线程间通讯示例
前两种我们太熟悉了,第三种条件锁使用起来也不难,这里通过Port
来实现一个线程间通讯的Demo。
新建一个iOS
工程,新建类AvatarDownloader
,模拟一个子线程中下载头像,主线程刷新UI
的过程
// AvatarDownloader.h
extern NSString * const AvatarDownloaderUrlKey;
extern NSString * const AvatarDownloaderPortKey;
@interface AvatarDownloader : NSObject
- (void)downloadAvatarInfo:(NSDictionary *)info;
@end
// AvatarDownloader.m
NSString * const AvatarDownloaderUrlKey = @"Url";
NSString * const AvatarDownloaderPortKey = @"Port";
@interface AvatarDownloader ()<NSMachPortDelegate>
@property (nonatomic, strong) NSPort *completePort;
@property (nonatomic, strong) NSMachPort *downloaderPort;
@end
@implementation AvatarDownloader
- (instancetype)init {
if (self = [super init]) {
self.downloaderPort = [[NSMachPort alloc] init];
self.downloaderPort.delegate = self;
}
return self;
}
- (void)downloadAvatarInfo:(NSDictionary *)info {
@autoreleasepool {
NSLog(@"download thread: %@", [NSThread currentThread]);
NSString *url = info[AvatarDownloaderUrlKey];
NSLog(@"download url: %@", url);
self.completePort = info[AvatarDownloaderPortKey];
// 模拟下载
sleep(2);
UIImage *img = [UIImage imageNamed:@"avatar.jpg"];
NSData *data = UIImageJPEGRepresentation(img, 1);
NSLog(@"download complete");
NSMutableArray *components = @[data].mutableCopy;
[self.completePort sendBeforeDate:[NSDate date]
msgid:1
components:components
from:self.downloaderPort
reserved:0];
}
}
#pragma mark - NSMachPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message {
NSLog(@"downloader handlePortMessage: %@", [NSThread mainThread]);
NSArray *components = [(id)message valueForKey:@"components"];
NSData *data = components[0];
NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"response msg from receiver: %@", msg);
}
根控制器代码如下:
// ViewController.m
#import "ViewController.h"
#import "AvatarDownloader.h"
NSString * const AVATAR_URL = @"http://img3.imgtn.bdimg.com/it/u=1559309274,2399850183&fm=26&gp=0.jpg";
@interface RootViewController ()<NSMachPortDelegate>
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (nonatomic, strong) NSMachPort *mainPort;
@property (nonatomic, strong) AvatarDownloader *downloader;
@end
@implementation RootViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 创建Port对象,并添加到主线程的Runloop中
self.mainPort = [[NSMachPort alloc] init];
self.mainPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.mainPort forMode:NSDefaultRunLoopMode];
NSDictionary *info = @{AvatarDownloaderUrlKey : AVATAR_URL,
AvatarDownloaderPortKey : self.mainPort};
self.downloader = [[AvatarDownloader alloc] init];
[NSThread detachNewThreadSelector:@selector(downloadAvatarInfo:)
toTarget:self.downloader
withObject:info];
}
#pragma mark - NSPortDelegate
- (void)handlePortMessage:(NSPortMessage *)message {
NSLog(@"handlePortMessage: %@", [NSThread currentThread]);
NSArray *array = [(id)message valueForKey:@"components"];
NSData *data = array[0];
UIImage *avatar = [UIImage imageWithData:data];
self.imageView.image = avatar;
NSData *responseMsg = [@"头像已收到" dataUsingEncoding:NSUTF8StringEncoding];
NSMutableArray *components = @[responseMsg].mutableCopy;
NSPort *remotePort = [(id)message valueForKey:@"remotePort"];
// downloader线程已销毁,因此要给remotePort发消息,就得把它添加到存活的runloop中
[[NSRunLoop currentRunLoop] addPort:remotePort forMode:NSDefaultRunLoopMode];
[remotePort sendBeforeDate:[NSDate date]
msgid:2
components:components
from:self.mainPort
reserved:0];
}
@end
NSPort
的使用要点:
NSPort
对象必须添加到要接收消息的线程的Runloop
中
- 接收消息的对象实现
NSPortDelegate
协议的-handlePortMessage:
方法来获取消息内容
运行程序后,控制台输出如下:
2020-02-23 00:11:43.448999+0800 TestObjC[3140:208871] download thread: <NSThread: 0x600001e1e740>{number = 6, name = (null)}
2020-02-23 00:11:43.449342+0800 TestObjC[3140:208871] download url: http://img3.imgtn.bdimg.com/it/u=1559309274,2399850183&fm=26&gp=0.jpg
2020-02-23 00:11:45.486259+0800 TestObjC[3140:208871] download complete
2020-02-23 00:11:45.486600+0800 TestObjC[3140:208701] handlePortMessage: <NSThread: 0x600001e49e00>{number = 1, name = main}
2020-02-23 00:11:45.492472+0800 TestObjC[3140:208701] downloader handlePortMessage: <NSThread: 0x600001e49e00>{number = 1, name = main}
2020-02-23 00:11:45.492666+0800 TestObjC[3140:208701] response msg from receiver: 头像已收到
代码中首先将self.mainPort
添加到主线程的Runloop
中,然后起新线程下载头像,下载完成后通过mainPort
发送消息,此时并没有手动切换线程,但是controller
中的回调却是在主线程中的,如此便完成了线程间的通讯。