iOS开发 ☞ RunLoop使用

runloop基本概念

Runloop 是什么?Runloop 还是比较顾名思义的一个东西,说白了就是一种循环,只不过它这种循环比较高级。一般的 while 循环会导致 CPU 进入忙等待状态,而 Runloop 则是一种“闲”等待,这部分可以类比 Linux 下的 epoll。当没有事件时,Runloop 会进入休眠状态,有事件发生时, Runloop 会去找对应的 Handler 处理事件。Runloop 可以让线程在需要做事的时候忙起来,不需要的话就让线程休眠。

盗一张苹果官方文档的图,也是几乎每个讲 Runloop 的文章都会引用的图,大体说明了 Runloop 的工作模式

图中展现了 Runloop 在线程中的作用:从 input source 和 timer source 接受事件,然后在线程中处理事件。

EvenLoop 模型

function loop() {
initialize();
do {
    var message = get_next_message();
    process_message(message);
   } while (message != quit);
}

在不知道如何让一个字线程保活的情况下,我们先用while循环,并且循环创建没有任何输入源的RunLoop对象,代码如下:

@interface ViewController ()
@property (nonatomic, strong) NSThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
[super viewDidLoad];
  _thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
  [_thread start]; 
 //一开始便给一个Source0,不需要while循环
 // [self performSelector:@selector(refreshUI) onThread:_thread withObject:nil waitUntilDone:NO];
}
- (void)touchesBegan:(NSSet *)touches withEvent:   (UIEvent *)event {

  [self performSelector:@selector(refreshUI) onThread:_thread withObject:nil waitUntilDone:NO];
}
- (void)startThread {
    while (1) {
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"thread:%@ func:%s",[NSThread currentThread],__func__);
    }
}

- (void)refreshUI {
    NSLog(@"thread:%@ func:%s",[NSThread currentThread],__func__);
}

打印如下:

TestRunLoop[1427:936309] thread:{number = 3, name = (null)} func:-[ViewController startThread]
无限次
TestRunLoop[1427:936309] thread:{number = 3, name = (null)} func:-[ViewController refreshUI]
停止打印

分析打印可以得到的结论是:RunLoop对象处理了perfromselect的输入源(Source0)使得函数在指定的线程执行。

RunLoop 对外的接口

深入理解RunLoop

在 CoreFoundation 里面关于 RunLoop 有5个类:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

其中 CFRunLoopModeRef 类并没有对外暴露,只是通过 CFRunLoopRef 的接口进行了封装。他们的关系如下:

一个 RunLoop 包含若干个 Mode,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
• Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
• Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。

CFRunLoopTimerRef 是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), // 即将进入Loop
kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer
kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source
kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6), // 刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7), // 即将退出Loop
};

上面的 Source/Timer/Observer 被统称为 mode item,一个 item 可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个 mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

由此得知,为了让一个线程循环的跑起来,我们可以给线程添加一个Source1的输入源,或者开启一个定时器,但是不能仅靠添加观察者让runloop跑起来,(看到很多资料都说添加观察者可以实现,下面我给出论证,欢迎指正)。

给一个线程添加观察者
//创建观察者所需的回调包
static void runLoopObserverCallBack(CFRunLoopObserverRef     observer,  CFRunLoopActivity activity, void *info)
{
    NSLog(@"%@",observer);
    NSLog(@"%lu",activity);
}

@interface ViewController ()
@property (nonatomic, strong) NSThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
     [super viewDidLoad];
    _thread = [[NSThread alloc] initWithTarget:self selector:@selector(startThread) object:nil];
    [_thread start];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
[self performSelector:@selector(refreshUI) onThread:_thread withObject:nil waitUntilDone:NO];
}

- (void)startThread {

NSRunLoop* myRunLoop = [NSRunLoop currentRunLoop];

// Create a run loop observer and attach it to the run loop.
CFRunLoopObserverContext  context = {0, (__bridge void *)(self), NULL, NULL, NULL};
CFRunLoopObserverRef  observer = CFRunLoopObserverCreate(kCFAllocatorDefault,
                                   kCFRunLoopAllActivities,
                                   YES,
                                   0,
                                   &runLoopObserverCallBack,
                                   &context);

if (observer)
{
   //需要使用Core Fundation框架的提供的API
    CFRunLoopRef    cfLoop = [myRunLoop getCFRunLoop];
    CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
}
[myRunLoop run];
}

