iOS 多线程编程 NSThread & NSOperation

iOS May 23, 2015

前方高能提醒,字多杀猫,慎点

NSThread

这个应该是用起来最简单的了,官方文档这么写的

/*	NSThread.h
	Copyright (c) 1994-2014, Apple Inc. All rights reserved.
*/

#import <Foundation/NSObject.h>
#import <Foundation/NSDate.h>

@class NSArray, NSMutableDictionary, NSDate;

@interface NSThread : NSObject  {
@private
    id _private;
    uint8_t _bytes[44];
}

+ (NSThread *)currentThread;

+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;

+ (BOOL)isMultiThreaded;

@property (readonly, retain) NSMutableDictionary *threadDictionary;

+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

+ (void)exit;

+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;

@property double threadPriority NS_AVAILABLE(10_6, 4_0); // To be deprecated; use qualityOfService below

@property NSQualityOfService qualityOfService NS_AVAILABLE(10_10, 8_0); // read-only after the thread is started

+ (NSArray *)callStackReturnAddresses NS_AVAILABLE(10_5, 2_0);
+ (NSArray *)callStackSymbols NS_AVAILABLE(10_6, 4_0);

@property (copy) NSString *name NS_AVAILABLE(10_5, 2_0);

@property NSUInteger stackSize NS_AVAILABLE(10_5, 2_0);

@property (readonly) BOOL isMainThread NS_AVAILABLE(10_5, 2_0);
+ (BOOL)isMainThread NS_AVAILABLE(10_5, 2_0); // reports whether current thread is main
+ (NSThread *)mainThread NS_AVAILABLE(10_5, 2_0);

- (instancetype)init NS_AVAILABLE(10_5, 2_0) NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument NS_AVAILABLE(10_5, 2_0);

@property (readonly, getter=isExecuting) BOOL executing NS_AVAILABLE(10_5, 2_0);
@property (readonly, getter=isFinished) BOOL finished NS_AVAILABLE(10_5, 2_0);
@property (readonly, getter=isCancelled) BOOL cancelled NS_AVAILABLE(10_5, 2_0);

- (void)cancel NS_AVAILABLE(10_5, 2_0);

- (void)start NS_AVAILABLE(10_5, 2_0);

- (void)main NS_AVAILABLE(10_5, 2_0);	// thread body method

@end

FOUNDATION_EXPORT NSString * const NSWillBecomeMultiThreadedNotification;
FOUNDATION_EXPORT NSString * const NSDidBecomeSingleThreadedNotification;
FOUNDATION_EXPORT NSString * const NSThreadWillExitNotification;

@interface NSObject (NSThreadPerformAdditions)

- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(id)arg waitUntilDone:(BOOL)wait;
	// equivalent to the first method with kCFRunLoopCommonModes

- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait modes:(NSArray *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
	// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg NS_AVAILABLE(10_5, 2_0);

@end

常用的方法有

// 新开一个线程
[[NSThread alloc] initWithTarget:self selector:@selector(threadTest) object:nil];

// 启动线程
[thread start];

// 类方法创建一个线程
[NSThread detachNewThreadSelector:@selector(threadTest) toTarget:self withObject:nil];

// 隐式创建一个线程
[self performSelectorInBackground:@selector(threadTest) withObject:nil];

// 获取当前线程
[NSThread currentThread];

// 获取主线程
[NSThread mainThread];

// 当前线程暂停两秒
[NSThread sleepForTimeInterval:2.0];
// 或者
NSDate *date = [NSDate dateWithTimeInterval:2 sinceDate:[NSDate date]];
[NSThread sleepUntilDate:date];

// 在主线程上执行一个操作
[self performSelectorOnMainThread:@selector(threadTest) withObject:nil waitUntilDone:YES];

// 在指定线程上执行一个操作
[self performSelector:@selector(threadTest) onThread:thread withObject:nil waitUntilDone:YES];

// 在当前线程执行一个操作
[self performSelector:@selector(threadTest) withObject:nil];

所以新建一个线程、执行一些操作,都是很简单的,就不上代码了 = =

需要注意的是,主线程会占用 1M 的栈区空间,新开的线程会占用 512K 的栈区空间,新开的线程可以暂停,可以休眠,但是杀不掉,系统自己会回收这个空间

NSOperation

然而并不是怎么想写 ╭(╯^╰)╮。主要是另外两个完全满足我需求了,遂懒得研究别的。

本着认(ren)真(jian)负(bu)责(chai)的态度,顺手复制点吧 = =,没有亲测,可不可靠自行测试!

下面来自李明杰老师的博客。

NSOperation

1.简介

NSOperation 实例封装了需要执行的操作和执行操作所需的数据,并且能够以并发或非并发的方式执行这个操作。

NSOperation 本身是抽象基类,因此必须使用它的子类,使用 NSOperation 子类的方式有 2 种:

1> Foundation 框架提供了两个具体子类直接供我们使用:NSInvocationOperation 和 NSBlockOperation

2> 自定义子类继承 NSOperation,实现内部相应的方法

2.执行操作

NSOperation 调用 start 方法即可开始执行操作,NSOperation 对象默认按同步方式执行,也就是在调用 start 方法的那个线程中直接执行。NSOperation 对象的 isConcurrent 方法会告诉我们这个操作相对于调用 start 方法的线程,是同步还是异步执行。isConcurrent 方法默认返回 NO,表示操作与调用线程同步执行。

3.取消操作

operation 开始执行之后,默认会一直执行操作直到完成,我们也可以调用 cancel 方法中途取消操作

[operation cancel];

4.监听操作的执行

如果我们想在一个 NSOperation 执行完毕后做一些事情,就调用 NSOperation 的 setCompletionBlock 方法来设置想做的事情。

operation.completionBlock = ^() {
    NSLog(@"执行完毕");
};

// 或者
[operation setCompletionBlock:^() {
    NSLog(@"执行完毕");
}];

NSInvocationOperation

1.简介

基于一个对象和 selector 来创建操作。如果你已经有现有的方法来执行需要的任务,就可以使用这个类

2.创建并执行操作

// 这个操作是:调用 self 的 run 方法
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(run) object:nil];
// 开始执行任务(同步执行)
[operation start];

NSBlockOperation

1.简介

能够并发地执行一个或多个 block 对象,所有相关的 block 都执行完之后,操作才算完成

2.创建并执行操作

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){  
        NSLog(@"执行了一个新的操作,线程:%@", [NSThread currentThread]);
}];
// 开始执行任务(这里还是同步执行)
[operation start];

3.通过 addExecutionBlock 方法添加 block 操作

NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^(){  
    NSLog(@"执行第1次操作,线程:%@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^() {
    NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^() {
    NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]);
}];

[operation addExecutionBlock:^() {
    NSLog(@"又执行了1个新的操作,线程:%@", [NSThread currentThread]);
}];

// 开始执行任务
[operation start];

打印信息如下:

2013-02-02 21:38:46.102 thread[4602:c07] 又执行了1个新的操作,线程:<NSThread: 0x7121d50>{name = (null), num = 1}
2013-02-02 21:38:46.102 thread[4602:3f03] 又执行了1个新的操作,线程:<NSThread: 0x742e1d0>{name = (null), num = 5}
2013-02-02 21:38:46.102 thread[4602:1b03] 执行第1次操作,线程:<NSThread: 0x742de50>{name = (null), num = 3}
2013-02-02 21:38:46.102 thread[4602:1303] 又执行了1个新的操作,线程:<NSThread: 0x7157bf0>{name = (null), num = 4}

可以看出,这 4 个 block 是并发执行的,也就是在不同线程中执行的,num 属性可以看成是线程的 id。

自定义NSOperation

1.简介

