歡迎大家指出文章中需要改正或者需要補充的地方,我會及時更新,非常感謝。
一. 多線程基礎
1. 進程
進程是指在系統中正在運行的一個應用程序
每個進程之間是獨立的,每個進程均運行在其專用且受保護的內存空間內
2.線程
1個進程要想執行任務,必須得有線程(每1個進程至少要有1條線程,稱為主線程)
一個進程(程序)的所有任務都在線程中執行
3. 進程和線程的比較
1.線程是CPU調用(執行任務)的最小單位。
2.進程是CPU分配資源的最小單位。
3.一個進程中至少要有一個線程。
4.同一個進程內的線程共享進程的資源。
4. 線程的串行
1個線程中任務的執行是串行的
如果要在1個線程中執行多個任務,那么只能一個一個地按順序執行這些任務
也就是說,在同一時間內,1個線程只能執行1個任務
5. 多線程
1個進程中可以開啟多條線程,每條線程可以并行(同時)執行不同的任務
多線程技術可以提高程序的執行效率
6. 多線程原理
同一時間,CPU只能處理1條線程,只有1條線程在工作(執行),多線程并發(同時)執行,其實是CPU快速地在多條線程之間調度(切換),如果CPU調度線程的時間足夠快,就造成了多線程并發執行的假象。
那么如果線程非常非常多,會發生什么情況?
CPU會在N多線程之間調度,CPU會累死,消耗大量的CPU資源,同時每條線程被調度執行的頻次也會會降低(線程的執行效率降低)。
因此我們一般只開3-5條線程。
7. 多線程優缺點
多線程的優點
能適當提高程序的執行效率
能適當提高資源利用率(CPU、內存利用率)
多線程的缺點
創建線程是有開銷的,iOS下主要成本包括:內核數據結構(大約1KB)、棧空間(子線程512KB、主線程1MB,也可以使用-setStackSize:設置,但必須是4K的倍數,而且最小是16K),創建線程大約需要90毫秒的創建時間
如果開啟大量的線程,會降低程序的性能,線程越多,CPU在調度線程上的開銷就越大。
程序設計更加復雜:比如線程之間的通信、多線程的數據共享等問題。
8. 多線程的應用
主線程的主要作用
顯示\刷新UI界面
處理UI事件(比如點擊事件、滾動事件、拖拽事件等)
主線程的使用注意
別將比較耗時的操作放到主線程中
耗時操作會卡住主線程,嚴重影響UI的流暢度,給用戶一種“卡”的壞體驗
將耗時操作放在子線程中執行,提高程序的執行效率
二. 多線程實現方案
1. pthread的簡單使用(了解)
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
//創建線程
pthread_t thread;
/*
第一個參數pthread_t *restrict:線程對象
第二個參數const pthread_attr_t *restrict:線程屬性
第三個參數void *(*)(void *) :指向函數的指針
第四個參數void *restrict:函數的參數
*/
pthread_create(&thread, NULL,run ,NULL);
}
//void *(*)(void *)
void *run(void *param)
{
for (NSInteger i =0 ; i<10000; i++) {
NSLog(@"%zd--%@-",i,[NSThread currentThread]);
}
return NULL;
}
2. NSThread的使用
2.1 創建線程
// 方法一:創建線程,需要自己開啟線程
NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(run) object:nil];
// 開啟線程
[thread start];
// 方法二:創建線程后自動啟動線程
[NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
// 方法三:隱式創建并啟動線程
[self performSelectorInBackground:@selector(run) withObject:nil];
后面兩種方法都不用我們開啟線程,相對方便快捷,但是沒有辦法拿到子線程對象,沒有辦法對子線程進行更詳細的設置,例如線程名字和優先級等。
2.2 NSThread的屬性
// 獲取當前線程
+ (NSThread *)currentThread;
// 創建啟動線程
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;
// 判斷是否是多線程
+ (BOOL)isMultiThreaded;
// 線程休眠 NSDate 休眠到什么時候
+ (void)sleepUntilDate:(NSDate *)date;
// 線程休眠時間
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 結束/退出當前線程
+ (void)exit;
// 獲取當前線程優先級
+ (double)threadPriority;
// 設置線程優先級 默認為0.5 取值范圍為0.0 - 1.0
// 1.0優先級最高
// 設置優先級
+ (BOOL)setThreadPriority:(double)p;
// 獲取指定線程的優先級
- (double)threadPriority NS_AVAILABLE(10_6, 4_0);
- (void)setThreadPriority:(double)p NS_AVAILABLE(10_6, 4_0);
// 設置線程的名字
- (void)setName:(NSString *)n NS_AVAILABLE(10_5, 2_0);
- (NSString *)name NS_AVAILABLE(10_5, 2_0);
// 判斷指定的線程是否是 主線程
- (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0);
// 判斷當前線程是否是主線程
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
// 獲取主線程
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);
- (id)init NS_AVAILABLE(10_5, 2_0); // designated initializer
// 創建線程
- (id)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);
// 指定線程是否在執行
- (BOOL)isExecuting NS_AVAILABLE(10_5, 2_0);
// 線程是否完成
- (BOOL)isFinished NS_AVAILABLE(10_5, 2_0);
// 線程是否被取消 (是否給當前線程發過取消信號)
- (BOOL)isCancelled NS_AVAILABLE(10_5, 2_0);
// 發送線程取消信號的 最終線程是否結束 由 線程本身決定
- (void)cancel NS_AVAILABLE(10_5, 2_0);
// 啟動線程
- (void)start NS_AVAILABLE(10_5, 2_0);
// 線程主函數 在線程中執行的函數 都要在-main函數中調用,自定義線程中重寫-main方法
- (void)main NS_AVAILABLE(10_5, 2_0); // thread body metho
2.3 NSThread線程的狀態(了解)
啟動線程
- (void)start;
// 進入就緒狀態 -> 運行狀態。當線程任務執行完畢,自動進入死亡狀態
阻塞(暫停)線程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;
// 進入阻塞狀態
強制停止線程
+ (void)exit;
// 進入死亡狀態
2.4 NSThread多線程安全隱患
多線程安全隱患的原因:1塊資源可能會被多個線程共享,也就是多個線程可能會訪問同一塊資源,比如多個線程訪問同一個對象、同一個變量、同一個文件。
那么當多個線程訪問同一塊資源時,很容易引發數據錯亂和數據安全問題。
通過上圖我們發現,當線程A訪問數據并對數據進行操作的同時,線程B訪問的數據還是沒有更新的數據,線程B同樣對數據進行操作,當兩個線程結束返回時,就會發生數據錯亂的問題。
那么我們看下圖的解決方法:添加互斥鎖。
我們可以看出,當線程A訪問數據并對數據進行操作的時候,數據被加上一把鎖,這個時候其他線程都無法訪問數據,知道線程A結束返回數據,線程B此時在訪問數據并修改,就不會造成數據錯亂了。
下面我們來看一下互斥鎖的使用:
互斥鎖使用格式
@synchronized(鎖對象) {
// 需要鎖定的代碼
}
互斥鎖的使用前提:多條線程搶奪同一塊資源時
注意:鎖定1份代碼只用1把鎖,用多把鎖是無效的
互斥鎖的優缺點
優點:能有效防止因多線程搶奪資源造成的數據安全問題
缺點:需要消耗大量的CPU資源
下面通過一個售票實例來看一下線程安全的重要性
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,strong)NSThread *thread01;
@property(nonatomic,strong)NSThread *thread02;
@property(nonatomic,strong)NSThread *thread03;
@property(nonatomic,assign)NSInteger numTicket;
//@property(nonatomic,strong)NSObject *obj;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 總票數為30
self.numTicket = 30;
self.thread01 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread01.name = @"售票員01";
self.thread02 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread02.name = @"售票員02";
self.thread03 = [[NSThread alloc]initWithTarget:self selector:@selector(saleTicket) object:nil];
self.thread03.name = @"售票員03";
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self.thread01 start];
[self.thread02 start];
[self.thread03 start];
}
// 售票
-(void)saleTicket
{
while (1) {
// 創建對象
// self.obj = [[NSObject alloc]init];
// 鎖對象,本身就是一個對象,所以self就可以了
// 鎖定的時候,其他線程沒有辦法訪問這段代碼
@synchronized (self) {
// 模擬售票時間,我們讓線程休息0.05s
[NSThread sleepForTimeInterval:0.05];
if (self.numTicket > 0) {
self.numTicket -= 1;
NSLog(@"%@賣出了一張票,還剩下%zd張票",[NSThread currentThread].name,self.numTicket);
}else{
NSLog(@"票已經賣完了");
break;
}
}
}
}
@end
當沒有加互斥鎖的時候我們看一下輸出
我們發現第29張,第27張都被銷售了3次,這顯然是不允許的,這就是數據錯亂,那么當我們加上互斥鎖時,其鎖定的時候其他線程沒有辦法訪問鎖定的內容,等其訪問完畢之后,其他線程才可以訪問,我們愛來看一下輸出
此時就不會出現同一張票被多次出售的數據錯亂的情況了。
2.5 NSThread線程之間的通信
什么叫做線程間通信
在1個進程中,線程往往不是孤立存在的,多個線程之間需要經常進行通信,例如我們在子線程完成下載圖片后,回到主線程刷新UI顯示圖片
線程間通信的體現
1個線程傳遞數據給另1個線程
在1個線程中執行完特定任務后,轉到另1個線程繼續執行任務
線程間通信常用的方法
// 返回主線程
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
// 返回指定線程
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait;
下面我們通過一個實例看一下線程之間的通信
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[NSThread detachNewThreadSelector:@selector(donwLoadImage) toTarget:self withObject:nil];
}
-(void)donwLoadImage
{
// 獲取圖片url地址 http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg
NSURL *url = [NSURL URLWithString:@"http://www.itunes123.com/uploadfile/2016/0421/20160421014340186.jpg"];
// 下載圖片二進制文件
NSData *data = [NSData dataWithContentsOfURL:url];
// 將圖片二進制文件轉化為image;
UIImage *image = [UIImage imageWithData:data];
// 參數 waitUntilDone 是否等@selector(showImage:) 執行完畢以后再執行下面的操作 YES :等 NO:不等
// 返回主線程顯示圖片
// [self performSelectorOnMainThread:@selector(showImage:) withObject:image waitUntilDone:YES];
// self.imageView 也可以直接調用這個方法 直接選擇 setImage方法,傳入參數image即可
// [self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:image waitUntilDone:YES];
// 返回特定的線程,[NSThread mainThread] 獲得主線程
[self performSelector:@selector(showImage:) onThread:[NSThread mainThread] withObject:image waitUntilDone:YES];
}
-(void)showImage:(UIImage *)image
{
self.imageView.image = image;
}
@end
3. GCD的使用(重點)
GCD的全稱是Grand Central Dispatch,是純C語言,提供了非常多強大的函數
GCD的優勢
GCD是蘋果公司為多核的并行運算提出的解決方案
GCD會自動利用更多的CPU內核(比如雙核、四核)
GCD會自動管理線程的生命周期(創建線程、調度任務、銷毀線程)
程序員只需要告訴GCD想要執行什么任務,不需要編寫任何線程管理代碼
3.1 任務和隊列
GCD中有2個核心概念:任務和隊列
任務:執行什么操作,任務有兩種執行方式: 同步函數 和 異步函數,他們之間的區別是
同步:只能在當前線程中執行任務,不具備開啟新線程的能力,任務立刻馬上執行,會阻塞當前線程并等待 Block中的任務執行完畢,然后當前線程才會繼續往下運行
異步:可以在新的線程中執行任務,具備開啟新線程的能力,但不一定會開新線程,當前線程會直接往下執行,不會阻塞當前線程
隊列:用來存放任務,分為串行隊列 和 并行隊列
串行隊列(Serial Dispatch Queue)
讓任務一個接著一個地執行(一個任務執行完畢后,再執行下一個任務)
并發隊列(Concurrent Dispatch Queue)
可以讓多個任務并發(同時)執行(自動開啟多個線程同時執行任務)
并發功能只有在異步(dispatch_async)函數下才有效
GCD的使用就2個步驟
定制任務
確定想做的事情將任務添加到隊列中
GCD會自動將隊列中的任務取出,放到對應的線程中執行
任務的取出遵循隊列的FIFO原則:先進先出,后進后出
3.2 GCD的創建
- 隊列的創建
// 第一個參數const char *label : C語言字符串,用來標識
// 第二個參數dispatch_queue_attr_t attr : 隊列的類型
// 并發隊列:DISPATCH_QUEUE_CONCURRENT
// 串行隊列:DISPATCH_QUEUE_SERIAL 或者 NULL
dispatch_queue_t queue = dispatch_queue_create(const char *label, dispatch_queue_attr_t attr);
創建并發隊列
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_CONCURRENT);
創建串行隊列
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
GCD默認已經提供了全局并發隊列,供整個應用使用,可以無需手動創建
/**
第一個參數:優先級 也可直接填后面的數字
#define DISPATCH_QUEUE_PRIORITY_HIGH 2 // 高
#define DISPATCH_QUEUE_PRIORITY_DEFAULT 0 // 默認
#define DISPATCH_QUEUE_PRIORITY_LOW (-2) // 低
#define DISPATCH_QUEUE_PRIORITY_BACKGROUND INT16_MIN // 后臺
第二個參數: 預留參數 0
*/
dispatch_queue_t quque1 = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
獲得主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
- 任務的執行
隊列在queue中,任務在block塊中
開啟同步函數 同步函數:要求立刻馬上開始執行
/*
第一個參數:隊列
第二個參數:block,在里面封裝任務
*/
dispatch_sync(queue, ^{
});
開啟異步函數 異步函數 :等主線程執行完畢之后,回過頭開線程執行任務
dispatch_async(queue, ^{
});
- 任務和隊列的組合
任務:同步函數 異步函數
隊列:串行 并行
異步函數+并發隊列:會開啟新的線程,并發執行
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
異步函數+串行隊列:會開啟一條線程,任務串行執行
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
dispatch_async(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
同步函數+并發隊列:不會開線程,任務串行執行
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
同步函數+串行隊列:不會開線程,任務串行執行
dispatch_queue_t queue = dispatch_queue_create("com.xxcc", DISPATCH_QUEUE_SERIAL);
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
異步函數+主隊列:不會開線程,任務串行執行
使用主隊列(跟主線程相關聯的隊列)
主隊列是GCD自帶的一種特殊的串行隊列,放在主隊列中的任務,都會放到主線程中執行
//1.獲得主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
//2.異步函數
dispatch_async(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
同步函數+主隊列:死鎖
//1.獲得主隊列
dispatch_queue_t queue = dispatch_get_main_queue();
//2.同步函數
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
因為這個方法在主線程中,給主線程中添加任務,而同步函數要求立刻馬上執行,因此就會相互等待而發生死鎖。將這個方法放入子線程中,則不會發生死鎖,任務串行執行。
總結:
- 同步函數和異步函數的執行順序
同步函數:立刻馬上執行,會阻塞當前線程
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self syncConcurrent];
}
//同步函數+并發隊列:不會開線程,任務串行執行
-(void)syncConcurrent
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
NSLog(@"--syncConcurrent--start-");
dispatch_sync(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"---download2---%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"---download3---%@",[NSThread currentThread]);
});
dispatch_sync(queue, ^{
NSLog(@"---download4---%@",[NSThread currentThread]);
});
NSLog(@"--syncConcurrent--end-");
}
我們看一下輸出
異步函數:當前線程會直接往下執行,不會阻塞當前線程
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self syncConcurrent];
}
//異步函數+并發隊列:會開啟新的線程,并發執行
-(void)asyncCONCURRENT
{
NSLog(@"--asyncCONCURRENT--start-");
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
NSLog(@"---download1---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"---download2---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"---download3---%@",[NSThread currentThread]);
});
dispatch_async(queue, ^{
NSLog(@"---download4---%@",[NSThread currentThread]);
});
NSLog(@"--asyncCONCURRENT--end-");
}
我們來看一下輸出
注意:GCD中開多少條線程是由系統根據CUP繁忙程度決定的,如果任務很多,GCD會開啟適當的子線程,并不會讓所有任務同時執行。
3.3 GCD線程間的通信
我們同樣通過一個實例來看
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_async(queue, ^{
// 獲得圖片URL
NSURL *url = [NSURL URLWithString:@"http://upload-images.jianshu.io/upload_images/2301429-d5cc0a007447e469.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"];
// 將圖片URL下載為二進制文件
NSData *data = [NSData dataWithContentsOfURL:url];
// 將二進制文件轉化為image
UIImage *image = [UIImage imageWithData:data];
NSLog(@"%@",[NSThread currentThread]);
// 返回主線程 這里用同步函數不會發生死鎖,因為這個方法在子線程中被調用。
// 也可以使用異步函數
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
NSLog(@"%@",[NSThread currentThread]);
});
});
}
@end
GCD線程間的通信非常簡單,使用同步或異步函數,傳入主隊列即可。
3.4 GCD其他常用函數
- 柵欄函數(控制任務的執行順序)
dispatch_barrier_async(queue, ^{
NSLog(@"--dispatch_barrier_async-");
});
我們來看一下柵欄函數的作用
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self barrier];
}
-(void)barrier
{
//1.創建隊列(并發隊列)
dispatch_queue_t queue = dispatch_queue_create("com.xxccqueue", DISPATCH_QUEUE_CONCURRENT);
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download1--%@",i,[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download2--%@",i,[NSThread currentThread]);
}
});
//柵欄函數
dispatch_barrier_async(queue, ^{
NSLog(@"我是一個柵欄函數");
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download3--%@",i,[NSThread currentThread]);
}
});
dispatch_async(queue, ^{
for (NSInteger i = 0; i<3; i++) {
NSLog(@"%zd-download4--%@",i,[NSThread currentThread]);
}
});
}
我們來看一下輸出
柵欄函數可以控制任務執行的順序,柵欄函數之前的執行完畢之后,執行柵欄函數,然后在執行柵欄函數之后的
- 延遲執行(延遲·控制在哪個線程執行)
/*
第一個參數:延遲時間
第二個參數:要執行的代碼
如果想讓延遲的代碼在子線程中執行,也可以更改在哪個隊列中執行 dispatch_get_main_queue() -> dispatch_get_global_queue(0, 0)
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"---%@",[NSThread currentThread]);
});
延遲執行的其他方法:
// 2s中之后調用run方法
[self performSelector:@selector(run) withObject:nil afterDelay:2.0];
// repeats:YES 是否重復
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
- 一次性代碼
-(void)once
{
//整個程序運行過程中只會執行一次
//onceToken用來記錄該部分的代碼是否被執行過
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSLog(@"-----");
});
}
一次性代碼主要應用在單例模式中,關于單例模式詳解大家可以去看iOS-單例模式寫一次就夠了這里不在贅述。
- 快速迭代(開多個線程并發完成迭代操作)
/*
第一個參數:迭代的次數
第二個參數:在哪個隊列中執行
第三個參數:block要執行的任務
*/
dispatch_apply(10, queue, ^(size_t index) {
});
快速迭代:開啟多條線程,并發執行,相比于for循環在耗時操作中極大的提高效率和速度
- 隊列組(同柵欄函數)
// 創建隊列組
dispatch_group_t group = dispatch_group_create();
// 創建并行隊列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 執行隊列組任務
dispatch_group_async(group, queue, ^{
});
//隊列組中的任務執行完畢之后,執行該函數
dispatch_group_notify(group, queue, ^{
});
下面看一了實例使用group下載兩張圖片然后合成在一起
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property (nonatomic, strong) UIImage *image1; /**< 圖片1 */
@property (nonatomic, strong) UIImage *image2; /**< 圖片2 */
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
[self group];
}
-(void)group
{
//下載圖片1
//創建隊列組
dispatch_group_t group = dispatch_group_create();
//1.開子線程下載圖片
//創建隊列(并發)
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
dispatch_group_async(group, queue, ^{
//1.獲取url地址
NSURL *url = [NSURL URLWithString:@"http://www.huabian.com/uploadfile/2015/0914/20150914014032274.jpg"];
//2.下載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
//3.把二進制數據轉換成圖片
self.image1 = [UIImage imageWithData:data];
NSLog(@"1---%@",self.image1);
});
//下載圖片2
dispatch_group_async(group, queue, ^{
//1.獲取url地址
NSURL *url = [NSURL URLWithString:@"http://img1.3lian.com/img2011/w12/1202/19/d/88.jpg"];
//2.下載圖片
NSData *data = [NSData dataWithContentsOfURL:url];
//3.把二進制數據轉換成圖片
self.image2 = [UIImage imageWithData:data];
NSLog(@"2---%@",self.image2);
});
//合成,隊列組執行完畢之后執行
dispatch_group_notify(group, queue, ^{
//開啟圖形上下文
UIGraphicsBeginImageContext(CGSizeMake(200, 200));
//畫1
[self.image1 drawInRect:CGRectMake(0, 0, 200, 100)];
//畫2
[self.image2 drawInRect:CGRectMake(0, 100, 200, 100)];
//根據圖形上下文拿到圖片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
//關閉上下文
UIGraphicsEndImageContext();
//回到主線程刷新UI
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = image;
NSLog(@"%@--刷新UI",[NSThread currentThread]);
});
});
}
4. NSOperation的使用(重點)
NSOperation 是蘋果公司對 GCD 的封裝,完全面向對象,并比GCD多了一些更簡單實用的功能,所以使用起來更加方便易于理解。NSOperation 和NSOperationQueue 分別對應 GCD 的 任務 和 隊列。
NSOperation和NSOperationQueue實現多線程的具體步驟
1.將需要執行的操作封裝到一個NSOperation對象中
2.將NSOperation對象添加到NSOperationQueue中
系統會自動將NSOperationQueue中的NSOperation取出來,并將取出的NSOperation封裝的操作放到一條新線程中執行
4.1 NSOperation的創建
NSOperation是個抽象類,并不具備封裝操作的能力,必須使用它的子類
使用NSOperation子類的方式有3種
- NSInvocationOperation
/*
第一個參數:目標對象
第二個參數:選擇器,要調用的方法
第三個參數:方法要傳遞的參數
*/
NSInvocationOperation *op = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(download) object:nil];
//啟動操作
[op start];
- NSBlockOperation(最常用)
//1.封裝操作
NSBlockOperation *op = [NSBlockOperation blockOperationWithBlock:^{
//要執行的操作,在主線程中執行
NSLog(@"1------%@",[NSThread currentThread]);
}];
//2.追加操作,追加的操作在子線程中執行,可以追加多條操作
[op addExecutionBlock:^{
NSLog(@"---download2--%@",[NSThread currentThread]);
}];
[op start];
- 自定義子類繼承NSOperation,實現內部相應的方法
// 重寫自定義類的main方法實現封裝操作
-(void)main
{
// 要執行的操作
}
// 實例化一個自定義對象,并執行操作
CLOperation *op = [[CLOperation alloc]init];
[op start];
自定義類封裝性高,復用性高。
4.2 NSOperationQueue的使用
NSOperation中的兩種隊列
主隊列:通過mainQueue獲得,凡是放到主隊列中的任務都將在主線程執行
非主隊列:直接alloc init出來的隊列。非主隊列同時具備了并發和串行的功能,通過設置最大并發數屬性來控制任務是并發執行還是串行執行
NSOperationQueue的作用
NSOperation可以調用start方法來執行任務,但默認是同步執行的
如果將NSOperation添加到NSOperationQueue(操作隊列)中,系統會自動異步執行NSOperation中的操作
添加操作到NSOperationQueue中
- (void)addOperation:(NSOperation *)op;
- (void)addOperationWithBlock:(void (^)(void))block;
注意:將操作添加到NSOperationQueue中,就會自動啟動,不需要再自己啟動了addOperation 內部調用 start方法
start方法 內部調用 main方法
4.3 NSOperation和NSOperationQueue結合使用創建多線程
注:這里使用NSBlockOperation示例,其他兩種方法一樣
// 1. 創建非主隊列 同時具備并發和串行的功能,默認是并發隊列
NSOperationQueue *queue =[[NSOperationQueue alloc]init];
//NSBlockOperation 不論封裝操作還是追加操作都是異步并發執行
// 2. 封裝操作
NSBlockOperation *op1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"download1 -- %@",[NSThread currentThread]);
}];
// 3. 將封裝操作加入主隊列
// 也可以不獲取封裝操作對象 直接添加操作到隊列中
//[queue addOperationWithBlock:^{
// 操作
//}];
[queue addOperation:op1];
4.4 NSOperation和NSOperationQueue的重要屬性和方法
NSOperation
- NSOperation的依賴
- (void)addDependency:(NSOperation *)op;
// 操作op1依賴op5,即op1必須等op5執行完畢之后才會執行
// 添加操作依賴,注意不能循環依賴,如果循環依賴會造成兩個任務都不會執行
// 也可以夸隊列依賴,依賴別的隊列的操作
[op1 addDependency:op5];
- NSOperation操作監聽
void (^completionBlock)(void)
// 監聽操作的完成
// 當op1線程完成之后,立刻就會執行block塊中的代碼
// block中的代碼與op1不一定在一個線程中執行,但是一定在子線程中執行
op1.completionBlock = ^{
NSLog(@"op1已經完成了---%@",[NSThread currentThread]);
};
NSOperationQueue
- maxConcurrentOperationCount
//1.創建隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
/*
默認是并發隊列,如果最大并發數>1,并發
如果最大并發數==1,串行隊列
系統的默認是最大并發數-1 ,表示不限制
設置成0則不會執行任何操作
*/
queue.maxConcurrentOperationCount = 1;
- suspended
//當值為YES的時候暫停,為NO的時候是恢復
queue.suspended = YES;
- -(void)cancelAllOperations;
//取消所有的任務,不再執行,不可逆
[queue cancelAllOperations];
注意:暫停和取消只能暫停或取消處于等待狀態的任務,不能暫停或取消正在執行中的任務,必須等正在執行的任務執行完畢之后才會暫停,如果想要暫停或者取消正在執行的任務,可以在每個任務之間即每當執行完一段耗時操作之后,判斷是否任務是否被取消或者暫停。如果想要精確的控制,則需要將判斷代碼放在任務之中,但是不建議這么做,頻繁的判斷會消耗太多時間
4.5 NSOperation和NSOperationQueue的一些其他屬性和方法
NSOperation
// 開啟線程
- (void)start;
- (void)main;
// 判斷線程是否被取消
@property (readonly, getter=isCancelled) BOOL cancelled;
// 取消當前線程
- (void)cancel;
//NSOperation任務是否在運行
@property (readonly, getter=isExecuting) BOOL executing;
//NSOperation任務是否已結束
@property (readonly, getter=isFinished) BOOL finished;
// 添加依賴
- (void)addDependency:(NSOperation *)op;
// 移除依賴
- (void)removeDependency:(NSOperation *)op;
// 優先級
typedef NS_ENUM(NSInteger, NSOperationQueuePriority) {
NSOperationQueuePriorityVeryLow = -8L,
NSOperationQueuePriorityLow = -4L,
NSOperationQueuePriorityNormal = 0,
NSOperationQueuePriorityHigh = 4,
NSOperationQueuePriorityVeryHigh = 8
};
// 操作監聽
@property (nullable, copy) void (^completionBlock)(void) NS_AVAILABLE(10_6, 4_0);
// 阻塞當前線程,直到該NSOperation結束。可用于線程執行順序的同步
- (void)waitUntilFinished NS_AVAILABLE(10_6, 4_0);
// 獲取線程的優先級
@property double threadPriority NS_DEPRECATED(10_6, 10_10, 4_0, 8_0);
// 線程名稱
@property (nullable, copy) NSString *name NS_AVAILABLE(10_10, 8_0);
@end
NSOperationQueue
// 獲取隊列中的操作
@property (readonly, copy) NSArray<__kindof NSOperation *> *operations;
// 隊列中的操作數
@property (readonly) NSUInteger operationCount NS_AVAILABLE(10_6, 4_0);
// 最大并發數,同一時間最多只能執行三個操作
@property NSInteger maxConcurrentOperationCount;
// 暫停 YES:暫停 NO:繼續
@property (getter=isSuspended) BOOL suspended;
// 取消所有操作
- (void)cancelAllOperations;
// 阻塞當前線程直到此隊列中的所有任務執行完畢
- (void)waitUntilAllOperationsAreFinished;
4.6 NSOperation線程之間的通信
NSOperation線程之間的通信方法
// 回到主線程刷新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
self.imageView.image = image;
}];
我們同樣使用下載多張圖片合成綜合案例
#import "ViewController.h"
@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;
@property(nonatomic,strong)UIImage *image1;
@property(nonatomic,strong)UIImage *image2;
@end
@implementation ViewController
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 創建非住隊列
NSOperationQueue *queue = [[NSOperationQueue alloc]init];
// 下載第一張圖片
NSBlockOperation *download1 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
self.image1 = [UIImage imageWithData:data];
}];
// 下載第二張圖片
NSBlockOperation *download2 = [NSBlockOperation blockOperationWithBlock:^{
NSURL *url = [NSURL URLWithString:@"http://img2.3lian.com/2014/c7/12/d/77.jpg"];
NSData *data = [NSData dataWithContentsOfURL:url];
self.image2 = [UIImage imageWithData:data];
}];
// 合成操作
NSBlockOperation *combie = [NSBlockOperation blockOperationWithBlock:^{
// 開啟圖形上下文
UIGraphicsBeginImageContext(CGSizeMake(375, 667));
// 繪制圖片1
[self.image1 drawInRect:CGRectMake(0, 0, 375, 333)];
// 繪制圖片2
[self.image2 drawInRect:CGRectMake(0, 334, 375, 333)];
// 獲取合成圖片
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
// 關閉圖形上下文
UIGraphicsEndImageContext();
// 回到主線程刷新UI
[[NSOperationQueue mainQueue]addOperationWithBlock:^{
self.imageView.image = image;
}];
}];
// 添加依賴,合成圖片需要等圖片1,圖片2都下載完畢之后合成
[combie addDependency:download1];
[combie addDependency:download2];
// 添加操作到隊列
[queue addOperation:download1];
[queue addOperation:download2];
[queue addOperation:combie];
}
@end
注意:子線程執行完操作之后就會立即釋放,即使我們使用強引用引用子線程使子線程不被釋放,也不能給子線程再次添加操作,或者再次開啟。
?建議收藏,用到的時候一查就明白了。
?本文借鑒了很多前輩的文章,如果有不對的地方請指正,歡迎大家一起交流學習 xx_cc 。