RunLoop 总结:RunLoop的应用场景(一)保证线程长久存活

作者:哈雷哈雷_Wong

1.保证线程的长时间存活

在iOS开发过程中,有时候我们不希望一些花费时间比较长的操作阻塞主线程,导致界面卡顿,那么我们就会创建一个子线程,然后把这些花费时间比较长的操作放在子线程中来处理。可是当子线程中的任务执行完毕后,子线程就会被销毁掉。** 怎么来验证上面这个结论呢?** 首先,我们创建一个HLThread类,继承自NSThread,然后重写dealloc 方法。

@interface HLThread : NSThread


@end


@implementation HLThread


- (void)dealloc

{

NSLog(@"%s",__func__);

}


@end



然后,在控制器中用HLThread创建一个线程,执行一个任务,观察任务执行完毕后,线程是否被销毁。

- (void)viewDidLoad {

[super viewDidLoad];

// 1.测试线程的销毁

[self threadTest];

}


- (void)threadTest

{

HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadOpetion) object:nil];

[subThread start];

}


- (void)subThreadOpetion

{

@autoreleasepool {

NSLog(@"%@----子线程任务开始",[NSThread currentThread]);

[NSThread sleepForTimeInterval:3.0];

NSLog(@"%@----子线程任务结束",[NSThread currentThread]);

}

}


控制台输出的结果如下:

2016-12-01 16:44:25.559 RunLoopDemo[4516:352041] <HLThread: 0x608000275680>{number = 4, name = (null)}----子线程任务开始

2016-12-01 16:44:28.633 RunLoopDemo[4516:352041] <HLThread: 0x608000275680>{number = 4, name = (null)}----子线程任务结束

2016-12-01 16:44:28.633 RunLoopDemo[4516:352041] -[HLThread dealloc]

当子线程中的任务执行完毕后,线程就被立刻销毁了。如果程序中,需要经常在子线程中执行任务,频繁的创建和销毁线程,会造成资源的浪费。这时候我们就可以使用RunLoop来让该线程长时间存活而不被销毁。

我们将上面的示例代码修改一下,修改后的代码过程为,创建一个子线程,当子线程启动后,启动runloop,点击视图,会在子线程中执行一个耗时3秒的任务(其实就是让线程睡眠3秒)。

修改后的代码如下:

@implementation ViewController


- (void)viewDidLoad {

[super viewDidLoad];

// 1.测试线程的销毁

[self threadTest];

}


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

{

[self performSelector:@selector(subThreadOpetion) onThread:self.subThread withObject:nil waitUntilDone:NO];

}


- (void)threadTest

{

HLThread *subThread = [[HLThread alloc] initWithTarget:self selector:@selector(subThreadEntryPoint) object:nil];

[subThread setName:@"HLThread"];

[subThread start];

self.subThread = subThread;

}


/**

子线程启动后,启动runloop

*/

- (void)subThreadEntryPoint

{

@autoreleasepool {

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

//如果注释了下面这一行,子线程中的任务并不能正常执行

[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];

NSLog(@"启动RunLoop前--%@",runLoop.currentMode);

[runLoop run];

}

}


/**

子线程任务

*/

- (void)subThreadOpetion

{

NSLog(@"启动RunLoop后--%@",[NSRunLoop currentRunLoop].currentMode);

NSLog(@"%@----子线程任务开始",[NSThread currentThread]);

[NSThread sleepForTimeInterval:3.0];

NSLog(@"%@----子线程任务结束",[NSThread currentThread]);

}


@end


先看控制台输出结果:

2016-12-01 17:22:44.396 RunLoopDemo[4733:369202] 启动RunLoop前--(null)

2016-12-01 17:22:49.285 RunLoopDemo[4733:369202] 启动RunLoop后--kCFRunLoopDefaultMode

2016-12-01 17:22:49.285 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子线程任务开始

2016-12-01 17:22:52.359 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子线程任务结束

2016-12-01 17:22:55.244 RunLoopDemo[4733:369202] 启动RunLoop后--kCFRunLoopDefaultMode

2016-12-01 17:22:55.245 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子线程任务开始

2016-12-01 17:22:58.319 RunLoopDemo[4733:369202] <HLThread: 0x60000027cb40>{number = 4, name = HLThread}----子线程任务结束


有几点需要注意:

  • 1.获取RunLoop只能使用 [NSRunLoop currentRunLoop] 或 [NSRunLoop mainRunLoop];

  • 2.即使RunLoop开始运行,如果RunLoop 中的 modes 为空,或者要执行的mode里没有item,那么RunLoop会直接在当前loop中返回,并进入睡眠状态。

  • 3.自己创建的Thread中的任务是在kCFRunLoopDefaultMode这个mode中执行的。

  • 4.在子线程创建好后,最好所有的任务都放在AutoreleasePool中。

注意点一解释

RunLoop官方文档中的第二段中就已经说明了,我们的应用程序并不需要自己创建RunLoop,而是要在合适的时间启动runloop。CF框架源码中有 CFRunLoopGetCurrent(void)CFRunLoopGetMain(void) ,查看源码可知,这两个API中,都是先从全局字典中取,如果没有与该线程对应的RunLoop,那么就会帮我们创建一个RunLoop(创建RunLoop的过程在函数 _CFRunLoopGet0(pthread_t t) 中)。

