在iOS中每个进程启动后都会建立一个主线程(UI线程),这个线程是其他线程的父线程。由于iOS中除了主线程,其他子线程是独立于Cocoa Touch的,所以只有主线程可以更新UI界面。iOS多线程的使用并不复杂,关键是如何控制好各个线程的执行顺序,处理好资源竞争问题。常用的多线程开发有三种形式:1.NSThread 2:NSOperation 3:GCD 这篇博客主要讲解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将图片显示到界面中。可以看到用户点击完下载按钮后,不管图片是否下载完成都可以继续操作界面,不会造成阻塞。
@interface ViewController (){ UIImageView *_imageView; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } - (void)layoutUI{ _imageView = [[UIImageView alloc] initWithFrame:[UIScreen mainScreen].bounds]; _imageView.contentMode = UIViewContentModeScaleAspectFit; [self.view addSubview:_imageView]; UIButton *button = [UIButton buttonWithType:0]; button.frame = CGRectMake((self.view.frame.size.width - 120) / 2, self.view.frame.size.height - 60, 120, 30); [button setTitle:@"加载图片" forState:UIControlStateNormal]; [button addTarget:self action:@selector(loadImageWithThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; }
#pragma mark - 多线程下载图片 - (void)loadImageWithThread{ //方法一 使用对象方法开辟线程 NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(loadImage) object:nil]; //启动线程 启动一个线程并非就一定立即执行 而是出于就绪状态 当系统调度时才真正执行 [thread start]; //使用类方法开辟线程 [NSThread detachNewThreadSelector:@selector(loadImage) toTarget:self withObject:nil]; //block方法 [NSThread detachNewThreadWithBlock:^{ [self loadImage]; }]; } #pragma mark -下载图片 - (void)loadImage{ //请求数据 NSData *imageData = [self requestData]; /* 请求到数据 回到主线程更新UI 每个对象都有 performSelectorOnMainThread: withObject: waitUntilDone:方法 它调用的selector方法是当前调用控件的方法,例如使用UIImageView调用的时候selector就是UIImageView的方法 Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装) waitUntilDone:是否线程任务完成执行 */ [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES]; } - (void)updateImage:(NSData *)imageData{ UIImage *image = [UIImage imageWithData:imageData]; _imageView.image = image; } - (NSData *)requestData{ @autoreleasepool { NSURL *url = [NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"]; NSData *data = [NSData dataWithContentsOfURL:url]; return data; } }
多个线程并发
上面这个演示并没有演示多个子线程操作之间的关系,现在不妨在界面中多加载几张图片,每个图片都来自远程请求。
大家应该注意到不管是使用+ (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的索引和图片数据,因此这里不妨定义一个类保存图片索引和图片数据以供后面使用。
代码如下:
#import <Foundation/Foundation.h> @interface TLFImageData : NSObject @property (nonatomic,strong) NSData *data; @property (nonatomic,assign) int index; @end
#import "ViewController.h" #import "TLFImageData.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; } @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; [self layoutUI]; } - (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];; } } UIButton *button = [UIButton buttonWithType:0]; button.frame = CGRectMake((self.view.frame.size.width - 120) / 2, self.view.frame.size.height - 60, 120, 30); [button setTitle:@"加载图片" forState:UIControlStateNormal]; [button addTarget:self action:@selector(loadImageWithThread) forControlEvents:UIControlEventTouchUpInside]; [self.view addSubview:button]; } #pragma mark - 多线程下载图片 - (void)loadImageWithThread{ for (int i=0; i<ROW_COUNT*COLUMN_COUNT; ++i){ NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]]; thread.name = [NSString stringWithFormat:@"myThread:%i",i]; [thread start]; } } #pragma mark -下载图片 - (void)loadImage:(NSNumber *)index{ //请求数据 //currentThread方法可以取得当前操作线程 NSLog(@"current thread:%@",[NSThread currentThread]); int i = (int)[index integerValue]; NSData *Data = [self requestData:i]; /* 请求到数据 回到主线程更新UI 每个对象都有 performSelectorOnMainThread: withObject: waitUntilDone:方法 它调用的selector方法是当前调用控件的方法,例如使用UIImageView调用的时候selector就是UIImageView的方法 Object:代表调用方法的参数,不过只能传递一个参数(如果有多个参数请使用对象进行封装) waitUntilDone:是否线程任务完成执行 */ TLFImageData *imageData = [[TLFImageData alloc] init]; imageData.data = Data; imageData.index = i; [self performSelectorOnMainThread:@selector(updateImage:) withObject:imageData waitUntilDone:YES]; } - (void)updateImage:(TLFImageData *)imageData{ UIImage *image = [UIImage imageWithData:imageData.data]; UIImageView *imageView = _imageViews[imageData.index]; imageView.image = image; } - (NSData *)requestData:(int)i{ @autoreleasepool { NSURL *url = [NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"]; NSData *data = [NSData dataWithContentsOfURL:url]; return data; } }
通过NSThread的currentThread可以取得当前操作的线程,其中会记录线程名称name和编号number,需要注意主线程编号永远为1。多个线程虽然按顺序启动,但是实际执行未必按照顺序加载照片(loadImage:方法未必依次创建,可以通过在loadImage:中打印索引查看),因为线程启动后仅仅处于就绪状态,实际是否执行要由CPU根据当前状态调度。
从上面的运行效果大家不难发现,图片并未按顺序加载,原因有两个:第一,每个线程的实际执行顺序并不一定按顺序执行(虽然是按顺序启动);第二,每个线程执行时实际网络状况很可能不一致。当然网络问题无法改变,只能尽可能让网速更快,但是可以改变线程的优先级,让15个线程优先执行某个线程。线程优先级范围为0~1,值越大优先级越高,每个线程的优先级默认为0.5。修改图片下载方法如下,改变最后一张图片加载的优先级,这样可以提高它被优先加载的几率,但是它也未必就第一个加载。因为首先其他线程是先启动的,其次网络状况我们没办法修改:
- (void)loadImageWithThread{ for (int i=0; i<ROW_COUNT*COLUMN_COUNT; ++i){ NSThread *thread=[[NSThread alloc]initWithTarget:self selector:@selector(loadImage:) object:[NSNumber numberWithInt:i]]; thread.name = [NSString stringWithFormat:@"myThread:%i",i]; /* 改变线程的优先级 优先级的范围在0-1 值越大优先级越高 每个线程默认的优先级是0.5 */ if (i == 1) { thread.threadPriority = 1.0; }else { thread.threadPriority = 0.0; } [thread start]; } }
在线程操作过程中可以让某个线程休眠等待,优先执行其他线程操作,而且在这个过程中还可以修改某个线程的状态或者终止某个指定的线程。为了解决一些需要依靠其他方法执行的方法的问题。我们可以让依靠其他方法的线程休眠。等待其他线程执行完,再执行。
- (NSData *)requestData:(int)i{ @autoreleasepool { if (i != (ROW_COUNT*COLUMN_COUNT-1)) { [NSThread sleepForTimeInterval:2.0]; } NSURL *url = [NSURL URLWithString:@"http://images.apple.com/iphone-6/overview/images/biggest_right_large.png"]; NSData *data = [NSData dataWithContentsOfURL:url]; return data; } }
线程状态分为isExecuting(正在执行)、isFinished(已经完成)、isCancellled(已经取消)三种。其中取消状态程序可以干预设置,只要调用线程的cancel方法即可。但是需要注意在主线程中仅仅能设置线程状态,并不能真正停止当前线程,如果要终止线程必须在线程中调用exist方法,这是一个静态方法,调用该方法可以退出当前线程。
使用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:在主线程上执行一个方法(前面已经使用过)。
例如前面加载图多个图片的方法,可以改为后台线程执行:
-(void)loadImageWithMultiThread{ int count=ROW_COUNT*COLUMN_COUNT; for (int i=0; i<count; ++i) { [self performSelectorInBackground:@selector(loadImage:) withObject:[NSNumber numberWithInt:i]]; } }