前言:先查看苹果的API,搞懂其作用,这是通往资深的一条路
runloop,管理事件/消息,让线程在没有处理消息时休眠以避免资源占用、在有消息到来时立刻被唤醒。它消除了消耗CPU周期轮询,并防止处理器本身进入休眠状态并节省电源
在runloop中,需要处理的事件分两种,一种是输入源,一种是定时器。
输入源分三类:performSelector源,基于端口(Mach port)的源,以及自定义的源
runloop与线程
apple不允许直接创建runloop。但提供了两个自动获取的函数
CFRunLoopGetMain() [NSRunLoop mainRunLoop];
CFRunLoopGetCurrent() [NSRunLoop currentRunLoop];
// class 可以用类实例方法的形式获取(此处需纠正)
@property (class, readonly, strong) NSRunLoop *mainRunLoop
这两个函数内部的大概逻辑
//全局的dictionary,key是pthread_t(线程),value是CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
//访问loopDic时的锁
static CFSpinLock_t loopsLock;
// 获取一个pthread对应的RunLoop
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
OSSpinLockLock(&loopsLock);
if (!loopsDic) {
// 第一次进入时,初始化全局dic,并先为主线程创建一个Runloop
loopDic = CFDictionaryCreateMutable();
CFRunLoopRef mainLoop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop)
}
CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread);
if (!loop) {
loop = _CFRunLoopCreate();
CFDictionarySetValue(loopsDic, thread, loop);
// 注册一个回调,当线程销毁时,顺便也销毁其对应的RunLoop
_CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
}
OSSpinLockUnLock(&loopsLock);
return loop;
}
CFRunLoopRef CFRunLoopGetMain(){
return _CFRunLoopGet(pthread_main_thread_np());
}
CFRunLoopRef CFRunLoopGetCurrent() {
return _CFRunLoopGet(pthread_self());
}
从上面的代码可以看出,线程和runloop之间是一一对应的,其关系是保存在一个全局的dictionary里。线程刚创建时并没有runloop,如果你不主动获取,那它一直都不会有。runloop的创建发生在第一次获取时,runloop的销毁是发生在线程结束时,你只能在一个线程的内部获取其runloop(主线程除外)
runloop对外的接口
一个runloop包含若干个mode,每个mode又包含若干个Source/Timer/Observer.每次调用runloop的主函数时,只能指定其中一个mode,这个mode被称为CurrentMode.如果要切换mode,只能退出runloop,再重新指定一个mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。
上面的Source/Timer/Observer被统称为mode item,一个 item可以被同时加入多个mode,如果一个mode中一个item都没有,则runloop会直接退出,不进入循环。
这里有个概念叫 "CommonModes":一个 Mode 可以将自己标记为"Common"属性(通过将其 ModeName 添加到 RunLoop 的 "commonModes" 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 "Common" 标记的所有Mode里。
应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultMode 和 UITrackingRunLoopMode。这两个 Mode 都已经被标记为"Common"属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。
有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 "commonModeItems" 中。"commonModeItems" 被 RunLoop 自动更新到所有具有"Common"属性的 Mode 里去。
kCFRunLoopDefaultMode app的默认mode,通常主线程是在这个mode下运行
UITrackingRunLoopMode 界面跟踪mode,用于scrollview追踪触摸滑动,保证界面滑动时不受其他mode影响
线程启动
线程启动之后,就进入三个状态中的一种:运行(running)、就绪(ready)、阻塞(blocked)
线程间的通信
Cocoa为ios线程间通信提供了2中方式
1.performSelector
@interface NSObject (NSThreadPerformAdditions)
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array;
- (void)performSelectorOnMainThread:(SEL)aSelector withObject:(nullable id)arg waitUntilDone:(BOOL)wait;
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait modes:(nullable NSArray<NSString *> *)array NS_AVAILABLE(10_5, 2_0);
- (void)performSelector:(SEL)aSelector onThread:(NSThread *)thr withObject:(nullable id)arg waitUntilDone:(BOOL)wait NS_AVAILABLE(10_5, 2_0);
// equivalent to the first method with kCFRunLoopCommonModes
- (void)performSelectorInBackground:(SEL)aSelector withObject:(nullable id)arg NS_AVAILABLE(10_5, 2_0);
@end·
2.NSMachPort
NSPort有三个子类 NSSocketPot NSMessagePort NSMachPort 但在ios下只有NSMachPort可用,
使用的方式为接收线程中注册NSMachPort,在另外的线程中使用此port发送消息,则被注册线程会收到相应消息,然后最终在主线程里调用某个回调函数。可以看到,使用NSMachPort的结果为调用了其它线程的1个函数,而这正是performSelector所做的事情,所以,NSMachPort是个鸡肋。线程间通信应该都通过performSelector来搞定
AutoreleasePool
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
PerformSelecter
当调用 NSObject 的 performSelecter:afterDelay: 后,实际上其内部会创建一个 Timer 并添加到当前线程的 RunLoop 中。所以如果当前线程没有 RunLoop,则这个方法会失效。
当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。
如何保证子线程不退出?
开启runloop,单纯开始runloop还不行,还需调用addPort方法
AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 RunLoop:
// AFNetWorking 2.0
+ (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;
}
runloop启动前,内部必须要有至少一个 Source/Timer/Observer,所以AFNetworking在[runloop run]之前先创建了一个新的NSMachPort添加去了。通常情况下,调用者需要持有这个NSMachPort并在外部线程通过这个port发送消息到loop内;但此处添加port只是为了让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];
}
当需要这个后台线程执行任务时,AFNetworking 通过调用 [NSObject performSelector:onThread:..] 将这个任务扔到了后台线程的 RunLoop 中。
每一个线程都有其对应的RunLoop,但是默认非主线程的RunLoop是没有运行的,需要为RunLoop添加至少一个事件源,然后去run它。一般情况下我们是没有必要去启用线程的RunLoop的,除非你在一个单独的线程中需要长久的检测某个事件。
每次 runloop 的时候,都会检查对象的 retainCount,如果retainCount 为 0,说明该对象没有地方需要继续使用了,可以释放掉了。
每个线程(包含主线程)都有一个Runloop。对于每一个Runloop,系统会隐式创建一个Autorelease pool,在每一个Runloop结束时,当前栈顶的Autorelease pool会被销毁,这样这个pool里的每个Object会被release。