注意点二解释

这一点,可以将示例代码中的 [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes]; ,可以看到注释掉后,无论我们如何点击视图,控制台都不会有任何的输出,那是因为mode 中并没有item任务。经过NSRunLoop封装后,只可以往mode中添加两类item任务:NSPort(对应的是source)、NSTimer,如果使用 CFRunLoopRef ,则可以使用C语言API,往mode中添加source、timer、observer。

如果不添加 [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes]; ,我们把runloop的信息输出,可以看到:

如果我们添加上 [runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes]; ,再把RunLoop的信息输出,可以看到:

注意点三解释

怎么确认自己创建的子线程上的任务是在kCFRunLoopDefaultMode这个mode中执行的呢?我们只需要在执行任务的时候,打印出该RunLoop的currentMode即可。

因为RunLoop执行任务是会在mode间切换,只执行该mode上的任务,每次切换到某个mode时,currentMode就会更新。源码请下载:CF框架源码 CFRunLoopRun() 方法中会调用 CFRunLoopRunSpecific() 方法,而 CFRunLoopRunSpecific() 方法中有这么两行关键代码:

CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);

......这中间还有好多逻辑代码

CFRunLoopModeRef previousMode = rl->_currentMode;

rl->_currentMode = currentMode;

...... 这中间也有一堆的逻辑

rl->_currentMode = previousMode;



我测试后,控制台输出的是:

2016-12-02 11:09:47.909 RunLoopDemo[5479:442560] 启动RunLoop后--kCFRunLoopDefaultMode

2016-12-02 11:09:47.910 RunLoopDemo[5479:442560] <HLThread: 0x608000270a80>{number = 4, name = HLThread}----子线程任务开始

2016-12-02 11:09:50.984 RunLoopDemo[5479:442560] <HLThread: 0x608000270a80>{number = 4, name = HLThread}----子线程任务结束


注意点四解释 关于AutoReleasePool的官方文档中有提到:

If you spawn a secondary thread.

You must create your own autorelease pool block as soon as the thread begins executing;

otherwise, your application will leak objects. (See Autorelease Pool Blocks and Threads for details.)


Each thread in a Cocoa application maintains its own stack of autorelease pool blocks.

If you are writing a Foundation-only program or if you detach a thread, you need to create your own autorelease pool block.

If your application or thread is long-lived and potentially generates a lot of autoreleased objects,

you should use autorelease pool blocks (like AppKit and UIKit do on the main thread);

otherwise, autoreleased objects accumulate and your memory footprint grows.

If your detached thread does not make Cocoa calls, you do not need to use an autorelease pool block.


AFNetworking中的RunLoop案例

在AFNetworking 2.6.3之前的版本,使用的还是NSURLConnection,可以在 AFURLConnectionOperation 中找到使用RunLoop的源码:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {

@autoreleasepool {

[[NSThread currentThread] setName:@"AFNetworking"];

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

[runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];

[runLoop run];

}

}

+ (NSThread *)networkRequestThread {

static NSThread *_networkRequestThread = nil;

static dispatch_once_t oncePredicate;

dispatch_once(&oncePredicate, ^{

_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];

[_networkRequestThread start];

});

return _networkRequestThread;

}


AFNetworking都是通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。

- (void)start {

[self.lock lock];

if ([self isCancelled]) {

[self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

} else if ([self isReady]) {

self.state = AFOperationExecutingState;

[self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];

}

[self.lock unlock];

}


我们在使用 NSURLConnection 或者 NSStream 时,也需要考虑到RunLoop问题,因为默认情况下这两个类的对象生成后,都是在当前线程的 NSDefaultRunLoopMode 模式下执行任务。如果是在主线程,那么就会出现滚动ScrollView以及其子视图时,主线程的RunLoop切换到 UITrackingRunLoopMode 模式,那么 NSURLConnection 或者 NSStream 的回调就无法执行了。

要解决这个问题,有两种方式:

第一种方式是创建出 NSURLConnection 对象或者 NSStream 对象后,再调用 - (void)scheduleInRunLoop:(NSRunLoop *)aRunLoop forMode:(NSRunLoopMode)mode ,设置RunLoopMode即可。需要注意的是 NSURLConnection 必须使用其初始化构造方法 - (nullable instancetype)initWithRequest:(NSURLRequest *)request delegate:(nullable id)delegate startImmediately:(BOOL)startImmediately 来创建对象,设置Mode才会起作用。

第二种方式,就是所有的任务都在子线程中执行,并保证子线程的RunLoop正常运行即可(即上面AFNetworking的做法,因为主线程的RunLoop切换到 UITrackingRunLoopMode ,并不影响其他线程执行哪个mode中的任务,计算机CPU是在每一个时间片切换到不同的线程去跑一会,呈现出的多线程效果)。

代码地址

https://github.com/Haley-Wong/RunLoopDemos

如果感觉这篇文章不错可以点击在看:point_down:

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章