NSThread

官方文档: https://developer.apple.com/reference/foundation/nsthread?language=objc

内容:线程的创建、启动、停止、状态,线程环境、属性、优先级,线程通信、线程常驻。

如果需要和线程直接打交道,需要创建常驻线程,可以使用 NSThread。创建线程子类需要重写 main 函数,不需要调用 super。

1、创建

// 指定初始化函数。
- (instancetype)init;

// selector:不能有返回值,最多只有一个参数。
// 注意:target 和 argument 会被强引用,只有线程 exits 后才会释放。
// 线程的 main 函数会自动调用 target 的 selector。
- (instancetype)initWithTarget:(id)target selector:(SEL)selector object:(id)argument;

// iOS 10.0
- (instancetype)initWithBlock:(void (^)(void))block;

2、启动线程

// 创建并启动线程,selector 作为线程的入口点。
// target 和 argument 会被强引用,只有线程 exits 后才会释放。
// target 执行完 selector,线程就会退出。
// 线程的 main 函数会自动调用 target 的 selector。
+ (void)detachNewThreadSelector:(SEL)selector toTarget:(id)target withObject:(id)argument;

// iOS 10.0
+ (void)detachNewThreadWithBlock:(void (^)(void))block;

// 启动线程。异步调用线程的 main 函数。
// 如果线程在创建的时候,设置了 target 和 selector,main 函数会自动调用。
- (void)start;

// 线程的入口。不要直接调用,调用 start 来启动线程。
// 如果线程在创建的时候,设置了 target 和 selector,main 函数会自动调用。
// 子类重写,定义线程要完成的任务,不需要调用 super。
- (void)main;

3、停止线程

// 结束当前线程,会先发送 NSThreadWillExitNotification 通知。
// 注意:调用前记得先释放线程申请的资源。
+ (void)exit;

// 标记为取消,不会立即结束线程。
// 支持取消的线程,会周期检查 cancelled 属性,来判断是否需要 exit。
- (void)cancel;

// 阻塞当前线程
+ (void)sleepUntilDate:(NSDate *)date;
+ (void)sleepForTimeInterval:(NSTimeInterval)ti;

4、线程状态

// 是否正在运行
@property(readonly, getter=isExecuting) BOOL executing;

// 是否运行完成
@property(readonly, getter=isFinished) BOOL finished;

// 是否取消了。
// 支持取消的线程,会周期检查 cancelled 属性,来判断是否需要 exit。
@property(readonly, getter=isCancelled) BOOL cancelled;

5、线程环境

// 是否主线程
@property(class, readonly) BOOL isMainThread;

// 是否多线程。
// If you detached a thread in your application using a 
// non-Cocoa API, such as the POSIX or Multiprocessing Services
//  APIs, this method could still return NO. 
+ (BOOL)isMultiThreaded;

// 获取主线程
@property(class, readonly, strong) NSThread *mainThread;

// 获取当前线程
@property(class, readonly, strong) NSThread *currentThread;

// 调用栈地址
@property(class, readonly, copy) NSArray<NSNumber *> *callStackReturnAddresses;

// 调用栈,崩溃时打印的东西
@property(class, readonly, copy) NSArray<NSString *> *callStackSymbols;

6、线程属性

// 用于存放用户数据,不影响线程操作。
// 比如存放线程的默认 NSConnection 或 NSAssertionHandler 对象。
@property(readonly, retain) NSMutableDictionary *threadDictionary;

// 线程名字
@property(copy) NSString *name;

// stack 的大小,以 bytes 为单位,必须是 4KB 的倍数。
// 在调用 start 之前设置,否则无效。
@property NSUInteger stackSize;

7、线程优先级

// 竞争资源的优先级
@property NSQualityOfService qualityOfService;

// 优先级定义
typedef NS_ENUM(NSInteger, NSQualityOfService) {
    NSQualityOfServiceUserInteractive = 0x21,
    NSQualityOfServiceUserInitiated = 0x19,
    NSQualityOfServiceUtility = 0x11,
    NSQualityOfServiceBackground = 0x09,
    NSQualityOfServiceDefault = -1
} API_AVAILABLE(macos(10.10), ios(8.0), watchos(2.0), tvos(9.0));

