IOS多线程编程PART 1/3 – NSTHREAD & RUN LOOP

前言

多线程的价值无需赘述,对于App性能和用户体验都有着至关重要的意义,在iOS开发中,Apple提供了不同的技术支持多线程编程,除了跨平台的pthread之外,还提供了NSThread、NSOperationQueue、GCD等多线程技术,从本篇Blog开始介绍这几种多线程技术的细节。

对于pthread这种跨平台的多线程技术,这本Programming with POSIX Threads做了详细介绍,不再提及。

NSThread
使用NSThead创建线程有很多方法:

  • +detachNewThreadSelector:toTarget:withObject:类方法直接生成一个子线程
[NSThread detachNewThreadSelector:@selector(threadRoutine:) toTarget:self withObject:nil];
  • 创建一个NSThread类实例,然后调用start方法。
NSThread *aThrad = [[NSThread alloc] initWithTarget:self selector:@selector(threadRoutine:) object:nil];
[aThread start];
  • 调用NSObject的+performSelectorInBackground:withObject:方法生成子线程。
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg
  • 创建一个NSThread子类,然后调用子类实例的start方法,。

创建线程也是有开销的,iOS下主要成本包括构造内核数据结构(大约1KB)、栈空间(子线程512KB、主线程1MB,不过可以使用方法-setStackSize:自己设置,注意必须是4K的倍数,而且最小是16K),创建线程大约需要90毫秒的创建时间。

第二种和第四种方法创建的线程有个好处是拥有线程的对象,因此可以使用performSelector:onThread:withObject:waitUntilDone:
在该线程上执行方法,这是一种非常方便的线程间通讯的方法(相对于设置麻烦的NSPort用于通讯),所要执行的方法可以直接添加到目标线程的Runloop中执行。Apple建议使用这个接口运行的方法不要是耗时或者频繁的操作,以免子线程的负载过重。

第三种方法其实与第一种方法是一样的,都会直接生成一个子线程。

NSThread的入口方法

要写一个有效的子线程入口方法需要注意很多问题,示例代码:

- (void)threadRoutine
{ 
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];              BOOL moreWorkToDo = YES;
BOOL exitNow = NO; 
NSRunLoop* runLoop = [NSRunLoop currentRunLoop]; NSMutableDictionary* threadDict = [[NSThread currentThread] threadDictionary]; 
[threadDict setValue:[NSNumber numberWithBool:exitNow] forKey:@"ThreadShouldExitNow"]; 
//添加事件源
 [self myInstallCustomInputSource]; 
while (moreWorkToDo && !exitNow) 
{ //执行线程真正的工作方法,如果完成了可以设置moreWorkToDo为False 
        [runLoop runUntilDate:[NSDate date]]; 
        exitNow = [[threadDict         valueForKey:@"ThreadShouldExitNow"] boolValue];
 } 
[pool release];}
  • 必须创建一个NSAutoreleasePool,因为子线程不会自动创建。同时要注意这个pool因为是最外层pool,如果线程中要进行长时间的操作生成大量autoreleased的对象,则只有在该子线程退出时才会回收,因此如果线程中会大量创建autoreleased对象,那么需要创建额外的NSAutoreleasePool,可以在NSRunloop每次迭代时创建和销毁一个NSAutoreleasePool。

  • 如果你的子线程会抛出异常,最好在子线程中设置一个异常处理函数,因为如果子线程无法处理抛出的异常,会导致程序直接Crash关闭。

  • (可选)设置Run Loop,如果子线程只是做个一次性的操作,那么无需设置Run Loop;如果子线程进入一个循环需要不断处理一些事件,那么设置一个Run Loop是最好的处理方式如果需要Timer,那么Run Loop就是必须的

  • 如果需要在子线程运行的时候让子线程结束操作,子线程每次Run Loop迭代中检查相应的标志位来判断是否还需要继续执行,可以使用threadDictionary以及设置Input Source的方式来通知这个子线程。那么什么是Run Loop呢?这是涉及NSThread及线程相关的编程时无法回避的一个问题。

Run Loop

Run Loop本身并不具备并发执行的功能,但是和多线程开发息息相关,而且概念令人迷惑,相关的介绍资料也很少,它的主要的特性如下:

  • 每个线程都有一个Run Loop,主线程的Run Loop会在App运行时自动运行,子线程中需要手动运行。

  • 每个Run Loop都会以一个模式mode来运行,可以使用NSRunLoop的- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate 方法运行在某个特定模式mode。

  • Run Loop的处理两大类事件源:Timer Source和Input Source(包括performSelector***方法簇、Port或者自定义Input Source),每个事件源都会绑定在Run Loop的某个特定模式mode上,而且只有RunLoop在这个模式运行的时候才会触发该Timer和Input Source。

  • 如果没有任何事件源添加到Run Loop上,Run Loop就会立刻exit。

