NSRunLoop的退出方式

来源:互联网 发布:2016中国网络暴力数据 编辑:程序博客网 时间:2024/06/16 10:03

点击上方“iOS开发”,选择“置顶公众号”

关键时刻,第一时间送达!


1.启动RunLoop


通过[NSRunLoop currentRunLoop]或者CFRunLoopGetCurrent()方式可以获取当前线程的runloop。根据苹果文档,启动一个runloop有以下三种方法:


- (void)run;  

- (void)runUntilDate:(NSDate *)limitDate;

- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;


这三种方式无论通过哪一种方式启动runloop,如果没有一个输入源或者timer附加于runloop上,runloop就会立刻退出。


(1) 使用第一种启动方式,runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;

(2) 使用第二种启动方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;

(3) 使用第三种启动方式,runloop会运行一次,超时时间到达或者第一个input source被处理,则runloop就会退出。


查看苹果文档得知,前两种启动方式会重复调用runMode:beforeDate:方法。感兴趣的童鞋可以进行验证,验证后你就会发现,其实是多此一举。然而“明知山有虎,偏向虎山行”,首先想到的方法是新建个NSRunLoop子类,然后在子类中重写runMode:beforeDate:方法,如果你这么做了,会发现没啥卵用。原因是上述三个方法都是在名叫NSRunLoopConveniences分类中实现的,所以只好新建一个分类通过交换方法实现进行验证,代码如下:


NSMachPort *_port; //global

NSRunLoop *_theRL;

NSThread *_thread;


- (void)viewDidLoad {

    [super viewDidLoad];

    _thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunLoopInNewThread) object:nil];

    [_thread setName:@"com.xindong.thread"];

    [_thread start];

}


- (void)createRunLoopInNewThread {

    _theRL = [NSRunLoop currentRunLoop];

    _port = (NSMachPort *)[NSMachPort port];

    // 添加一个端口作为输入源

    [_theRL addPort:_port forMode:NSDefaultRunLoopMode];


    [_theRL run];

//    [_theRL runUntilDate:[NSDate distantFuture]];

//    [_theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];

}


@implementation NSRunLoop (Hook)


+ (void)load {

    static dispatch_once_t onceToken;

    dispatch_once(&onceToken, ^{

        [self _swizzleImpWithOrigin:@selector(runMode:beforeDate:) swizzle:@selector(xd_runMode:beforeDate:)];

    });

}


+ (void)_swizzleImpWithOrigin:(SEL)originSelector swizzle:(SEL)swizzleSelector {


    Class _class = [self class];

    Method originMethod = class_getInstanceMethod(_class, originSelector);

    Method swizzleMethod = class_getInstanceMethod(_class, swizzleSelector);


    IMP originIMP = method_getImplementation(originMethod);

    IMP swizzleIMP = method_getImplementation(swizzleMethod);


    BOOL add = class_addMethod(_class, originSelector, swizzleIMP, method_getTypeEncoding(swizzleMethod));


    if (add) {

        class_addMethod(_class, swizzleSelector, originIMP, method_getTypeEncoding(originMethod));

    } else {

        method_exchangeImplementations(originMethod, swizzleMethod);

    }

}


- (BOOL)xd_runMode:(NSRunLoopMode)mode beforeDate:(NSDate *)limitDate {


    NSThread *thread = [NSThread currentThread];


    // 这里我们只对自己创建的线程runloop的`runMode:beforeDate:`方法进行修改.

    if ([thread.name isEqualToString:@"com.xindong.thread"]) {

        NSLog(@"runloop+hook: com.xindong.thread线程 ");

        return YES; //如果这里返回`NO`, runloop会立刻退出, 故要返回`YES`进行验证.

    }


    NSLog(@"runloop+hook: 其他可能未知的线程%@ ", thread.name);

    return [self xd_runMode:mode beforeDate:limitDate];

}


@end


结果如下:


控制台.png

线程栈.png


OK,验证通过。简单了解runloop的三种启动方式之后,那么如何能够正确地退出runloop呢?


2.退出RunLoop



目前我想到以下几种可以尝试的方法:(1) 移除input sources或者timer;(2) 设置超时时间或者添加一个定时源;(3) 强制退出线程;(4) 通过方法CFRunLoopStop来停止runloop。想知道这四种方式是否都可行,还请继续往下看……


  • 第一种启动方式run