// 用于 UI 交互,比如处理控件事件和屏幕绘制
NSQualityOfServiceUserInteractive

// 用于用户发起的请求,而且结果必须立即反馈给用户继续交互的任务。
// 比如用户点击列表触发的数据加载。
NSQualityOfServiceUserInitiated

// 用于用户不需要立即得到结果的任务。
// 比如大量文件操作,media 导入等。
NSQualityOfServiceUtility

// 用于用户不可见、无需用户察觉的任务。
// 比如数据备份、内容预加载。
NSQualityOfServiceBackground

// 表示没有定义优先级。
NSQualityOfServiceDefault

// 即将废弃,使用 qualityOfService 代替。
// 0.0 ~ 1.0,最高 1.0。
@property double threadPriority;
+ (double)threadPriority;
+ (BOOL)setThreadPriority:(double)p;

8、线程通信

8.1
// 主线程在 NSDefaultRunLoopMode 调用 aSelector。
// aSelector:没有返回值,形参最多有 1 个。
// wait:YES 会阻塞当前线程,直到主线程执行完 aSelector。NO 会立即返回。
// 如果当前线程就是主线程,wait 是 YES,aSelector 会立即执行。
// 无法取消,想要取消要调用带有 afterDelay 的函数。比如 performSelector:withObject:afterDelay:。
- (void)performSelectorOnMainThread:(SEL)aSelector 
   withObject:(id)arg 
   waitUntilDone:(BOOL)wait;

8.2
// array:aSelector 可以在哪些模式下被调用,至少要有一个值,否则不会被调用。
// 可以是:NSRunLoopCommonModes、NSDefaultRunLoopMode、UITrackingRunLoopMode。
// NSRunLoopMode 其实就是 NSString。
- (void)performSelectorOnMainThread:(SEL)aSelector 
   withObject:(id)arg 
   waitUntilDone:(BOOL)wait 
   modes:(NSArray<NSString *> *)array;

8.3
// wait:YES 会阻塞当前线程,直到目标线程执行完 aSelector。NO 会立即返回。
// 如果当前线程和目标线程相同,wait 是 YES,aSelector会立即执行。
- (void)performSelector:(SEL)aSelector 
   onThread:(NSThread *)thr 
   withObject:(id)arg 
   waitUntilDone:(BOOL)wait;

8.4
// array:aSelector 可以在哪些模式下被调用,至少要有一个值,否则不会被调用。
// 可以是:NSRunLoopCommonModes、NSDefaultRunLoopMode、UITrackingRunLoopMode。
// NSRunLoopMode 其实就是 NSString。
- (void)performSelector:(SEL)aSelector 
    onThread:(NSThread *)thr 
    withObject:(id)arg 
    waitUntilDone:(BOOL)wait 
    modes:(NSArray<NSString *> *)array;

8.5
// Invokes a method of the receiver on a new background thread.
// The method represented by aSelector must set up the thread environment 
// just as you would for any other new thread in your program.
// For more information about how to configure and run threads, see Threading Programming Guide.
- (void)performSelectorInBackground:(SEL)aSelector withObject:(id)arg;

8.6
// timer 在当前线程的 NSDefaultRunLoopMode 才会 fire。
// This method sets up a timer to perform the aSelector message on the current thread’s run loop. 
// The timer is configured to run in the default mode (NSDefaultRunLoopMode). 
// When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. 
// It succeeds if the run loop is running and in the default mode; 
// otherwise, the timer waits until the run loop is in the default mode.
- (void)performSelector:(SEL)aSelector 
     withObject:(id)anArgument 
     afterDelay:(NSTimeInterval)delay;
8.7
// array:aSelector 可以在哪些模式下被调用,至少要有一个值,否则不会被调用。
// 可以是:NSRunLoopCommonModes、NSDefaultRunLoopMode、UITrackingRunLoopMode。
- (void)performSelector:(SEL)aSelector 
     withObject:(id)anArgument 
     afterDelay:(NSTimeInterval)delay 
     inModes:(NSArray<NSRunLoopMode> *)modes;
