在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在保存到沙盒。