1. 根据文档所说,如果想退出runloop,不应该使用第一种启动方式来启动runloop。但是文档也有说: If no input sources or timers are attached to the run loop, this method exits immediately. 大概意思就是说如果runloop没有input sources或者附加的timer,runloop就会退出,那么我们是不是就可以在runloop启动之后,通过移除input sources或者timer来退出runloop呢?我们来试下,代码如下:


NSMachPort *_port; //global

NSRunLoop *_theRL;

NSThread *_thread;


- (void)viewDidLoad {

   // 开启一个新线程

   _thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunLoopInNewThread) object:nil]; 

   [_thread setName:@"com.xindong.thread"];

   [_thread start];

}


- (void)createRunLoopInNewThread {

    _theRL = [NSRunLoop currentRunLoop];

    _port = (NSMachPort *)[NSMachPort port];

    // 给runloop添加一个端口作为输入源,保证可以随时处理消息

    [_theRL addPort:_port forMode:NSDefaultRunLoopMode];


    // 该方法会在当前线程的runloop中创建一个timer,并在当前线程中执行selector

    [self performSelector:@selector(excuteInNewThread:) withObject:@"param1" afterDelay:2];

    [self performSelector:@selector(excuteInNewThread:) withObject:@"param2" afterDelay:3];


    [_theRL run];


    // 如果当前线程的runloop没有退出,则`[_theRL run]`之后的代码不会执行. 

    NSLog(@"runloop已退出"); //只有当runloop退出,这里才会执行。可以通过注册runloop观察者进行验证,这里就不贴代码了,具体代码请到demo里查看。

}


- (void)excuteInNewThread:(NSString *)param {

    NSLog(@"%@", param);

    // 将当前线程的runloop中的port移除

    [_theRL removePort:_port forMode:NSDefaultRunLoopMode];

}


控制台输出结果:



注:excuteInNewThread:方法已执行,表明runloop已成功开启,否则该方法不会执行。


在excuteInNewThread:方法中,将port从当前runloop中移除,在两个事件param1和param2处理完之后 (即两个timer触发完毕),runloop就退出了。虽然可以将runloop退出,但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。


2. 我们知道,runloop接收输入事件来自两种不同的来源:输入源 (input source)  定时源 (timer source)。输入源传递异步事件,通常消息来自于其他线程或程序。定时源则传递同步事件,发生在特定时间或者重复的时间间隔。由于run启动方式不能设置超时时间,所以我们在runloop中添加一个定时源进行测试,对上述代码稍作修改:


- (void)createRunLoopInNewThread {

    _theRL = [NSRunLoop currentRunLoop];


    // 注意:这里repeats参数要设为`NO`.

    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.5 repeats:NO block:^(NSTimer * _Nonnull timer) {

        NSLog(@"timer block");

    }];

    // 添加一个定时源 

    [_theRL addTimer:timer forMode:NSDefaultRunLoopMode];


    [_theRL run];


    // 如果当前线程的runloop没有退出,则`[_theRL run]`之后的代码不会执行. 

    NSLog(@"runloop已退出"); //只有当runloop退出,这里才会执行。可以通过注册runloop观察者进行验证,这里就不贴代码了,具体代码请到demo里查看。

}


控制台输出结果如下:



timer触发后,由于runloop没有可监视的源,故runloop就会退出。如果不想让runloop退出,只需要将创建timer的repeats参数设置为YES即可。但是这样设置的话,有个明显的缺点就是:每隔1.5s runloop就会从休眠中唤醒,从而会引起CPU资源和系统内存不必要的消耗。通过注册runloop观察者在控制台进行打印查看:



3. 我们再试试通过强制退出线程的方法,看看会不会让当前线程的runloop也跟着退出?在excuteInNewThread:方法中稍作修改,代码如下:


- (void)createRunLoopInNewThread {

    _theRL = [NSRunLoop currentRunLoop];

    _port = (NSMachPort *)[NSMachPort port];

    // 给runloop添加一个端口作为输入源,保证可以随时处理消息

    [_theRL addPort:_port forMode:NSDefaultRunLoopMode];


    // 该方法会在当前线程的runloop中创建一个timer,并在当前线程中执行selector

    [self performSelector:@selector(excuteInNewThread:) withObject:@"param1" afterDelay:2];

    [self performSelector:@selector(excuteInNewThread:) withObject:@"param2" afterDelay:3];


    [_theRL run];


    // 如果当前线程的runloop没有退出,则`[_theRL run]`之后的代码不会执行. 

    NSLog(@"runloop已退出"); //只有当runloop退出,这里才会执行。可以通过注册runloop观察者进行验证,这里就不贴代码了,具体代码请到demo里查看。

}

