关闭

IOS多线程开发其实很简单

125人阅读 评论(0) 收藏 举报

概览

大家都知道,在开发过程中应该尽可能减少用户等待时间,让程序尽可能快的完成运算。可是无论是哪种语言开发的程序最终往往转换成汇编语言进而解释成机器码来执行。但是机器码是按顺序执行的,一个复杂的多步操作只能一步步按顺序逐个执行。改变这种状况可以从两个角度出发:对于单核处理器,可以将多个步骤放到不同的线程,这样一来用户完成UI操作后其他后续任务在其他线程中,当CPU空闲时会继续执行,而此时对于用户而言可以继续进行其他操作;对于多核处理器,如果用户在UI线程中完成某个操作之后,其他后续操作在别的线程中继续执行,用户同样可以继续进行其他UI操作,与此同时前一个操作的后续任务可以分散到多个空闲CPU中继续执行(当然具体调度顺序要根据程序设计而定),及解决了线程阻塞又提高了运行效率。苹果从iPad2 开始使用双核A5处理器(iPhone中从iPhone 4S开始使用),A7中还加入了协处理器,如何充分发挥这些处理器的性能确实值得思考。今天将重点分析iOS多线程开发:

  1. 多线程
    1. 简介 
    2. iOS多线程 
  2. NSThread
    1. 解决线程阻塞问题 
    2. 多线程并发 
    3. 线程状态 
    4. 扩展-NSObject分类扩展 
  3. NSOperation
    1. NSInvocationOperation 
    2. NSBlockOperation 
    3. 线程执行顺序 
  4. GCD
    1. 串行队列 
    2. 并发队列 
    3. 其他任务执行方法 
  5. 线程同步
    1. NSLock同步锁 
    2. @synchronized代码块 
    3. 扩展--使用GCD解决资源抢占问题 
    4. 扩展--控制线程通信 
  6. 总结
  7.  

多线程

简介

当用户播放音频、下载资源、进行图像处理时往往希望做这些事情的时候其他操作不会被中断或者希望这些操作过程中更加顺畅。在单线程中一个线程只能做一件事情,一件事情处理不完另一件事就不能开始,这样势必影响用户体验。早在单核处理器时期就有多线程,这个时候多线程更多的用于解决线程阻塞造成的用户等待(通常是操作完UI后用户不再干涉,其他线程在等待队列中,CPU一旦空闲就继续执行,不影响用户其他UI操作),其处理能力并没有明显的变化。如今无论是移动操作系统还是PC、服务器都是多核处理器,于是“并行运算”就更多的被提及。一件事情我们可以分成多个步骤,在没有顺序要求的情况下使用多线程既能解决线程阻塞又能充分利用多核处理器运行能力。

下图反映了一个包含8个操作的任务在一个有两核心的CPU中创建四个线程运行的情况。假设每个核心有两个线程,那么每个CPU中两个线程会交替执行,两个CPU之间的操作会并行运算。单就一个CPU而言两个线程可以解决线程阻塞造成的不流畅问题,其本身运行效率并没有提高,多CPU的并行运算才真正解决了运行效率问题,这也正是并发和并行的区别。当然,不管是多核还是单核开发人员不用过多的担心,因为任务具体分配给几个CPU运算是由系统调度的,开发人员不用过多关心系统有几个CPU。开发人员需要关心的是线程之间的依赖关系,因为有些操作必须在某个操作完成完才能执行,如果不能保证这个顺序势必会造成程序问题。

MultiThread

iOS多线程

在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程。由于在iOS中除了主线程,其他子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面(新版iOS中,使用其他线程更新UI可能也能成功,但是不推荐)。iOS中多线程使用并不复杂,关键是如何控制好各个线程的执行顺序、处理好资源竞争问题。常用的多线程开发有三种方式:

1.NSThread 

2.NSOperation 

3.GCD

三种方式是随着iOS的发展逐渐引入的,所以相比而言后者比前者更加简单易用,并且GCD也是目前苹果官方比较推荐的方式(它充分利用了多核处理器的运算性能)。做过.Net开发的朋友不难发现其实这三种开发方式 刚好对应.Net中的多线程、线程池和异步调用,因此在文章中也会对比讲解。