8.8
// Cancels perform requests previously registered with the performSelector:withObject:afterDelay: instance method.
// All perform requests having the same target aTarget are canceled. 
// This method removes perform requests only in the current run loop, not all run loops.
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget 
     selector:(SEL)aSelector 
     object:(id)anArgument;

注意:
线程通信的 performSelector 系列函数,都有这段文字说明,不知道是不是文档的 bug。我的理解是,这些函数的正确工作,依赖于线程的 runloop,而 dispatch queue 的线程可能没有 runloop,比如在 dispatch_async 里面调用 performSelector,aSelector 可能不会被调用。如果要在 dispatch queue 延迟调用某函数,可以用 dispatch_after 系列函数。

This method registers with the runloop of its current context, and depends on that runloop being run on a regular basis to perform correctly. One common context where you might call this method and end up registering with a runloop that is not automatically run on a regular basis is when being invoked by a dispatch queue. If you need this type of functionality when running on a dispatch queue, you should use dispatch_after and related methods to get the behavior you want.

9、线程常驻

GCD 是从线程池里分配线程的,有可能会枯竭,所以在某些场景创建自己的常驻线程还是有必要的。
步骤是在线程的入口函数,创建自动释放池,创建 runloop,给 runloop 添加输入源或者 timer。最后启动 runloop,如果不需要退出,就调用 run 方法来启动,否则还是调用 runMode:beforeDate: 来启动。要小心强引用导致内存无法释放的问题。

9.1 创建线程

#import "MYViewController.h"

@interface MYViewController ()
@property (nonatomic, strong) NSThread *thread;
@property (nonatomic, assign) BOOL shouldKeepRunning;
@property (nonatomic, assign) CFRunLoopObserverRef observer;
@end

@implementation MYViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.view.backgroundColor = [UIColor whiteColor];
    
    [self createBtn];
    [self createThread];
}

- (void)createThread {
    // 创建线程。注意,会被强引用,线程 exit 后才会释放。
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(entryPoint) object:nil];
    self.thread.name = @"子线程";
    
    // 启动线程
    [self.thread start];
}

- (void)entryPoint {
    // 创建自动释放池,否则会有内存泄露。
    @autoreleasepool {
        
        // 创建 runloop。获取的时候系统会自动创建。
        NSRunLoop *loop = [NSRunLoop currentRunLoop];
        
        // 添加输入源,否则 runloop 会直接退出。
        [loop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        
        // 添加观察者,观察 runloop 的状态
        [self addObserver];
        
        
        // 启动 runloop。两种方式。
        
        // 1、调用 run 方法启动有个问题:很难停下来。
        // 手动移除输入源或 timer 也没用,系统可能会添加,导致无法退出 runloop。
        // 如果没有输入源或 timer,会立即返回,否则 不会 往下执行。
//        [loop run];
        
        
        // 2、这里换个方法启动。想停下来,shouldKeepRunning 设置为 NO 就行了。
        self.shouldKeepRunning = YES;
        while (self.shouldKeepRunning && 
              [loop runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:5]]) {
            // 这里不会一直执行,runMode 函数返回才会执行
            // 比如在 performSelector 的函数执行后,runMode 函数才会返回
            // 或者超时了,runMode 函数也会返回
            static int i;
            NSLog(@"这里是循环 %d", i++);
        }
        NSLog(@"这里还执行吗?上面的循环结束了才执行");
    }
}

9.2 

#pragma mark - 退出

