在iOS中實現多線程技術的方法:pthread、NSThread、GCD、NSOperation
多線程的實質就是開闢新的線程、添加隊列、在隊列中添加同步任務或者異步任務
一、pthread是一套通用的C語言的多線程API,適用於Unix,Linux,Windows等系統,可跨平臺,使用難度大,幾乎不用
二、NSThread:
是OC的線程對象,一個NSThread對象就是一條線程;
1、創建線程的方式:
1> 先創建在啓動線程
//一個線程對應一個runloop,創建一個線程會自動開闢一個runloop
NSThread * runloopTherd = [[NSThread alloc]initWithTarget:self selector:@selector(runLoop) object:nil];
//事情做完後纔會死
[runloopTherd start];
2> 直接創建並啓動線程
//直接創建並啓動線程
[NSThread detachNewThreadSelector:@selector(runLoop) toTarget:self withObject:nil];
3> 隱式創建 直接創建並啓動
//直接創建並開啓線程
[NSThread performSelectorInBackground:@selector(runThread) withObject:@"my"];
2、線程之間的通信:當線程A傳遞數據給線程B,在線程B中完成特定的任務之後,再轉到A繼續
例如:圖片下載並顯示
當觸摸屏幕的時候
//獲取圖片的url
NSURL * url = [NSURL URLWithString:@""];
//由於下載圖片是一個耗時的操作,需要開闢一條線程,object用來傳遞數據
NSThread * threadImage = [[NSThread alloc]initWithTarget:self selector:@selector(downLoadImage:) object:url];
[threadImage start];
-(void)downLoadImage:(NSURL *)urlstr
{
//下載圖片
NSData * imageData = [NSData dataWithContentsOfURL:urlstr];
//生成圖片
UIImage * downImage = [UIImage imageWithData:imageData];
//返回主線程賦值圖片
[self.imageView performSelectorOnMainThread:@selector(setImage:) withObject:downImage waitUntilDone:YES];
}
三、GCD實現多線程
GCD:Grand Central Dispatc,強大的中央調度器,是蘋果公司爲多核的並行原酸提出的解決方案,會自動根據CPU內核來開啓線程執行任務,GCD會自動管理線程的生命週期,創建線程、任務調度、線程銷燬,不需要我們自己手動管理內存
1、基本術語
任務:block 需要執行的操作,下載還是播放等
隊列:Queue 用來放任務的,任務取出的時候應該是先進先出,因此放在隊列中,包括併發隊列和串行隊列
同步:當前線程中可以立即執行任務,不具備開啓線程的能力
異步:當前線程結束時執行任務,具備開啓線程的能力
併發隊列:可以讓多個任務同時進行,自動開啓多個線程同時執行
串行隊列:順序的執行,五張圖片一張一張的下載
2、創建隊列----串行、並行、主隊列、全局隊列
*** 主隊列就在主線程中執行,並且主隊列不具備開線程的能力
/*函數 dispatch_queue_create 兩個參數
const char * label 隊列名稱
dispatch_queue_attr_t attr 隊列類型
DISPATCH_QUEUE_SERIAL 串行
DISPATCH_QUEUE_CONCURRENT 併發
*/
//創建串行隊列
dispatch_queue_t serial = dispatch_queue_create("myQueue", DISPATCH_QUEUE_SERIAL);
//創建併發隊列
dispatch_queue_t concurrent = dispatch_queue_create("myQueue", DISPATCH_QUEUE_CONCURRENT);
//全局隊列:全局隊列是併發隊列
/*
參數1 : long identifier 隊列的優先級
#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 後臺
參數2 : unsigned long flags 隊列參數,一般寫0
*/
dispatch_queue_t global = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0);
//獲取主隊列。主隊列中的任務都會在主線程中執行
dispatch_queue_t mainqueue = dispatch_get_main_queue();
3、同步、異步函數
//======================== 同步 ============================
/*
函數 dispatch_sync ()
參數:dispatch_queue_t _Nonnull queue 哪個隊列
參數:<#^(void)block#> 任務
*/
// 同步串行隊列,馬上執行,在當前線程
dispatch_sync(serial, ^{
NSLog(@"~~~%@~~~", [NSThread currentThread]);
});
// 同步並行隊列,馬上執行,在當前線程
dispatch_sync(concurrent, ^{
NSLog(@"~~~%@~~~", [NSThread currentThread]);
});
//======================== 異步 ============================
/*
函數 dispatch_aasync ()
參數:dispatch_queue_t _Nonnull queue 哪個隊列
參數:<#^(void)block#> 任務
*/
//異步函數串行隊列,開闢線程,多個任務按順序執行
dispatch_async(serial, ^{
dispatch_async(serial, ^{
NSLog(@"~~~%@~~~", [NSThread currentThread]);
});
dispatch_async(serial, ^{
NSLog(@"~~~%@~~~", [NSThread currentThread]);
});
dispatch_async(serial, ^{
NSLog(@"~~~%@~~~", [NSThread currentThread]);
});
});
//異步函數並行隊列,開闢線程,多個任務一起執行
dispatch_async(concurrent, ^{
dispatch_async(serial, ^{
NSLog(@"~~~%@~~~", [NSThread currentThread]);
});
dispatch_async(serial, ^{
NSLog(@"~~~%@~~~", [NSThread currentThread]);
});
dispatch_async(serial, ^{
NSLog(@"~~~%@~~~", [NSThread currentThread]);
});
});
使用同步函數添加任務A到串行隊列,說明要在當前串行隊列立即執行A,任務A執行完後,纔會執行任務A後面的代碼。也就是說任務A必須要等到當前串行隊列執行完成任務B後才能執行,因此必須先執行A中立即添加的任務,又要必須等到任務B執行完才能執行下一個任務,會死循環,卡死。誰也無法執行
4、GCD--線程之間的通信 下載圖片的例子
NSURL * disUrl = [NSURL URLWithString:@""];
//異步開一個線程下載圖片
dispatch_async(dispatch_queue_create("image", DISPATCH_QUEUE_PRIORITY_DEFAULT), ^{
NSData * disData = [NSData dataWithContentsOfURL:disUrl];
UIImage * disImage = [UIImage imageWithData:disData];
//返回主線程使用圖片
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = disImage;
});
});
5、GCD其他常用函數
1> dispatch_barrier 柵欄、障礙、界限
在barrier之前的先執行,然後執行barrier,再執行barrier後面的,而barrier的隊列不能是全局的併發隊列
應用:讀寫鎖
例子:假如我們在平常編碼中,要保證某個屬性線程安全的讀寫,一般加鎖方式:這就是atomic的加鎖方式,這種方式不一定就是安全的,在訪問屬性時,如果在一個縣城上多次調用getter方法,每次得到的值不一定相同,在兩次讀操作之間也可能會寫入新的shu'xing'zh
- (void)setAge:(NSString *)age
{
@synchronized(self){
_age = [age copy];
}
}
-(NSString *)age
{
@synchronized(self){
return _age;
}
}
所以我們就用到了最優寫法,加上柵欄,也就是barrier
- (void)setAge:(NSString *)age
{
dispatch_barrier_async(queue, ^{
_age = [age copy];
});
}
-(NSString *)age
{
__block NSString * testAge;
dispatch_sync(queue, ^{
testAge = _age;
});
return testAge;
}
這段代碼中加上了dispatch_barrier_async函數,也就是說在讀操作中要等之前加的寫操作完成後才能執行。
2 > dispatch_after 延遲執行
-(void)after{
//方法1 延遲兩秒執行
[self performSelector:@selector(run) withObject:@"參數" afterDelay:2.0];
//方法2
/*
dispatch_time(dispatch_time_t when, int64_t delta);
#define DISPATCH_TIME_NOW (0ull)
#define DISPATCH_TIME_FOREVER (~0ull)
*/
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
//方法3
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
3 > 單例模式,在整個應用程序中共享一份資源,只需要初始化一次
+(instancetype)sharePerson
{
static Person * person = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
person = [[Person alloc]init];
});
return person;
}
+(instancetype)allocWithZone:(struct _NSZone *)zone
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
person = [Person allocWithZone:zone];
});
return person;
}
-(id)copy
{
return person;
}
面試題:口述單例創建的過程
創建一個用static修飾的全局變量,並置爲nil,使用dispatch_onece函數檢查是否爲nil,如果是nil就創建一個並返回全局實例,需要實現allocwithzone方法,並且爲了防止由於多次訪問而得到新的實例,需要重寫copy方法,返回本身。
4 > dispatch_group 隊列組
隊列組是把相關的任務添加到一個組中進行,通過監聽組內所有任務的情況作出相應處理;比如多張圖片下載,並且合成新圖片
-(void)dispatchGroup
{
//創建隊列
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//創建組
dispatch_group_t group = dispatch_group_create();
//用組隊列下載圖片1
dispatch_group_async(group, queue, ^{
NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@""]];
self.imageOne = [UIImage imageWithData:imageData];
});
//用組隊列下載圖片2
dispatch_group_async(group, queue, ^{
NSData * imageData = [NSData dataWithContentsOfURL:[NSURL URLWithString:@""]];
self.imageTwo = [UIImage imageWithData:imageData];
});
//將圖1和圖2合併成一張新圖片
dispatch_group_notify(group, queue, ^{
CGFloat imageH = self.imageView.bounds.size.height;
CGFloat imageW = self.imageView.bounds.size.width;
//開啓圖形上下文
UIGraphicsBeginImageContext(self.imageView.bounds.size);
//畫圖
[self.imageOne drawInRect:CGRectMake(0,0,imageW/2,imageH)];
[self.imageTwo drawInRect:CGRectMake(imageW/2, 0, imageW/2, imageH)];
//將圖片取出
UIImage * newImage = UIGraphicsGetImageFromCurrentImageContext();
//關閉圖形上下文
UIGraphicsEndImageContext();
//回到主線程加載圖片
dispatch_async(dispatch_get_main_queue(), ^{
self.imageView.image = newImage;
});
});
}
5 > 定時器,GCD定時器不受Mode的影響,因此要比NSTimer準確
//
// GCDTimer.m
// testGCD
//
// Created by 李寧 on 2018/8/28.
// Copyright © 2018年 李壞. All rights reserved.
//
#import "GCDTimer.h"
@interface GCDTimer()
@property (nonatomic,strong)dispatch_source_t timer;
@end
@implementation GCDTimer
-(void)myTimer
{
static int count = 0;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0* NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
});
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
//創建一個定時器
/*
dispatch_source_create
參數一:dispatch_source_type_t 定時器的類型
參數二:uintptr_t handle 句柄
參數三:unsigned long mask 一般寫0
參數四:dispatch_queue_t 對列,dispatch_source_t是OC的對象
*/
self.timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
//回調函數的時間間隔,爲了嚴謹,用int64,相乘以後就變了
int64_t intervarl = (int64_t)(2.0* NSEC_PER_SEC);
//設置開始時間 從現在開始3s後開始
/*
函數 dispatch_time
參數一:dispatch_time_t when
#define DISPATCH_TIME_NOW (0ull) 現在開始
#define DISPATCH_TIME_FOREVER (~0ull) 啥時候開始都可以
參數二:int64_t delta
*/
dispatch_time_t start = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3.0* NSEC_PER_SEC));
//設置定時器的各種屬性
/*
函數:dispatch_source_set_timer
參數一:dispatch_source_t _Nonnull source, timer
參數二:dispatch_time_t start, 開始時間
參數三:uint64_t interval, 時間間隔
參數四:uint64_t leeway 不需要傳
*/
dispatch_source_set_timer(self.timer, start, intervarl, 0);
//設置回調,即每次的事件間隔需要做什麼
dispatch_source_set_event_handler(self.timer, ^{
NSLog(@"I am a timer");
//如果希望三次之後就停止
count ++;
if(count > 3){
dispatch_cancel(self.timer);
self.timer = nil;
}
});
//恢復定時器
dispatch_resume(self.timer);
}
@end
四、NSOperation
NSOperation是個抽象類,並不具備封裝操作的能力,他依賴於兩個子類
1、NSInvocationOperation
2、NSBlockOperation
3、自定義子類繼承自NSOperation,實現內部的相應的方法
2、使用NSOperation實現多線程的步驟
1 > 創建NSOperation對象
2 > 創建NSOperationQueue隊列
3 > 將NSOperation對象添加到NSOperationQueue中
NSInvocationOperation * option = [[NSInvocationOperation alloc]initWithTarget:self selector:@selector(my) object:nil];
//調用start並不會開闢新的線程而是在當前線程中同步執行,只有將operation對象加到隊列中才會異步
[option start];
NSBlockOperation * blockOp = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"--%@--",[NSThread currentThread]);
//打印結果證明是在主線程
}];
//增加額外的任務,只有當任務數大於1的時候纔會開異步執行
[blockOp addExecutionBlock:^{
NSLog(@"--%@--",[NSThread currentThread]);
}];
//自定義Operation:需要實現- (void)main方法,需要做的事情放在mian方法中
//當創建一個隊列,放到這個隊列中的NSOperation對象會自動放到子線程中執行
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
//創建一個主線程。放到裏的對象也會自動在子線程中執行
NSOperationQueue * mainQueue = [NSOperationQueue mainQueue];
//設置最大併發數:同時執行任務的數量,3表示同時執行3個任務,
queue.maxConcurrentOperationCount = 3;
3、隊列的取消、暫停、恢復、優先級
//- (void)cancelAllOperations;
//- (void)waitUntilAllOperationsAreFinished;
//取消所有隊列,也可以單個取消隊列,但是一旦開始就不能取消
[mainQueue cancelAllOperations];
//yes表示暫停、No表示恢復隊列
[mainQueue setSuspended:YES];
4、添加依賴
可以跨隊列依賴,但是不能循環依賴,不管NSOperation對象在哪個隊列,只要是兩個NSOperation對象就可以依賴
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
NSBlockOperation * block1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"-----%@----",[NSThread currentThread]);
}];
NSBlockOperation * block2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"-----%@----",[NSThread currentThread]);
}];
NSBlockOperation * block3 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"-----%@----",[NSThread currentThread]);
}];
NSBlockOperation * block4 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"-----%@----",[NSThread currentThread]);
}];
//添加依賴,block1和block2執行完成之後再執行block3,叫做block3依賴於block1和block2
//給block3添加依賴
[block3 addDependency:block1];
[block3 addDependency:block2];
//不能循環依賴,但可以跨隊列依賴,不管是在哪個隊列,只要是NSOperation對象就可以
[block4 addDependency:block3];
[queue addOperation:block1];
[queue addOperation:block2];
[queue addOperation:block3];
[queue addOperation:block4];
5、線程間的通信:多張圖片下載最後合成
-(void)downLoadImage
{
__block UIImage * image1 = nil;
__block UIImage * image2 = nil;
NSOperationQueue * queue = [[NSOperationQueue alloc]init];
NSBlockOperation * blcok1 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"-----%@----",[NSThread currentThread]);
image1 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@""]]];
}];
NSBlockOperation * blcok2 = [NSBlockOperation blockOperationWithBlock:^{
NSLog(@"-----%@----",[NSThread currentThread]);
image2 = [UIImage imageWithData:[NSData dataWithContentsOfURL:[NSURL URLWithString:@""]]];
}];
CGFloat imageH = imageView.bounds.size.height;
CGFloat imageW = imageView.bounds.size.width;
NSBlockOperation * block3 = [NSBlockOperation blockOperationWithBlock:^{
//開啓上下文
UIGraphicsBeginImageContext(CGSizeMake(imageW, imageH));
[image1 drawInRect:CGRectMake(0,0,imageW/2, imageH)];
[image2 drawInRect:CGRectMake(0,imageW/2,imageW/2, imageH)];
UIImage * image3 = UIGraphicsGetImageFromCurrentImageContext();
//關閉上下文
UIGraphicsEndImageContext();
//回到主線程
[[NSOperationQueue mainQueue]addOperation:[NSBlockOperation blockOperationWithBlock:^{
self->imageView.image = image3;
}]];
}];
//添加依賴
[block3 addDependency:blcok1];
[block3 addDependency:blcok2];
//將任務添加到隊列中
[queue addOperation:blcok1];
[queue addOperation:blcok2];
[queue addOperation:block3];
}
五、多線程的應用
SDWebImage框架的底層主要就是基於多線程,實現小圖片的多圖片下載,SDWebImag由兩個緩存區,一個是內存層面上的,一個是硬盤層面上的,內存中是以key--value的形式存儲圖片,當沒有內存空間的時候自動清理圖片,文件是以時間爲單位的,默認圖片存儲一週
1、入口setImageWithURL:placeHolderImage:option:會先顯示佔位圖片,然後根據URL處理圖片
2、進入SDWebImageMangaer的downLoadWithURL:delegate:option:userInfo方法交給SDImageCache,從緩存中查找圖片是否已經存在,如果存在SDImageCacheDelegate回調imageCache:didFindImage:forkey到SDWebImageManager顯示,如果沒有
3、如果內存的緩存中沒有,生成NSINvocationOperation添加到隊列中去硬盤中查找,根據key--value,如果找到了先加到緩存中(如果緩存中空閒內存過小,會先清空緩存)SDImageCacheDelegate回調方法 imageCache:didFindImage:forkeyPath顯示圖片
4、如果在硬盤中沒有找到,說明圖片不存在,需要下載,回調imageCache:didNotFindImage:forKeyPath
5、生成一個下載器:SDWebImageDownLoader開始下載,這一步由NSURLConnection完成,實現相關的delegate來判斷下載完成、失敗、下載中狀態
6、下載完成後交給SDWebImagDecoder做圖片的編碼處理,是在NSOperationQueue中完成的
7、當完成後會調用imageDownloader:didFinishWithImage回調給SDWebImageManager告訴圖片下載完成
8、在NSOperationQueue中分別將圖片在主線程顯示,在子線程中先保存到SDImageCache在保存到沙盒。