NSThread

NSThread是轻量级的多线程开发,使用起来也并不复杂,但是使用NSThread需要自己管理线程生命周期。可以使用对象方法+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument直接将操作添加到线程中并启动,也可以使用对象方法- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 创建一个线程对象,然后调用start方法启动线程。

解决线程阻塞问题

在资源下载过程中,由于网络原因有时候很难保证下载时间,如果不使用多线程可能用户完成一个下载操作需要长时间的等待,这个过程中无法进行其他操作。下面演示一个采用多线程下载图片的过程,在这个示例中点击按钮会启动一个线程去下载图片,下载完成后使用UIImageView将图片显示到界面中。可以看到用户点击完下载按钮后,不管图片是否下载完成都可以继续操作界面,不会造成阻塞。

<span style="color: green;">//
//  NSThread实现多线程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

</span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCMainViewController.h"

</span><span style="color: black;">@</span><span style="color: blue;">interface </span><span style="color: black;">KCMainViewController (){
    UIImageView *_imageView;
}

@end

@implementation KCMainViewController

- (</span><span style="color: blue;">void</span><span style="color: black;">)viewDidLoad {
    [super viewDidLoad];
    
    [self layoutUI];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 界面布局
-(</span><span style="color: blue;">void</span><span style="color: black;">)layoutUI{
    _imageView =[[UIImageView alloc]initWithFrame:[UIScreen mainScreen].applicationFrame];
    _imageView.contentMode=UIViewContentModeScaleAspectFit;
    [self.view addSubview:_imageView];
    
    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@</span><span style="color: rgb(163, 21, 21);">"加载图片" </span><span style="color: black;">forState:UIControlStateNormal];
    </span><span style="color: green;">//添加方法
    </span><span style="color: black;">[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 将图片显示到界面
-(</span><span style="color: blue;">void</span><span style="color: black;">)updateImage:(NSData *)imageData{
    UIImage *image=[UIImage imageWithData:imageData];
    _imageView.image=image;
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 请求图片数据
-(NSData *)requestData{
    </span><span style="color: green;">//对于多线程操作建议把线程操作放到@autoreleasepool中
    </span><span style="color: black;">@autoreleasepool {
        NSURL *url=[NSURL URLWithString:@</span><span style="color: rgb(163, 21, 21);">"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"</span><span style="color: black;">];
        NSData *data=[NSData dataWithContentsOfURL:url];
        </span><span style="color: blue;">return </span><span style="color: black;">data;
    }
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 加载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImage{
    </span><span style="color: green;">//请求数据
    </span><span style="color: black;">NSData *data= [self requestData];
    </span><span style="color: green;">/*将数据显示到UI控件,注意只能在主线程中更新UI,
     另外performSelectorOnMainThread方法是NSObject的分类方法,每个NSObject对象都有此方法,
     它调用的selector方法是当前调用控件的方法,例如使用UIImageView调用的时候selector就是UIImageView的方法
     Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装)
     waitUntilDone:是否线程任务完成执行
    */
    </span><span style="color: black;">[self performSelectorOnMainThread:@selector(updateImage:) withObject:data waitUntilDone:YES];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 多线程下载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{
    </span><span style="color: green;">//方法1:使用对象方法
    //创建一个线程,第一个参数是请求的操作,第二个参数是操作方法的参数
//    NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
//    //启动一个线程,注意启动一个线程并非就一定立即执行,而是处于就绪状态,当系统调度时才真正执行
//    [thread start];
    
    //方法2:使用类方法
    </span><span style="color: black;">[NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil];
}
@end
</span>

运行效果:

NSThreadEffect

程序比较简单,但是需要注意执行步骤:当点击了“加载图片”按钮后启动一个新的线程,这个线程在演示中大概用了5s左右,在这5s内UI线程是不会阻塞的,用户可以进行其他操作,大约5s之后图片下载完成,此时调用UI线程将图片显示到界面中(这个过程瞬间完成)。另外前面也提到过,更新UI的时候使用UI线程,这里调用了NSObject的分类扩展方法,调用UI线程完成更新。

多个线程并发

上面这个演示并没有演示多个子线程操作之间的关系,现在不妨在界面中多加载几张图片,每个图片都来自远程请求。

大家应该注意到不管是使用+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument 方法还是使用- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait方法都只能传一个参数,由于更新图片需要传递UIImageView的索引和图片数据,因此这里不妨定义一个类保存图片索引和图片数据以供后面使用。

KCImageData.h

<span style="color: green;">//
//  KCImageData.h
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

</span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);"><Foundation/Foundation.h>

</span><span style="color: black;">@</span><span style="color: blue;">interface </span><span style="color: black;">KCImageData : NSObject

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 索引
@</span><span style="color: blue;">property </span><span style="color: black;">(nonatomic,assign) </span><span style="color: blue;">int </span><span style="color: black;">index;

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 图片数据
@</span><span style="color: blue;">property </span><span style="color: black;">(nonatomic,strong) NSData *data;

@end</span>

接下来将创建多个UIImageView并创建多个线程用于往UIImageView中填充图片。

KCMainViewController.m

<span style="color: green;">//
//  NSThread实现多线程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

</span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCMainViewController.h"
</span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCImageData.h"
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_COUNT 5
</span><span style="color: blue;">#define </span><span style="color: black;">COLUMN_COUNT 3
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_HEIGHT 100
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_WIDTH ROW_HEIGHT
</span><span style="color: blue;">#define </span><span style="color: black;">CELL_SPACING 10

@</span><span style="color: blue;">interface </span><span style="color: black;">KCMainViewController (){
    NSMutableArray *_imageViews;
}

@end

@implementation KCMainViewController

- (</span><span style="color: blue;">void</span><span style="color: black;">)viewDidLoad {
    [super viewDidLoad];
    
    [self layoutUI];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 界面布局
-(</span><span style="color: blue;">void</span><span style="color: black;">)layoutUI{
    </span><span style="color: green;">//创建多个图片控件用于显示图片
    </span><span style="color: black;">_imageViews=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">];
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">r=0; r<ROW_COUNT; r++) {
        </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">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;
</span><span style="color: green;">//            imageView.backgroundColor=[UIColor redColor];
            </span><span style="color: black;">[self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }
    
    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@</span><span style="color: rgb(163, 21, 21);">"加载图片" </span><span style="color: black;">forState:UIControlStateNormal];
    </span><span style="color: green;">//添加方法
    </span><span style="color: black;">[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 将图片显示到界面
-(</span><span style="color: blue;">void</span><span style="color: black;">)updateImage:(KCImageData *)imageData{
    UIImage *image=[UIImage imageWithData:imageData.data];
    UIImageView *imageView= _imageViews[imageData.index];
    imageView.image=image;
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 请求图片数据
-(NSData *)requestData:(</span><span style="color: blue;">int </span><span style="color: black;">)index{
    </span><span style="color: green;">//对于多线程操作建议把线程操作放到@autoreleasepool中
    </span><span style="color: black;">@autoreleasepool {
        NSURL *url=[NSURL URLWithString:@</span><span style="color: rgb(163, 21, 21);">"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"</span><span style="color: black;">];
        NSData *data=[NSData dataWithContentsOfURL:url];
        </span><span style="color: blue;">return </span><span style="color: black;">data;
    }
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 加载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImage:(NSNumber *)index{
    </span><span style="color: green;">//    NSLog(@"%i",i);
    //currentThread方法可以取得当前操作线程
    </span><span style="color: black;">NSLog(@</span><span style="color: rgb(163, 21, 21);">"current thread:%@"</span><span style="color: black;">,[NSThread currentThread]);
    
    </span><span style="color: blue;">int </span><span style="color: black;">i=[index integerValue];
    
</span><span style="color: green;">//    NSLog(@"%i",i);//未必按顺序输出
    
    </span><span style="color: black;">NSData *data= [self requestData:i];
    
    KCImageData *imageData=[[KCImageData alloc]init];
    imageData.index=i;
    imageData.data=data;
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 多线程下载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{
    </span><span style="color: green;">//创建多个线程用于填充图片
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<ROW_COUNT*COLUMN_COUNT; ++i) {
</span><span style="color: green;">//        [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]];
        </span><span style="color: black;">NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
        thread.name=[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"myThread%i"</span><span style="color: black;">,i];</span><span style="color: green;">//设置线程名称
        </span><span style="color: black;">[thread start];
    }
}
@end</span>

NSThreadEffect2

通过NSThread的currentThread可以取得当前操作的线程,其中会记录线程名称name和编号number,需要注意主线程编号永远为1。多个线程虽然按顺序启动,但是实际执行未必按照顺序加载照片(loadImage:方法未必依次创建,可以通过在loadImage:中打印索引查看),因为线程启动后仅仅处于就绪状态,实际是否执行要由CPU根据当前状态调度。

从上面的运行效果大家不难发现,图片并未按顺序加载,原因有两个:第一,每个线程的实际执行顺序并不一定按顺序执行(虽然是按顺序启动);第二,每个线程执行时实际网络状况很可能不一致。当然网络问题无法改变,只能尽可能让网速更快,但是可以改变线程的优先级,让15个线程优先执行某个线程。线程优先级范围为0~1,值越大优先级越高,每个线程的优先级默认为0.5。修改图片下载方法如下,改变最后一张图片加载的优先级,这样可以提高它被优先加载的几率,但是它也未必就第一个加载。因为首先其他线程是先启动的,其次网络状况我们没办法修改:

<span style="color: black;">-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{
    NSMutableArray *threads=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">];
    </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT;
    </span><span style="color: green;">//创建多个线程用于填充图片
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) {
</span><span style="color: green;">//        [NSThread detachNewThreadSelector:@selector(loadImage:) toTarget:self withObject:[NSNumber numberWithInt:i]];
        </span><span style="color: black;">NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
        thread.name=[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"myThread%i"</span><span style="color: black;">,i];</span><span style="color: green;">//设置线程名称
        </span><span style="color: blue;">if</span><span style="color: black;">(i==(count-1)){
            thread.threadPriority=1.0;
        }</span><span style="color: blue;">else</span><span style="color: black;">{
            thread.threadPriority=0.0;
        }
        [threads addObject:thread];
    }
    
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; i++) {
        NSThread *thread=threads[i];
        [thread start];
    }
}</span>

线程状态

在线程操作过程中可以让某个线程休眠等待,优先执行其他线程操作,而且在这个过程中还可以修改某个线程的状态或者终止某个指定线程。为了解决上面优先加载最后一张图片的问题,不妨让其他线程先休眠一会等待最后一个线程执行。修改图片加载方法如下即可:

<span style="color: black;">-(NSData *)requestData:(</span><span style="color: blue;">int </span><span style="color: black;">)index{
    </span><span style="color: green;">//对于多线程操作建议把线程操作放到@autoreleasepool中
    </span><span style="color: black;">@autoreleasepool {
        </span><span style="color: green;">//对非最后一张图片加载线程休眠2秒
        </span><span style="color: blue;">if </span><span style="color: black;">(index!=(ROW_COUNT*COLUMN_COUNT-1)) {
            [NSThread sleepForTimeInterval:2.0];
        }
        NSURL *url=[NSURL URLWithString:_imageNames[index]];
        NSData *data=[NSData dataWithContentsOfURL:url];

        </span><span style="color: blue;">return </span><span style="color: black;">data;
    }
}</span>
在这里让其他线程休眠2秒,此时你就会看到最后一张图片总是第一个加载(除非网速特别差)。 

线程状态分为isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经取消)三种。其中取消状态程序可以干预设置,只要调用线程的cancel方法即可。但是需要注意在主线程中仅仅能设置线程状态,并不能真正停止当前线程,如果要终止线程必须在线程中调用exist方法,这是一个静态方法,调用该方法可以退出当前线程。

假设在图片加载过程中点击停止按钮让没有完成的线程停止加载,可以改造程序如下:

<span style="color: green;">//
//  NSThread实现多线程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

</span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCMainViewController.h"
</span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCImageData.h"
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_COUNT 5
</span><span style="color: blue;">#define </span><span style="color: black;">COLUMN_COUNT 3
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_HEIGHT 100
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_WIDTH ROW_HEIGHT
</span><span style="color: blue;">#define </span><span style="color: black;">CELL_SPACING 10

@</span><span style="color: blue;">interface </span><span style="color: black;">KCMainViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
    NSMutableArray *_threads;
}

@end

@implementation KCMainViewController

- (</span><span style="color: blue;">void</span><span style="color: black;">)viewDidLoad {
    [super viewDidLoad];

    [self layoutUI];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 界面布局
-(</span><span style="color: blue;">void</span><span style="color: black;">)layoutUI{
    </span><span style="color: green;">//创建多个图片空间用于显示图片
    </span><span style="color: black;">_imageViews=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">];
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">r=0; r<ROW_COUNT; r++) {
        </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">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;
</span><span style="color: green;">//            imageView.backgroundColor=[UIColor redColor];
            </span><span style="color: black;">[self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }

    </span><span style="color: green;">//加载按钮
    </span><span style="color: black;">UIButton *buttonStart=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    buttonStart.frame=CGRectMake(50, 500, 100, 25);
    [buttonStart setTitle:@</span><span style="color: rgb(163, 21, 21);">"加载图片" </span><span style="color: black;">forState:UIControlStateNormal];
    [buttonStart addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:buttonStart];
    
    </span><span style="color: green;">//停止按钮
    </span><span style="color: black;">UIButton *buttonStop=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    buttonStop.frame=CGRectMake(160, 500, 100, 25);
    [buttonStop setTitle:@</span><span style="color: rgb(163, 21, 21);">"停止加载" </span><span style="color: black;">forState:UIControlStateNormal];
    [buttonStop addTarget:self action:@selector(stopLoadImage) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:buttonStop];
    
    </span><span style="color: green;">//创建图片链接
    </span><span style="color: black;">_imageNames=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">];
    [_imageNames addObject:@</span><span style="color: black;">    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<IMAGE_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg"</span><span style="color: black;">,i]];
    }</span><span style="color: black;">    
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 将图片显示到界面
-(</span><span style="color: blue;">void</span><span style="color: black;">)updateImage:(KCImageData *)imageData{
    UIImage *image=[UIImage imageWithData:imageData.data];
    UIImageView *imageView= _imageViews[imageData.index];
    imageView.image=image;
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 请求图片数据
-(NSData *)requestData:(</span><span style="color: blue;">int </span><span style="color: black;">)index{
    </span><span style="color: green;">//对于多线程操作建议把线程操作放到@autoreleasepool中
    </span><span style="color: black;">@autoreleasepool {
        NSURL *url=[NSURL URLWithString:_imageNames[index]];
        NSData *data=[NSData dataWithContentsOfURL:url];

        </span><span style="color: blue;">return </span><span style="color: black;">data;
    }
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 加载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImage:(NSNumber *)index{
    </span><span style="color: blue;">int </span><span style="color: black;">i=[index integerValue];

    NSData *data= [self requestData:i];

    
    NSThread *currentThread=[NSThread currentThread];
    
</span><span style="color: green;">//    如果当前线程处于取消状态,则退出当前线程
    </span><span style="color: blue;">if </span><span style="color: black;">(currentThread.isCancelled) {
        NSLog(@</span><span style="color: rgb(163, 21, 21);">"thread(%@) will be cancelled!"</span><span style="color: black;">,currentThread);
        [NSThread exit];</span><span style="color: green;">//取消当前线程
    </span><span style="color: black;">}
    
    KCImageData *imageData=[[KCImageData alloc]init];
    imageData.index=i;
    imageData.data=data;
    [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 多线程下载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{
    </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT;
    _threads=[NSMutableArray arrayWithCapacity:count];
    
    </span><span style="color: green;">//创建多个线程用于填充图片
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) {
        NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]];
        thread.name=[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"myThread%i"</span><span style="color: black;">,i];</span><span style="color: green;">//设置线程名称
        </span><span style="color: black;">[_threads addObject:thread];
    }
    </span><span style="color: green;">//循环启动线程
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) {
        NSThread *thread= _threads[i];
        [thread start];
    }
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 停止加载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)stopLoadImage{
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<ROW_COUNT*COLUMN_COUNT; i++) {
        NSThread *thread= _threads[i];
        </span><span style="color: green;">//判断线程是否完成,如果没有完成则设置为取消状态
        //注意设置为取消状态仅仅是改变了线程状态而言,并不能终止线程
        </span><span style="color: blue;">if </span><span style="color: black;">(!thread.isFinished) {
            [thread cancel];
            
        }
    }
}
@end</span>

运行效果(点击加载大概1秒后点击停止加载): 

 NSThreadEffect3

使用NSThread在进行多线程开发过程中操作比较简单,但是要控制线程执行顺序并不容易(前面万不得已采用了休眠的方法),另外在这个过程中如果打印线程会发现循环几次就创建了几个线程,这在实际开发过程中是不得不考虑的问题,因为每个线程的创建也是相当占用系统开销的。

扩展--NSObject分类扩展方法

为了简化多线程开发过程,苹果官方对NSObject进行分类扩展(本质还是创建NSThread),对于简单的多线程操作可以直接使用这些扩展方法。

- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg:在后台执行一个操作,本质就是重新创建一个线程执行当前方法。

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait:在指定的线程上执行一个方法,需要用户创建一个线程对象。

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait:在主线程上执行一个方法(前面已经使用过)。

例如前面加载图多个图片的方法,可以改为后台线程执行:

<span style="color: black;">-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{
    </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT;
    
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) {
        [self performSelectorInBackground:@selector(loadImage:) withObject:[NSNumber numberWithInt:i]];
    }
}</span>

NSOperation

使用NSOperation和NSOperationQueue进行多线程开发类似于C#中的线程池,只要将一个NSOperation(实际开中需要使用其子类NSInvocationOperation、NSBlockOperation)放到NSOperationQueue这个队列中线程就会依次启动。NSOperationQueue负责管理、执行所有的NSOperation,在这个过程中可以更加容易的管理线程总数和控制线程之间的依赖关系。

NSOperation有两个常用子类用于创建线程操作:NSInvocationOperation和NSBlockOperation,两种方式本质没有区别,但是是后者使用Block形式进行代码组织,使用相对方便。

NSInvocationOperation

首先使用NSInvocationOperation进行一张图片的加载演示,整个过程就是:创建一个操作,在这个操作中指定调用方法和参数,然后加入到操作队列。其他代码基本不用修改,直接修加载图片方法如下:

<span style="color: black;">-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{
    </span><span style="color: green;">/*创建一个调用操作
     object:调用方法参数
    */
    </span><span style="color: black;">NSInvocationOperation *invocationOperation=[[NSInvocationOperation alloc]initWithTarget:self selector:@selector(loadImage) object:nil];
    </span><span style="color: green;">//创建完NSInvocationOperation对象并不会调用,它由一个start方法启动操作,但是注意如果直接调用start方法,则此操作会在主线程中调用,一般不会这么操作,而是添加到NSOperationQueue中
//    [invocationOperation start];
    
    //创建操作队列
    </span><span style="color: black;">NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    </span><span style="color: green;">//注意添加到操作队后,队列会开启一个线程执行此操作
    </span><span style="color: black;">[operationQueue addOperation:invocationOperation];
}</span>

NSBlockOperation

下面采用NSBlockOperation创建多个线程加载图片。

<span style="color: green;">//
//  NSOperation实现多线程
//  MultiThread
//
//  Created by Kenshin Cui on 14-3-22.
//  Copyright (c) 2014年 Kenshin Cui. All rights reserved.
//

</span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCMainViewController.h"
</span><span style="color: blue;">#import </span><span style="color: rgb(163, 21, 21);">"KCImageData.h"
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_COUNT 5
</span><span style="color: blue;">#define </span><span style="color: black;">COLUMN_COUNT 3
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_HEIGHT 100
</span><span style="color: blue;">#define </span><span style="color: black;">ROW_WIDTH ROW_HEIGHT
</span><span style="color: blue;">#define </span><span style="color: black;">CELL_SPACING 10

@</span><span style="color: blue;">interface </span><span style="color: black;">KCMainViewController (){
    NSMutableArray *_imageViews;
    NSMutableArray *_imageNames;
}

@end

@implementation KCMainViewController

- (</span><span style="color: blue;">void</span><span style="color: black;">)viewDidLoad {
    [super viewDidLoad];

    [self layoutUI];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 界面布局
-(</span><span style="color: blue;">void</span><span style="color: black;">)layoutUI{
    </span><span style="color: green;">//创建多个图片控件用于显示图片
    </span><span style="color: black;">_imageViews=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">];
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">r=0; r<ROW_COUNT; r++) {
        </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">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;
</span><span style="color: green;">//            imageView.backgroundColor=[UIColor redColor];
            </span><span style="color: black;">[self.view addSubview:imageView];
            [_imageViews addObject:imageView];

        }
    }

    UIButton *button=[UIButton buttonWithType:UIButtonTypeRoundedRect];
    button.frame=CGRectMake(50, 500, 220, 25);
    [button setTitle:@</span><span style="color: rgb(163, 21, 21);">"加载图片" </span><span style="color: black;">forState:UIControlStateNormal];
    </span><span style="color: green;">//添加方法
    </span><span style="color: black;">[button addTarget:self action:@selector(loadImageWithMultiThread) forControlEvents:UIControlEventTouchUpInside];
    [self.view addSubview:button];
    
    </span><span style="color: green;">//创建图片链接
    </span><span style="color: black;">_imageNames=[NSMutableArray </span><span style="color: blue;">array</span><span style="color: black;">];
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<IMAGE_COUNT; i++) {
        [_imageNames addObject:[NSString stringWithFormat:@</span><span style="color: rgb(163, 21, 21);">"http://images.cnblogs.com/cnblogs_com/kenshincui/613474/o_%i.jpg"</span><span style="color: black;">,i]];
    }</span><span style="color: black;">    
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 将图片显示到界面
-(</span><span style="color: blue;">void</span><span style="color: black;">)updateImageWithData:(NSData *)data andIndex:(</span><span style="color: blue;">int </span><span style="color: black;">)index{
    UIImage *image=[UIImage imageWithData:data];
    UIImageView *imageView= _imageViews[index];
    imageView.image=image;
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 请求图片数据
-(NSData *)requestData:(</span><span style="color: blue;">int </span><span style="color: black;">)index{
    </span><span style="color: green;">//对于多线程操作建议把线程操作放到@autoreleasepool中
    </span><span style="color: black;">@autoreleasepool {
        NSURL *url=[NSURL URLWithString:_imageNames[index]];
        NSData *data=[NSData dataWithContentsOfURL:url];

        </span><span style="color: blue;">return </span><span style="color: black;">data;
    }
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 加载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImage:(NSNumber *)index{
    </span><span style="color: blue;">int </span><span style="color: black;">i=[index integerValue];

    </span><span style="color: green;">//请求数据
    </span><span style="color: black;">NSData *data= [self requestData:i];
    NSLog(@</span><span style="color: rgb(163, 21, 21);">"%@"</span><span style="color: black;">,[NSThread currentThread]);
    </span><span style="color: green;">//更新UI界面,此处调用了主线程队列的方法(mainQueue是UI主线程)
    </span><span style="color: black;">[[NSOperationQueue mainQueue] addOperationWithBlock:^{
        [self updateImageWithData:data andIndex:i];
    }];
}

</span><span style="color: blue;">#pragma </span><span style="color: black;">mark 多线程下载图片
-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{
    </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT;
    </span><span style="color: green;">//创建操作队列
    </span><span style="color: black;">NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    operationQueue.maxConcurrentOperationCount=5;</span><span style="color: green;">//设置最大并发线程数
    //创建多个线程用于填充图片
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count; ++i) {
        </span><span style="color: green;">//方法1:创建操作块添加到队列
//        //创建多线程操作
//        NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
//            [self loadImage:[NSNumber numberWithInt:i]];
//        }];
//        //创建操作队列
//
//        [operationQueue addOperation:blockOperation];
        
        //方法2:直接使用操队列添加操作
        </span><span style="color: black;">[operationQueue addOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
        
    }
}
@end</span>

对比之前NSThread加载张图片很发现核心代码简化了不少,这里着重强调两点:

  1. 使用NSBlockOperation方法,所有的操作不必单独定义方法,同时解决了只能传递一个参数的问题。 
  2. 调用主线程队列的addOperationWithBlock:方法进行UI更新,不用再定义一个参数实体(之前必须定义一个KCImageData解决只能传递一个参数的问题)。 
  3. 使用NSOperation进行多线程开发可以设置最大并发线程,有效的对线程进行了控制(上面的代码运行起来你会发现打印当前进程时只有有限的线程被创建,如上面的代码设置最大线程数为5,则图片基本上是五个一次加载的)。

线程执行顺序

前面使用NSThread很难控制线程的执行顺序,但是使用NSOperation就容易多了,每个NSOperation可以设置依赖线程。假设操作A依赖于操作B,线程操作队列在启动线程时就会首先执行B操作,然后执行A。对于前面优先加载最后一张图的需求,只要设置前面的线程操作的依赖线程为最后一个操作即可。修改图片加载方法如下:

<span style="color: black;">-(</span><span style="color: blue;">void</span><span style="color: black;">)loadImageWithMultiThread{
    </span><span style="color: blue;">int </span><span style="color: black;">count=ROW_COUNT*COLUMN_COUNT;
    </span><span style="color: green;">//创建操作队列
    </span><span style="color: black;">NSOperationQueue *operationQueue=[[NSOperationQueue alloc]init];
    operationQueue.maxConcurrentOperationCount=5;</span><span style="color: green;">//设置最大并发线程数
    
    </span><span style="color: black;">NSBlockOperation *lastBlockOperation=[NSBlockOperation blockOperationWithBlock:^{
        [self loadImage:[NSNumber numberWithInt:(count-1)]];
    }];
    </span><span style="color: green;">//创建多个线程用于填充图片
    </span><span style="color: blue;">for </span><span style="color: black;">(</span><span style="color: blue;">int </span><span style="color: black;">i=0; i<count-1; ++i) {
        </span><span style="color: green;">//方法1:创建操作块添加到队列
        //创建多线程操作
        </span><span style="color: black;">NSBlockOperation *blockOperation=[NSBlockOperation blockOperationWithBlock:^{
            [self loadImage:[NSNumber numberWithInt:i]];
        }];
        </span><span style="color: green;">//设置依赖操作为最后一张图片加载操作
        </span><span style="color: black;">[blockOperation addDependency:lastBlockOperation];
        
        [operationQueue addOperation:blockOperation];
        
    }
    </span><span style="color: green;">//将最后一个图片的加载操作加入线程队列
    </span><span style="color: black;">[operationQueue addOperation:lastBlockOperation];
}</span>

运行效果:

NSOperationEffect

可以看到虽然加载最后一张图片的操作最后被加入到操作队列,但是它却是被第一个执行的。操作依赖关系可以设置多个,例如A依赖于B、B依赖于C…但是千万不要设置为循环依赖关系(例如A依赖于B,B依赖于C,C又依赖于A),否则是不会被执行的。

GCD

GCD(Grand Central Dispatch)是基于C语言开发的一套多线程开发机制,也是目前苹果官方推荐的多线程开发方法。前面也说过三种开发中GCD抽象层次最高,当然是用起来也最简单,只是它基于C语言开发,并不像NSOperation是面向对象的开发,而是完全面向过程的。对于熟悉C#异步调用的朋友对于GCD学习起来应该很快,因为它与C#中的异步调用基本是一样的。这种机制相比较于前面两种多线程开发方式最显著的优点就是它对于多核运算更加有效。

GCD中也有一个类似于NSOperationQueue的队列,GCD统一管理整个队列中的任务。但是GCD中的队列分为并行队列和串行队列两类:

  • 串行队列:只有一个线程,加入到队列中的操作按添加顺序依次执行。 
  • 并发队列:有多个线程,操作进来之后它会将这些队列安排在可用的处理器上,同时保证先进来的任务优先处理。

其实在GCD中还有一个特殊队列就是主队列,用来执行主线程上的操作任务(从前面的演示中可以看到其实在NSOperation中也有一个主队列)。