一提到多线程,我们会不由自主地想到3个名词:NSThread、Cocoa NSOperation、GCD。(这三种编程方式从左到右:抽象度层次是从低到高的,抽象度越高使用就越简单,无可厚非NSThread在小型项目中最适合使用)
首先我提一下Cocoa NSOperation:ios多线程编程之NSOperation和NSOperationQueue。
通过上面简单的介绍,大家可能多少了解到多线程使用什么代表性的名词,其实这些名词只是对多线程的管理。好了,该进入主题了。。。
NSThread:
优点:在于较其他两个轻量级;
缺点:需要自己管理线程的生命周期,线程同步。线程同步对数据的加锁会有一定的系统开销。
NSThread实现的技术有三种,一般使用cocoa thread技术(也不需知道这是什么技术,知道怎么用,什么使用原理就行了),在这里其他的两种我就不一一介绍了。
NSThread的两种直接创建方式:
1.实例方法:
-(id)initWithTarget:(id)target selector:(SEL)sel object:(id)arg;
2.类方法:
+ (void)detachNewThreadSelector:(SEL)aSelector toTarget:(id)aTarget withObject:(id)arg;
selector:线程执行的方法,这个selector只能有一个参数,而且不能有返回值。
target:selector消息发送的对象。
arg:传输给target的唯一参数,也可以为nil。
NSThread的两种直接使用方式(与直接创建方式一致):
1、[NSThread detachNewThreadSelector:@selector(doSomething:) toTarget:self withObject:nil]; 2、NSThread* myThread = [[NSThread alloc] initWithTarget:self selector:@selector(doSomething:) object:nil]; [myThread start];
来一个下载一张图片的例子吧!
@property (strong,nonatomic) UIImageView *imageView;
- (void)viewDidLoad { [super viewDidLoad]; [self NSThreadInit]; } //初始化视图上控件(下载图片按钮和ImageView控件) -(void)NSThreadInit{ _imageView = [[UIImageView alloc]initWithFrame:CGRectMake(100, 100, 100, 100)]; [self.view addSubview:_imageView]; UIButton *Get_Image_Btn = [[UIButton alloc]initWithFrame:CGRectMake(100, 250, 100, 30)]; [Get_Image_Btn setTitle:@"加载图片" forState:UIControlStateNormal]; [Get_Image_Btn addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; Get_Image_Btn.backgroundColor = [UIColor blueColor]; [self.view addSubview:Get_Image_Btn]; } //将图片显示到界面 -(void)updateImage:(NSData *)imageData{ UIImage *image = [UIImage imageWithData:imageData]; _imageView.image = image; } //请求图片 -(NSData *)requestData{ NSURL *url = [NSURL URLWithString:@"http://avatar.csdn.net/2/C/D/1_totogo2010.jpg"]; NSData *data = [NSData dataWithContentsOfURL:url]; return data; } //加载图片 -(void)loadImage{ NSData *data = [self requestData];
//[self updateImage:data]; [self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES]; } //多线程下载图片 -(void)loadImageWithMultiThread{
//方法1:使用对象方法 NSThread *thread = [[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil]; [thread start]; //方法2:使用类方法 // [NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil]; }
PS:线程下载完图片后怎么通知主线程更新界面:
[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];
performSelectorOnMainThread是NSObject的方法,除了可以更新主线程的数据外,还可以更新其他线程的比如
用:performSelector:onThread:withObject:waitUntilDone;
线程并发处理(多图下载):创建图片对象带有(NSData、int)参数,在加载图片时传入带有图片Data和下标的对象,创建多个线程用于填充图片,此前object传入nil,现在传入i值( for(int i=0 ;i<n;i++))。
GCD:
如果上述NSThread已掌握,接下来GCD也不成问题了。
要不就直接看例子,还是下载图片的例子(多图下载):
首先创建一个对象:
@interface KCImageData : NSObject @property (assign,nonatomic) int index; @property (strong,nonatomic) NSData *data;
在viewController里进行实现:
#import "ViewController.h" #import "KCImageData.h" #define ROW_COUNT 5 #define COLUMN_COUNT 3 #define ROW_HEIGHT 100 #define ROW_WIDTH ROW_HEIGHT #define CELL_SPACING 10 @interface ViewController (){ NSMutableArray *_imageViews; NSMutableArray *_imageNames; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark 界面布局 -(void)layoutUI{ //创建多个图片控件用于显示图片 _imageViews=[NSMutableArray array]; for (int r=0; r<ROW_COUNT; r++) { for (int c=0; c<COLUMN_COUNT; c++) { UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)]; imageView.contentMode=UIViewContentModeScaleAspectFit; [self.view addSubview:imageView]; [_imageViews addObject:imageView]; } } //UIButtonTypeRoundedRect 按钮圆角 UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame=CGRectMake(50, 500, 220, 25); [button setTitle:@"加载图片" forState:UIControlStateNormal]; //添加方法 [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; //创建图片链接 _imageNames=[NSMutableArray array]; for (int i=0; i<ROW_COUNT*COLUMN_COUNT; i++) { [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]]; } } #pragma mark 将图片显示到界面 -(void)updateImageWithData:(NSData *)data andIndex:(int)index{ UIImage *image = [UIImage imageWithData:data]; UIImageView *iamgeView = _imageViews[index]; iamgeView.image = image; } #pragma mark 请求图片数据 -(NSData *)requestData:(int)index{ NSURL *url = [NSURL URLWithString:_imageNames[index]]; NSData *data = [NSData dataWithContentsOfURL:url]; return data; } #pragma mark 加载图片 -(void)loadImage:(NSNumber *)index{ //如果在串行队列中会发现当前线程打印变化完全一样,因为他们在一个线程中 NSLog(@"thread is : %@",[NSThread currentThread]); int i = [index intValue]; //请求数据 NSData *data = [self requestData:i]; //更新UI界面,此处调用了GCD主线程队列的方法 dispatch_queue_t mainQueue = dispatch_get_main_queue(); dispatch_sync(mainQueue, ^{ [self updateImageWithData:data andIndex:i]; }); } #pragma mark 多线程下载图片 -(void)loadImageWithMultiThread{ int count = ROW_COUNT*COLUMN_COUNT; /*创建一个串行队列 第一个参数:队列名称 第二个参数:队列类型 */ dispatch_queue_t serialQueue = dispatch_queue_create("myThreadQueuel", DISPATCH_QUEUE_SERIAL);//注意queue对象不是指针类型 //创建多个线程用于填充图片 for (int i = 0; i < count; i++) { //异步执行队列任务 dispatch_async(serialQueue, ^{ [self loadImage:[NSNumber numberWithInt:i]]; }); } }
对,这里就是关键。
GCD是基于C语言开发的一套多线程开发机制,也是目前苹果官方推荐的多线程开发方法
GCD中也有NSOperationQueue的队列,GCD统一管理管理整个队列中的任务。但是GCD中的队列分为并行队列和串行队列两类:
串行队列:只有一个线程,加入到队列中的操作按添加顺序依次执行。
并发队列:有多个线程,操作进来之后它会将这些队列安排在可用的处理器上,同时保证先进来的任务优先处理
在GCD中还有一个特殊队列就是主队列,用来执行主线程上的操作任务
串行队列:
使用串行队列是首先要创建一个串行队列,然后调用异步调用方法,在此方法中传入串行队列和线程操作即可自动执行(多张图片会按照顺序加载,因为当前队列中只有一个线程)
并发队列:
并发队列同样是使用dispatch_queue_create()方法创建,只是最后一个参数指定为DISPATCH_QUEUE_CONCURRENT进行创建,但是在实际开发中我们通常不会重新创建一个并发队列而是创建一个并发队列而是使用dispatch_get_glibal_queue()方法取得一个全局的并发队列(如果有多个并发队列可以使用前者创建)
Cocoa NSOperation:
使用NSOperation和NSOperationQueue进行多线程开发类似于C#中的线程池,只要将一个NSOperation(实际开发中 需要使用其子类NSInvocationOperation、NSBlockOperation)放到NSOperationQueue这个队列中线程就 会依次启动。NSOperationQueue负责管理、执行所有的NSOperation,在这个过程中可以更加容易的管理线程总数和控制线程之间的依 赖关系。
PS:为什么要使用NSOperationQueue,实现了什么?与GCD有什么区别?
答:使用NSOperationQueue用来管理子类化的NSOperation对象,控制其线程并发数目。GCD和NSOperationQueue都可以对线程的管理,区别是NSOperation是对线程的高度抽象,区别是NSOperation和NSOperationQueue是对多线程的面向对象抽象。项目中使用NSOperation的优点是NSOperation是对线程的高度抽象,在项目中使用它,会使项目的程序结构更好,子类化NSOperation的设计思路,是具有面向对象的优点(复用、封装),使得实现是多线程支持,而接口简单,建议在复杂的项目中使用。项目中使用GCD的优点是GCD本身非常简单、易用,对于不复杂的多线程操作,会节省代码量,而Block参数的使用,会使代码更为易读,建议在简单项目中使用。
NSOperation有两个常用子类用于创建线程操作:NSInvocationOperation和NSBlockOperation,两种方式本质没有区别,但是是后者使用Block形式进行代码组织,使用相对方便。
NSInvocationOperation
首先使用NSInvocationOperation进行一张图片的加载演示,整个过程就是:创建一个操作,在这个操作中指定调用方法和参数,然后加入到操作队列。其他代码基本不用修改,直接加载图片方法如下:
-(void)loadImageWithMultiThread{ /*创建一个调用操作 object:调用方法参数 */ NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil]; //创建完NSInvocationOperation对象并不会调用,它由一个start方法启动操作,但是注意如果直接调用start方法,则此操作会在主线程中调用,一般不会这么操作,而是添加到NSOperationQueue中 // [invocationOperation start]; //创建操作队列 NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init]; //注意添加到操作队后,队列会开启一个线程执行此操作 [operationQueue addOperation:invocationOperation]; }
NSBlockOperation
下面采用NSBlockOperation创建多个线程加载图片。
#import "KCMainViewController.h" #import "KCImageData.h" #define ROW_COUNT 5 #define COLUMN_COUNT 3 #define ROW_HEIGHT 100 #define ROW_WIDTH ROW_HEIGHT #define CELL_SPACING 10 @interface KCMainViewController (){ NSMutableArray *_imageViews; NSMutableArray *_imageNames; } @end @implementation KCMainViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } #pragma mark 界面布局 -(void)layoutUI{ //创建多个图片控件用于显示图片 _imageViews=[NSMutableArray array]; for (int r=0; r<ROW_COUNT; r++) { for (int c=0; c<COLUMN_COUNT; c++) { UIImageView *imageView=[[UIImageView alloc]initWithFrame:CGRectMake(c*ROW_WIDTH+(c*CELL_SPACING), r*ROW_HEIGHT+(r*CELL_SPACING ), ROW_WIDTH, ROW_HEIGHT)]; imageView.contentMode=UIViewContentModeScaleAspectFit; // imageView.backgroundColor=[UIColor redColor]; [self.view addSubview:imageView]; [_imageViews addObject:imageView]; } } UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect]; button.frame=CGRectMake(50, 500, 220, 25); [button setTitle:@"加载图片" forState:UIControlStateNormal]; //添加方法 [button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; //创建图片链接 _imageNames=[NSMutableArray array]; for (int i=0; i<IMAGE_COUNT; i++) { [_imageNames addObject:[NSString stringWithFormat:@"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg",i]]; } } #pragma mark 将图片显示到界面 -(void)updateImageWithData:(NSData *)data andIndex:(int )index{ UIImage *image=[UIImage imageWithData:data]; UIImageView *imageView= _imageViews[index]; imageView.image=image; } #pragma mark 请求图片数据 -(NSData *)requestData:(int )index{ NSURL *url=[NSURL URLWithString:_imageNames[index]]; NSData *data=[NSData dataWithContentsOfURL:url]; return data; } #pragma mark 加载图片 -(void)loadImage:(NSNumber *)index{ int i=[index integerValue]; //请求数据 NSData *data= [self requestData:i]; NSLog(@"%@",[NSThread currentThread]); //更新UI界面,此处调用了主线程队列的方法(mainQueue是UI主线程) [[NSOperationQueue mainQueue] addOperationWithBlock:^{ [self updateImageWithData:data andIndex:i]; }]; } #pragma mark 多线程下载图片 -(void)loadImageWithMultiThread{ int count=ROW_COUNT*COLUMN_COUNT; //创建操作队列 NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init]; operationQueue.maxConcurrentOperationCount=5;//设置最大并发线程数 //创建多个线程用于填充图片 for (int i=0; i<count; ++i) { //方法1:创建操作块添加到队列 // //创建多线程操作 // NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{ // [self loadImage:[NSNumber numberWithInt:i]]; // }]; // //创建操作队列 // // [operationQueue addOperation:blockOperation]; //方法2:直接使用操队列添加操作 [operationQueue addOperationWithBlock:^{ [self loadImage:[NSNumber numberWithInt:i]]; }]; } } @end
对比之前NSThread加载张图片很发现核心代码简化了不少,这里着重强调两点:
- 使用NSBlockOperation方法,所有的操作不必单独定义方法,同时解决了只能传递一个参数的问题。
- 调用主线程队列的addOperationWithBlock:方法进行UI更新,不用再定义一个参数实体(之前必须定义一个KCImageData解决只能传递一个参数的问题)。
- 使用NSOperation进行多线程开发可以设置最大并发线程,有效的对线程进行了控制(上面的代码运行起来你会发现打印当前进程时只有有限的线程被创建,如上面的代码设置最大线程数为5,则图片基本上是五个一次加载的)。
在这里我再顺便提一下线程同步问题:
(线程同步、NSLock来源于载录--基本上说得很详细了)
线程同步:
说到多线程就不得不提多线程中的锁机制,多线程操作过程中往往多个线程是并发执行的,同一个资源可能被多个线程同时访问,造成资源抢夺,这个过程中如果没有锁机制往往会造成重大问题。举例来说,每年春节都是一票难求,在12306买票的过程中,成百上千的票瞬间就消失了。不妨假设某辆车有1千张票,同时有几万人在抢这列车的车票,顺利的话前面的人都能买到票。但是如果现在只剩下一张票了,而同时还有几千人在购买这张票,虽然在进入购票环节的时候会判断当前票数,但是当前已经有100个线程进入购票的环节,每个线程处理完票数都会减1,100个线程执行完当前票数为-99,遇到这种情况很明显是不允许的。
要解决资源抢夺问题在iOS中有常用的有两种方法:一种是使用NSLock同步锁,另一种是使用@synchronized代码块。两种方法实现原理是类似的,只是在处理上代码块使用起来更加简单(C#中也有类似的处理机制synchronized和lock)。
这里不妨还拿图片加载来举例,假设现在有9张图片,但是有15个线程都准备加载这9张图片,约定不能重复加载同一张图片,这样就形成了一个资源抢夺的情况。在下面的程序中将创建9张图片,每次读取照片链接时首先判断当前链接数是否大于1,用完一个则立即移除,最多只有9个。在使用同步方法之前先来看一下错误的写法:[GCD(线程同步(问题未能解决_抢票问题))]----原因:首先在_imageNames中存储了9个链接用于下载图片,然后在requestData:方法中每次只需先判断_imageNames的个数,如果大于一就读取一个链接加载图片,随即把用过的链接删除,一切貌似都没有问题。
NSLock:
iOS中对于资源抢占的问题可以使用同步锁NSLock来解决,使用时把需要加锁的代码(以后暂时称这段代码为”加锁代码“)放到NSLock的lock和unlock之间,一个线程A进入加锁代码之后由于已经加锁,另一个线程B就无法访问,只有等待前一个线程A执行完加锁代码后解锁,B线程才能访问加锁代码。需要注意的是lock和unlock之间的”加锁代码“应该是抢占资源的读取和修改代码,不要将过多的其他操作代码放到里面,否则一个线程执行的时候另一个线程就一直在等待,就无法发挥多线程的作用了。
另外,在上面的代码中”抢占资源“_imageNames定义成了成员变量,这么做是不明智的,应该定义为“原子属性”。对于被抢占资源来说将其定义为原子属性是一个很好的习惯,因为有时候很难保证同一个资源不在别处读取和修改。nonatomic属性读取的是内存数据(寄存器计算好的结果),而atomic就保证直接读取寄存器的数据,这样一来就不会出现一个线程正在修改数据,而另一个线程读取了修改之前(存储在内存中)的数据,永远保证同时只有一个线程在访问一个属性。
多线程原理:
同一时间,CPU只能处理1条线程,只有1条线程在工作(执行)
多线程并发(同时)执行,其实是CPU快速地在多条线程之间调度(切换)
如果CPU调度线程的时间足够快,就造成了多线程并发执行的假象
思考:如果线程非常非常多,会发生什么情况?
CPU会在N多线程之间调度,CPU会累死,消耗大量的CPU资源
每条线程被调度执行的频次会降低(线程的执行效率降低)
多线程的优缺点:
多线程的优点
能适当提高程序的执行效率
能适当提高资源利用率(CPU、内存利用率)
多线程的缺点
开启线程需要占用一定的内存空间(默认情况下,主线程占用1M,子线程占用512KB),如果开启大量的线程,会占用大量的内存空间,降低程序的性能
线程越多,CPU在调度线程上的开销就越大
程序设计更加复杂:比如线程之间的通信、多线程的数据共享
多线程在iOS开发中的应用
主线程:一个iOS程序运行后,默认会开启1条线程,称为“主线程”或“UI线程”
主线程的主要作用
显示\刷新UI界面(子线程里进行耗时的操作(文件读写,上传等),UI里展示一个进度条,当子线程的任务完成,通知主线程关闭进度条(刷新UI)。)
处理UI事件(比如点击事件、滚动事件、拖拽事件等)
PS:主线程的使用注意:别将比较耗时的操作放到主线程中。
耗时操作会卡住主线程,严重影响UI的流畅度,给用户一种“卡”的坏体验
解决线程
在资源下载过程中,由于网络原因有时候很难保证下载时间,如果不使用多线程可能用户完成一个下载操作需要长时间的等待,这个过程中无法进行其它操作。
可能上面的所有内容还是让你无法在自己的项目上灵活实用,下面我就来几个简单的小事例吧!
/**
*
使用GCD创建多线层
dispatch_queue_t groupBack=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(groupBack, ^{
});
*/
//使用GCD通知主线程执行 -(void)initWithGradeCenterDispatch { dispatch_queue_t Queue=dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0); dispatch_async(Queue, ^{ NSURL * URL=[NSURL URLWithString:@"url"]; NSData * DATA=[[NSData alloc]initWithContentsOfURL:URL]; UIImage * image=[[UIImage alloc]initWithData:DATA]; if (DATA!=nil) { //通知主线程更新界面 dispatch_async(dispatch_get_main_queue(), ^{
UIImageView *_imageView = [[UIImageView alloc] init]; _imageView.image=image; }); } }); }
//创建一个NSOPeration队列请求 -(void)initWithQueue {
NSOperationQueue *queue; if (queue==nil) { queue=[[[NSOperationQueue alloc]init]autorelease]; } //self.queue.maxConcurrentOperationCount NSURL * url=[NSURL URLWithString:@"url"]; ASIHTTPRequest * request=[ASIHTTPRequest requestWithURL:url]; //队列取消所有的请求 //[queue cancelAllOperations]; request.delegate=self; [request setDidFinishSelector:@selector(requestDone:)]; [request setDidFailSelector:@selector(requestFaileds:)]; [queue addOperation:request]; }
-----辉小鱼