如果 NSInvocationOperation 和 NSBlockOperation 对象不能满足需求,你可以直接继承 NSOperation,并添加任何你想要的行为。继承所需的工作量主要取决于你要实现非并发还是并发的 NSOperation。定义非并发的 NSOperation 要简单许多,只需要重载 -(void)main 这个方法,在这个方法里面执行主任务,并正确地响应取消事件;对于并发 NSOperation,你必须重写 NSOperation 的多个基本方法进行实现(这里暂时先介绍非并发的 NSOperation)。

2.非并发的 NSOperation

比如叫做 DownloadOperation,用来下载图片

1> 继承 NSOperation,重写 main 方法,执行主任务

DownloadOperation.h

#import <Foundation/Foundation.h>
@protocol DownloadOperationDelegate;

@interface DownloadOperation : NSOperation
// 图片的url路径
@property (nonatomic, copy) NSString *imageUrl;
// 代理
@property (nonatomic, retain) id<DownloadOperationDelegate> delegate;

- (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate;
@end

// 图片下载的协议
@protocol DownloadOperationDelegate <NSObject>
- (void)downloadFinishWithImage:(UIImage *)image;
@end

DownloadOperation.m

#import "DownloadOperation.h"

@implementation DownloadOperation
@synthesize delegate = _delegate;
@synthesize imageUrl = _imageUrl;

// 初始化
- (id)initWithUrl:(NSString *)url delegate:(id<DownloadOperationDelegate>)delegate {
    if (self = [super init]) {
        self.imageUrl = url;
        self.delegate = delegate;
    }
    return self;
}
// 释放内存  
- (void)dealloc {
    [super dealloc];
    [_delegate release];
    [_imageUrl release];
}  

// 执行主任务
- (void)main {
    // 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
    @autoreleasepool {
        // ....
    }
}
@end

2> 正确响应取消事件

operation 开始执行之后,会一直执行任务直到完成,或者显式地取消操作。取消可能发生在任何时候,甚至在 operation 执行之前。尽管 NSOperation 提供了一个方法,让应用取消一个操作,但是识别出取消事件则是我们自己的事情。如果 operation 直接终止,可能无法回收所有已分配的内存或资源。因此 operation 对象需要检测取消事件,并优雅地退出执行。

NSOperation 对象需要定期地调用 isCancelled 方法检测操作是否已经被取消,如果返回 YES (表示已取消),则立即退出执行。不管是自定义 NSOperation 子类,还是使用系统提供的两个具体子类,都需要支持取消。isCancelled 方法本身非常轻量,可以频繁地调用而不产生大的性能损失。

以下地方可能需要调用isCancelled:

  • 在执行任何实际的工作之前
  • 在循环的每次迭代过程中,如果每个迭代相对较长可能需要调用多次
  • 代码中相对比较容易中止操作的任何地方

DownloadOperation 的 main 方法实现如下

- (void)main {
    // 新建一个自动释放池,如果是异步执行操作,那么将无法访问到主线程的自动释放池
    @autoreleasepool {
        if (self.isCancelled) return;
        
        // 获取图片数据
        NSURL *url = [NSURL URLWithString:self.imageUrl];
        NSData *imageData = [NSData dataWithContentsOfURL:url];
        
        if (self.isCancelled) {
            url = nil;
            imageData = nil;
            return;
        }
        
        // 初始化图片
        UIImage *image = [UIImage imageWithData:imageData];
        
        if (self.isCancelled) {
            image = nil;
            return;
        }
        
        if ([self.delegate respondsToSelector:@selector(downloadFinishWithImage:)]) {
            // 把图片数据传回到主线程
            [(NSObject *)self.delegate performSelectorOnMainThread:@selector(downloadFinishWithImage:) withObject:image waitUntilDone:NO];
        }
    }
}

GCD

文章实在太长了,就另外开了一篇,点击 iOS 多线程编程 GCD

Tags

Jie Li

🚘 On-road / 📉 US Stock / 💻 Full Stack Developer / 🎓 Grad Student / ®️ ENTJ

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.