一、進程與線程
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
中的回調卻是在主線程中的,如此便完成了線程間的通訊。