- (void)excuteInNewThread:(NSString *)param {

    NSLog(@"%@", param);

    [NSThread exit];

}


控制台输出结果:



我们发现,只输出了param1,说明在第一次执行excuteInNewThread:方法时,当前线程确已退出,所以param2不会输出,但是NSLog(@"runloop已退出");却没有执行,说明当前线程的runloop并没有退出,再加上线程栈上的一些资源没有释放,从而引起了内存泄漏,如下图。所以,此方法并不可取。



4. 至此,剩下最后一种的退出方式就是Core Foundation下的CFRunLoopStop函数。那么使用CFRunLoopStop能否退出runloop呢?我们对代码稍作修改,进行测试:


- (void)createRunLoopInNewThread {


    // 注册runloop观察者

    static CFRunLoopObserverRef _observer;

    RegisterRunLoopObserver(kCFRunLoopAllActivities, _observer, 0, kCFRunLoopDefaultMode, (__bridge void*)self, RunLoopCallBack);


    _theRL = [NSRunLoop currentRunLoop];


    _port = (NSMachPort *)[NSMachPort port];

    [_theRL addPort:_port forMode:NSDefaultRunLoopMode];


    [self performSelector:@selector(excuteInNewThread:) withObject:@"param1" afterDelay:2];

    [self performSelector:@selector(excuteInNewThread:) withObject:@"param2" afterDelay:3];


    [_theRL run];


    // 如果当前线程的runloop没有退出,则`[_theRL run]`之后的代码不会执行.

    NSLog(@"runloop已退出"); //只有当runloop退出,这里才会执行。可以通过注册runloop观察者进行验证,这里就不贴代码了,具体代码请到demo里查看。

}


- (void)excuteInNewThread:(NSString *)param {

    NSLog(@"%@", param);

    CFRunLoopStop(CFRunLoopGetCurrent());

}


控制台输出结果:



通过runloop观察者的回调得知,每次执行完CFRunLoopStop方法时,runloop确实退出了,但是由于run这种启动方式会重复调用runMode:beforeDate:方法,从而在退出当前runloop之后又会重新启动runloop,所以上述代码中的NSLog(@"runloop已退出")方法也就不会执行。同理,通过第二种方式runUntilDate:启动runloop,也不能使用CFRunLoopStop方法来退出。


  • 第二种启动方式runUntilDate:

通过这种方式启动,可以通过设置超时时间来退出runloop。除此之外,跟第一种启动方式run类似,这里不再赘述。


  • 第三种启动方式runMode:beforeDate:

通过这种方式启动,runloop会运行一次,当超时时间到达或者第一个输入源被处理,runloop就会退出。测试代码如下:


- (void)createRunLoopInNewThread {

    // 注册runloop观察者

    static CFRunLoopObserverRef _observer;

    RegisterRunLoopObserver(kCFRunLoopAllActivities, _observer, 0, kCFRunLoopDefaultMode, (__bridge void*)self, RunLoopCallBack);


    _theRL = [NSRunLoop currentRunLoop];


    _port = (NSMachPort *)[NSMachPort port];

    [_theRL addPort:_port forMode:NSDefaultRunLoopMode];


    [_theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];


    // 如果当前线程的runloop没有退出,则`[_theRL run]`之后的代码不会执行.

    NSLog(@"runloop已退出"); //只有当runloop退出,这里才会执行。可以通过注册runloop观察者进行验证,这里就不贴代码了,具体代码请到demo里查看。

}


#pragma mark - Touch


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    UIColor *color = self.view.backgroundColor;

    self.view.backgroundColor = color == [UIColor redColor] ? [UIColor yellowColor] : [UIColor redColor];


    // 线程间的通信 (这里是main thread)

    [self performSelector:@selector(communicateToNewThreadFromMainThread) onThread:_thread withObject:nil waitUntilDone:NO];

}


- (void)communicateToNewThreadFromMainThread {

    NSLog(@"communicate successfully "); //这里是com.xindong.thread. 这里执行完,表示第一个输入源事件被处理.

}


控制台输出如下:



当我们触摸屏幕时,communicateToNewThreadFromMainThread方法被执行,即输入源事件被处理,然后runloop退出。如果我们想控制runloop的退出时机,而不是在处理完一个输入源事件之后就退出,那么就要重复调用runMode:beforeDate:,具体可以参考苹果文档给出的方案,如下:


BOOL shouldKeepRunning = YES; // global

NSRunLoop *theRL = [NSRunLoop currentRunLoop];

while (shouldKeepRunning && [theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);


接着我们对代码进行修改,以便我们可以控制runloop的退出时机,改后如下:


- (void)createRunLoopInNewThread {

    // 注册runloop观察者

    static CFRunLoopObserverRef _observer;

    RegisterRunLoopObserver(kCFRunLoopAllActivities, _observer, 0, kCFRunLoopDefaultMode, (__bridge void*)self, RunLoopCallBack);


    _theRL = [NSRunLoop currentRunLoop];


    _port = (NSMachPort *)[NSMachPort port];

    [_theRL addPort:_port forMode:NSDefaultRunLoopMode];


    while (shouldKeepRunning && [_theRL runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]]);


    // 如果当前线程的runloop没有退出,则`[_theRL run]`之后的代码不会执行.

    NSLog(@"runloop已退出"); //只有当runloop退出,这里才会执行。可以通过注册runloop观察者进行验证,这里就不贴代码了,具体代码请到demo里查看。   

}


#pragma mark - Touch


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {

    UIColor *color = self.view.backgroundColor;

    self.view.backgroundColor = color == [UIColor redColor] ? [UIColor yellowColor] : [UIColor redColor];


    // 线程间的通信 (这里是main thread)

    [self performSelector:@selector(communicateToNewThreadFromMainThread) onThread:_thread withObject:nil waitUntilDone:NO];

}


- (void)communicateToNewThreadFromMainThread {

    NSLog(@"communicate successfully "); //这里是com.xindong.thread

    [self quitRunLoop];

}


- (void)quitRunLoop {

    shouldKeepRunning = NO;

    CFRunLoopStop(CFRunLoopGetCurrent());

}


通过上述方式启动和退出runloop,没有引起内存泄漏,也没有造成内存增长,并且对runloop的退出时机可以自由控制。相对来说,使用此方案更好一些。



3. 总结



如果不想退出runloop可以使用第一种方式启动runloop;如果使用第二种方式启动runloop,可以通过设置超时时间来退出;如果使用第三种方式启动runloop,可以通过设置超时时间或者使用CFRunLoopStop方法来退出。


文章中若有不对之处,还望各位朋友不吝指正。毕竟能力水平有限,不敢保证准确无误。


参考资料:


https://bestswifter.com/runloop-and-thread/

http://blog.ibireme.com/2015/05/18/runloop/

http://blog.csdn.net/yxh265/article/details/51483822

http://www.dreamingwish.com/frontui/article/default/ios-multithread-program-runloop-the.html


  • 链接:http://mp.weixin.qq.com/s/6fawtC6u-yMQvqUOvXrvOg

  • iOS开发整理发布,转载请联系作者授权

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 犯罪嫌疑人死在看守所怎么办 犯罪嫌疑人死不承认怎么办 高血压进了看守所怎么办 看守所里被欺负怎么办 老师上课迟到了怎么办 艾滋病看守所不收怎么办 没能力偿还债务怎么办 监狱病人的癌症怎么办 监狱的传染病人怎么办 犯人在监狱重病怎么办 亲戚被关拘留所怎么办 打麻将被拘留怎么办 轻伤检察院不批捕怎么办 吸毒人被拘留怎么办 法院拘留没去怎么办 羁押期限超过了怎么办 羁押期限已满怎么办 刑事拘留22天了怎么办 被派出所拘留 车怎么办 被公安怀疑贩毒怎么办 报案后证据不足怎么办 担心丈夫复嫖怎么办 交通事故当事人重伤笔录怎么办 交通事故做完笔录后怎么办 罚款单子丢了怎么办 刑事案件人跑了怎么办 打架当事人跑了怎么办 赌博被派出所抓怎么办 治安案件人跑了怎么办 去警察局做笔录怎么办 未成年打架留下案底怎么办 土地被别人侵占怎么办 在公安局有案底怎么办 做小姐有案底怎么办 党员被黑社会打怎么办 周期内被扣15分怎么办 驾证扣了50分怎么办? 酒驾拘留工作怎么办 开电动车被拘留怎么办 有一次吸毒案底怎么办 家里收到拘留书怎么办