Run Loop接口

要操作Run Loop,Foundation层和Core Foundation层都有对应的接口可以操作Run Loop。

Foundation层对应的是NSRunLoop:
1.jpg
Core Foundation层对应的是CFRunLoopRef:
2.jpg

两组接口差不多,不过功能上还是有许多区别的,例如CF层可以添加自定义Input Source事件源(CFRunLoopSourceRef)和Run Loop观察者Observer(CFRunLoopObserverRef),很多类似功能的接口特性也是不一样的。

Run Loop运行
- (void)main
{
    @autoreleasepool
    {
        NSLog(@"starting threading ");
        self.timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(doTimerTask) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:self.timer forMode:NSDefaultRunLoopMode];
        while (!self.isCanceled)
        {
            [self doOtherTask];
            
            BOOL ret = [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
             NSLog(@"after runloop counting.........: %d", ret);
            if (!ret)
            {
                _isCanceled = YES;
            }
        }
        NSLog(@"finishing thread.........");
    }
}

- (void)doTimerTask
{
    NSLog(@"do timer task  , %@", [NSThread currentThread]);
}

- (void)doOtherTask
{
    NSLog(@"do other task");
}

我们看到入口方法里创建了一个NSTimer,并且以NSDefaultRunLoopMode模式加入到当前子线程的NSRunLoop中。
进入循环后肯定会执行-doOtherTask方式法一次,然后再以NSDefaultRunLoopMode模式运行NSRunLoop,如果一次Timer事件触发处理后,这个Run Loop会返回吗?答案是不会,Why?

NSRunLoop的底层是由CFRunLoopRef实现的,你可以想象成一个循环或者类似Linux下select或者epoll,当没有事件触发时,你调用的Run Loop运行方法不会立刻返回,它会持续监听其他事件源,如果需要Run Loop会让子线程进入sleep等待状态而不是空转,

只有当Timer Source或者Input Source事件发生时,子线程才会被唤醒,然后处理触发的事件,然而由于Timer source比较特殊,Timer Source事件发生处理后,
**Run Loop运行方法- (BOOL)runMode:(NSString )mode beforeDate:(NSDate )limitDate;也不会返回
而其他非Timer事件的触发处理会让这个Run Loop退出并返回YES。

当Run Loop运行在一个特定模式时,如果该模式下没有事件源,运行Run Loop会立刻返回NO

NSRunLoop的运行接口:

//运行 NSRunLoop,运行模式为默认的NSDefaultRunLoopMode模式,没有超时限制

  • (void)run;

//运行 NSRunLoop: 参数为运行模式、时间期限,返回值为YES表示是处理事件后返回的,NO表示是超时或者停止运行导致返回的- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;

//运行 NSRunLoop: 参数为运时间期限,运行模式为默认的NSDefaultRunLoopMode模式 -(void)runUntilDate:(NSDate *)limitDate;

详细讲解下NSRunLoop的三个运行接口:

    • (void)run; 无条件运行
      不建议使用,因为这个接口会导致Run Loop永久性的运行在NSDefaultRunLoopMode模式,即使使用CFRunLoopStop(runloopRef);也无法停止Run Loop的运行,那么这个子线程就无法停止,只能永久运行下去。
    • (void)runUntilDate:(NSDate *)limitDate;
      有一个超时时间限制

比上面的接口好点,有个超时时间,可以控制每次Run Loop的运行时间,也是运行在NSDefaultRunLoopMode模式。这个方法运行Run Loop一段时间会退出给你检查运行条件的机会,如果需要可以再次运行Run Loop。注意CFRunLoopStop(runloopRef);
也无法停止Run Loop的运行,因此最好自己设置一个合理的Run Loop运行时间。示例:

while (!Done)
{ 
  [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
   NSLog(@"exiting runloop.........:");
}
    • (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
      有一个超时时间限制,而且设置运行模式

这个接口在非Timer事件触发、显式的用CFRunLoopStop停止Run Loop、到达limitDate后会退出返回。如果仅是Timer事件触发并不会让Run Loop退出返回;如果是PerfromSelector***事件或者其他Input Source事件触发处理后,Run Loop会退出返回YES。示例:

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

推荐阅读更多精彩内容