- (void)refreshUI {
NSLog(@"%s",__func__);
}

打印可以看到添加的观察者:

给线程添加观察者的应用:iOS实时卡顿监控 、How To Use Runloop

应用实例

AFNetworking

AFURLConnectionOperation 这个类是基于 NSURLConnection 构建的,其希望能在后台线程接收 Delegate 回调。为此 AFNetworking 单独创建了一个线程,并在这个线程中启动了一个 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;
}

RunLoop 启动前内部必须要有至少一个 Timer/Observer/Source,所以 AFNetworking 在 [runLoop run] 之前先创建了一个新的 NSMachPort 添加进去了。通常情况下,调用者需要持有这个 NSMachPort (mach_port) 并在外部线程通过这个 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结构

已开源

struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock;          /* locked for accessing mode list */
__CFPort _wakeUpPort;           // used for CFRunLoopWakeUp 
Boolean _unused;
volatile _per_run_data *_perRunData;              // reset for runs of the run loop
pthread_t _pthread;
uint32_t _winthread;
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;//切换mode时,这个mode item 将会同步到所有标记为common的mode中。
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
struct _block_item *_blocks_head;
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};

struct __CFRunLoopMode {
CFRuntimeBase _base;
pthread_mutex_t _lock;  /* must have the run loop locked before locking this */
CFStringRef _name;
Boolean _stopped;
char _padding[3];
CFMutableSetRef _sources0;
CFMutableSetRef _sources1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
CFMutableDictionaryRef _portToV1SourceMap;
__CFPortSet _portSet;
CFIndex _observerMask;
#if USE_DISPATCH_SOURCE_FOR_TIMERS
dispatch_source_t _timerSource;
dispatch_queue_t _queue;
Boolean _timerFired; // set to true by the source when a timer has        fired
Boolean _dispatchTimerArmed;
#endif
#if USE_MK_TIMER_TOO
mach_port_t _timerPort;
Boolean _mkTimerArmed;
#endif
#if DEPLOYMENT_TARGET_WINDOWS
DWORD _msgQMask;
void (*_msgPump)(void);
#endif
uint64_t _timerSoftDeadline; /* TSR */
uint64_t _timerHardDeadline; /* TSR */
};

这里有个概念叫 "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 里去。

总结一下,一个RunLoop对象可以运行在不同的模式下(_modes)运行,每次只能运行一个mode,每次切换mode都要退出runloop再以指定的mode重新运行。如果想一个mode item 在哪个模式都有效,可以标记一个mode item 为 common item (NSRunLoopCommonModes),同时要保证对应的mode 为 common mode(主线程的runloop,default mode 和 uitrackingrunloopmode 已经被标记为common)。

示例程序:

@interface ViewController ()
@property (nonatomic, strong) NSThread *thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    _thread = [[NSThread alloc] initWithTarget:self      selector:@selector(startThread) object:nil];
    [_thread start];
}

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [self performSelector:@selector(refreshUI) onThread:_thread    withObject:nil waitUntilDone:NO];
}

- (void)startThread {
   //保证runloop不退出
    [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
   //添加Source0
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:0] interval:2 repeats:YES block:^(NSTimer * _Nonnull timer) {
         
     }];
   //将timer标记为common item
    [[NSRunLoop currentRunLoop] addTimer:timer  forMode:NSRunLoopCommonModes];
    [[NSRunLoop currentRunLoop] run];
}
Cocoa API 分析

1、run​Mode:​before​Date:​

如果没有任何的inputSource 或者 timer 该方法直接返回NO(向下执行),如果有输入源,那么第一个源输入到来的时候或者设置的超时时间到来的时候立即返回。

2、runUntilDate:

当没有Source或者timer的时候立即返回。这个方法待论证

相关资料:

深入理解RunLoop

IOS---实例化讲解RunLoop

How To Use Runloop

资料整理

你可能感兴趣的:(iOS开发 ☞ RunLoop使用)