- (void)viewDidDisappear:(BOOL)animated {
    [super viewDidDisappear:animated];
    
    // 退出 runloop
    
    // 1、如果是调用 runMode:beforeDate: 启动 runloop,这样就能退出了
    self.shouldKeepRunning = NO;

    // 在子线程移除观察者,否则控制器无法释放。
    [self performSelector:@selector(removeMyObserver) onThread:self.thread withObject:nil waitUntilDone:NO];

    
    // 2、如果是调用 run 方法启动 runloop,很难退出,还会导致无法释放的问题。
    
//    [self.thread cancel]; // cancel 不能退出 runloop。
//    self.thread = nil; // 也不能退出 runloop。
    
    // 可以释放控制器。runloop 没退出,直接干掉了。线程也被干掉了。
    // 可是为什么不会崩溃?performSelector 是实例方法啊。。。
    // 直接在子线程调用 exit,可以退出线程但无法释放控制器,为什么呢?
    [NSThread performSelector:@selector(exit) onThread:self.thread withObject:nil waitUntilDone:NO];
//    [NSThread.class performSelector:@selector(exit) onThread:self.thread withObject:nil waitUntilDone:NO]; // 结果同上
    
    // 会崩溃。提示 +exit 是 unrecognized selector。
//    [self.thread performSelector:@selector(exit) onThread:self.thread withObject:nil waitUntilDone:NO];
    
    // 线程被干掉了,但是没释放控制器
//    [self performSelector:@selector(exitMyThread) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)exitMyThread {
    [self.thread cancel]; // 子线程还在
    self.thread = nil; // 子线程还在
    
//    CFRunLoopStop([[NSRunLoop currentRunLoop] getCFRunLoop]); // 会退出当前循环,然后重新进入循环。。。

    // 子线程退出了,但控制器没有释放。想不通,有大神指点吗?
    [NSThread exit];
    
    // 下面的代码不会执行了,因为线程已经退出了
    NSLog(@"这句代码不会执行了,因为线程已经退出了");
}

- (void)dealloc
{
    NSLog(@"MYViewController 释放了");
}

9.3 观察

#pragma mark - 观察

- (void)addObserver {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"%@: %@", [NSThread currentThread].name, [self getStateWithActivity:activity]);
    });
    
    CFRunLoopAddObserver(runloop, observer, kCFRunLoopDefaultMode);
    self.observer = observer;
}

- (void)removeMyObserver {
    if (self.observer) {
        CFRunLoopRef runloop = CFRunLoopGetCurrent();
        CFRunLoopRemoveObserver(runloop, self.observer, kCFRunLoopDefaultMode);
        CFRelease(self.observer);
        self.observer = nil;
    }
}

- (NSString *)getStateWithActivity:(CFRunLoopActivity) activity {
    NSString *state = @"未知";
    
    switch (activity) {
        case kCFRunLoopEntry: {
            state = @"进入";
            break;
        }
            
        case kCFRunLoopBeforeTimers: {
            state = @"定时器";
            break;
        }
            
        case kCFRunLoopBeforeSources: {
            state = @"输入源";
            break;
        }
            
        case kCFRunLoopBeforeWaiting: {
            state = @"等待前";
            break;
        }
            
        case kCFRunLoopAfterWaiting: {
            state = @"等待后";
            break;
        }
            
        case kCFRunLoopExit: {
            state = @"退出";
            break;
        }
            
        case kCFRunLoopAllActivities: {
            state = @"所有";
            break;
        }
            
        default:
            break;
    }
    return state;
}

9.4 事件处理

#pragma mark - 事件处理

- (void)createBtn {
    UIButton *btn = [UIButton buttonWithType:UIButtonTypeSystem];
    [btn setTitle:@"点击" forState:UIControlStateNormal];
    btn.frame = CGRectMake(100, 200, 100, 100);
    [self.view addSubview:btn];
    [btn addTarget:self action:@selector(clickBtn) forControlEvents:UIControlEventTouchUpInside];
}

- (void)clickBtn {
    [self performSelector:@selector(log) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void)log {
    NSLog(@"在子线程处理点击事件");
}

如有错误,欢迎指正和交流。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,651评论 6 501
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,468评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,931评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,218评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,234评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,198评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,084评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,926评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,341评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,563评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,731评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,430评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,036评论 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,676评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,829评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,743评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,629评论 2 354

推荐阅读更多精彩内容