001-NSRunLoop及CFRunLoop浅析

一.前记:

一直知道有Runloop这个东西,但做了不少项目了,却从来没有在项目里自己用过,有用到也是系统或者第三方框架.前段时间有幸项目里有用的到的地方.故而研究了几天,于是记下这篇有关自己理解.

二.先附上代码:

1.子线程创建RunLoop并执行任务

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

- (void)runloopMethod {
NSTimer*timer = [NSTimer timerWithTimeInterval:2 target:self selector:@selector(actionMethod) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
[[NSRunLoop currentRunLoop] run];
}

///子线程定时器执行的方法
- (void)actionMethod {
//在子线程执行,可以是耗时操作,与界面相关时切换到主线程刷新UI即可
}

2.使用CFRunLoop

static void Callback (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    NSLog(@"Runloop callback");
}
- (void)addObserverByRunloop {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    CFRunLoopObserverContext context = {
        0,
        (__bridge void*)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };
    static CFRunLoopObserverRef defaultModeObserver;
    defaultModeObserver = CFRunLoopObserverCreate(NULL,kCFRunLoopBeforeWaiting,YES,NSIntegerMax-999, &Callback, &context);
    CFRunLoopAddObserver(runloop, defaultModeObserver,kCFRunLoopCommonModes);
    CFRelease(defaultModeObserver);
}

三.说明:

1.NSRunLoop类浅析

1.1 NSRunLoop类用官方文档(Xcode 8.3)查看,隶属于Objective-C下Foundation下的一个类。

NSRunLoop类的隶属.png
NSRunLoop类.png

1.2 第一个重要属性currentRunLoop

对外是只读的:

@property (class, readonly, strong) NSRunLoop *currentRunLoop;

文档的说明,也清晰明了

Returns the run loop for the current thread.

返回当前线程的运行循环
Return Value

The NSRunLoop object for the current thread.

返回的是当前线程的一个NSRunLoop对象
Discussion

If a run loop does not yet exist for the thread, one is created and returned.

如果线程不存在运行循环,则创建并返回一个

应用:我们使用以下语法,能轻松获取当前线程的RunLoop

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];

1.3 currentMode

声明:

@property (nullable, readonly, copy) NSRunLoopMode currentMode;

文档说明:

The receiver's current input mode.
得到当前运行循环的模式
Discussion
The receiver's current input mode. This method returns the current input mode only while the receiver is running; otherwise, it returns nil.

只能获得已经跑起来的运行循环的模式,否则返回空。

☆☆☆ 重点 ☆☆☆

1.只有主线程的运行循环默认是在跑的,也就是说创建的子线程,运行循环并没有跑起来。
☆所以在主线程创建定时器,可以正常运行,而在子线程创建定时器,压根就不跑,原因就是子线程的运行循环并没有开启,执行了任务后,子线程直接被释放了。故而子线程如果不手动开启运行循环,定时器是失效的!

往事回顾:

项目里一直有用到一个框架,名字叫GCDAsyncSocket
里面有一个方法如下(大约在6202行):

+ (void)listenerThread { @autoreleasepool
{
    LogInfo(@"ListenerThread: Started");
    
    // We can't run the run loop unless it has an associated input source or a timer.
    // So we'll just create a timer that will never fire - unless the server runs for a decades.
    [NSTimer scheduledTimerWithTimeInterval:[[NSDate distantFuture] timeIntervalSinceNow]
                                     target:self
                                   selector:@selector(ignore:)
                                   userInfo:nil
                                    repeats:YES];
    
    [[NSRunLoop currentRunLoop] run];
    
    LogInfo(@"ListenerThread: Stopped");
}}

之前看到这段代码时,迷迷糊糊,有点费解,现在不敢说完全懂了,但似乎心中有了一丝明悟

1.4 既然上面提到主线程默认有运行循环,那也应该有个方法可以直接得到主线程的运行循环

声明:

@property (class, readonly, strong) NSRunLoop *mainRunLoop NS_AVAILABLE(10_5, 2_0);
Returns the run loop of the main thread.

Return Value

An object representing the main thread’s run loop.
得到主线程的运行循环

1.5 可以在运行循环上添加定时器 addTimer:forMode:

声明:

- (void)addTimer:(NSTimer *)timer forMode:(NSRunLoopMode)mode;
Registers a given timer with a given input mode.
用指定的模式给指定的定时器注册

其实:可以给定时器不止一种模式的!
1.6 以上说到了模式,这里说明下

☆☆☆ 运行循环一共有五种模式 ☆☆☆

我们能用到的模式只有以下两种:

  • NSDefaultRunLoopMode 默认模式

  • UITrackingRunLoopMode UI模式

因为模式使用时可以<b>同时使用不同的模式</b>(本质上是不能同时使用的,但使用这种模式时会在两种模式间切换,所以可以简单理解为是同时使用),故而产生以下模式:

  • NSRunLoopCommonModes 占位模式,其实就是默认模式和UI模式一起使用。

还有两种模式是使用不到的:App创建时的模式和内核模式,都由系统调度。

2.CFRunLoop类

2.1 CFRunLoop类的隶属:CFRunLoop类是Objective-C下Core Foundation的类

CFRunLoop类的隶属.png
CFRunLoop类.png

2.2 因为目前只用到了一个使用场景,就根据这个使用场景,简单说明下

  • 使用场景:往往大家都知道耗时操作在子线程,UI操作在主线程,而遇到<b>UI操作是耗时操作</b>时就会产生卡顿的感觉(UI操作是耗时操作时,理应放在主线程执行)。
  • 卡顿原因:运行循环在一个循环过程中有大量工作要做(耗时),导致下一次循环需要等待!
  • 解决方案:给运行循环添加监听事件,把耗时操作拆分成很多小的任务,在运行循环每一次循环时只执行一个很小的任务。<b> 这样,耗时操作也在执行,运行循环也没有卡住(无卡顿感)</b>。

2.3 其实,解决方案很简单!只需要使用CFRunLoop的CFRunLoopAddObserver就行了!也就是观察运行循环。不过有个难点是CFRunLoop是C语法。

声明:

void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFRunLoopMode mode);

这个函数的作用其实就是给运行循环添加一个观察者对象。

麻烦的是有三个参数,需要弄懂才好填。参数往下看,分开说明。

2.4 参数一:CFRunLoopRef rl

这个参数其实就是要问:你需要给哪个运行循环添加观察者呢?

我们直接获得当前运行循环就行了。

CFRunLoopRef runloop = CFRunLoopGetCurrent();

2.5 参数二:CFRunLoopObserverRef observer

这个参数其实就是要问:观察者是谁呢?

那我们直接创建一个观察者就行啦。

☆ 注意点:为了避免观察者意外死亡(过了这个方法,就被释放了),所以使用静态变量!

static CFRunLoopObserverRef defaultModeObserver;

有两个方法供我们创建观察者:

创建观察者.png
CFRunLoopObserverRef CFRunLoopObserverCreate(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, CFRunLoopObserverCallBack callout, CFRunLoopObserverContext *context);
CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block)(CFRunLoopObserverRef observer, CFRunLoopActivity activity));

创建观察者我们使用CFRunLoopObserverCreate就行了!第二个是使用Block,差别不大。

脸黑😳 的是创建一个观察者,有六个参数!!!(这也是尽管功能强大,但使用的人很少的原因之一吧)

<b>创建观察者参数一:CFAllocatorRef allocator</b>

The allocator to use to allocate memory for the new object. Pass NULL or kCFAllocatorDefault to use the current default allocator.

文档是以上这么描述的:传个NULL或者kCFAllocatorDefault就行了,我们点进去查看kCFAllocatorDefault是什么的时候,文档说:This is a synonym for NULL. 意思是:kCFAllocatorDefault其实就是NULL。苹果这么玩,深深的无奈!

<b>创建观察者参数二:CFOptionFlags activities</b>

Set of flags identifying the activity stages of the run loop during which the observer should be called. See CFRunLoopActivityfor the list of stages. To have the observer called at multiple stages in the run loop, combine the CFRunLoopActivity values using the bitwise-OR operator.

上述文档意思就是问:你要观察运行循环的哪个阶段(CFRunLoopActivity)?

无奈,继续查看CFRunLoopActivity是什么鬼,声明如下:

typedef enum CFRunLoopActivity : CFOptionFlags {
    kCFRunLoopEntry = (1UL << 0),
    kCFRunLoopBeforeTimers = (1UL << 1),
    kCFRunLoopBeforeSources = (1UL << 2),
    kCFRunLoopBeforeWaiting = (1UL << 5),
    kCFRunLoopAfterWaiting = (1UL << 6),
    kCFRunLoopExit = (1UL << 7),
    kCFRunLoopAllActivities = 0x0FFFFFFFU
} CFRunLoopActivity;
  • kCFRunLoopEntry:
The entrance of the run loop, before entering the event processing loop. This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode.

简言之:就是运行循环的入口

  • kCFRunLoopBeforeTimers:
Inside the event processing loop before any timers are processed.

意思是:在事件处理循环定时器处理之前。

  • kCFRunLoopBeforeSources:
Inside the event processing loop before any sources are processed.

意思是:在事件处理循环之前处理来源。

  • kCFRunLoopBeforeWaiting:
Inside the event processing loop before the run loop sleeps, waiting for a source or timer to fire. This activity does not occur if CFRunLoopRunInMode is called with a timeout of 0 seconds. It also does not occur in a particular iteration of the event processing loop if a version 0 source fires.

意思是:运行循环在等待之前。

  • kCFRunLoopAfterWaiting:
Inside the event processing loop after the run loop wakes up, but before processing the event that woke it up. This activity occurs only if the run loop did in fact go to sleep during the current loop.

意思是:运行循环在等待之后。

  • kCFRunLoopExit:
The exit of the run loop, after exiting the event processing loop. This activity occurs once for each call to CFRunLoopRun and CFRunLoopRunInMode.

意思是:运行循环退出了。

稍微分析一下,我们是要处理耗时操作的,选择kCFRunLoopBeforeWaiting看起来比较合适,每当运行循环要休息的时候,就给它点事情做做。

<b>创建观察者参数三:Boolean repeats</b>

终于逮着一个简单的参数:问是否需要重复?

毫不犹豫回答:YES

<b>创建观察者参数四:CFIndex order</b>

A priority index indicating the order in which run loop observers are processed. When multiple run loop observers are scheduled in the same activity stage in a given run loop mode, the observers are processed in increasing order of this parameter. Pass 0 unless there is a reason to do otherwise.

上述文档说明看得有点晕,只能看懂一个大概,结合查阅资料以及结合翻译。个人的简单理解就是:需要填写一个优先级,填0基本是就是不执行了,数值越高,优先级越高!担忧填最高的优先级会出现意外,所以选择一个稳妥点的方式:NSInteger的最大值然后减去一个999

于是准备填:NSIntegerMax - 999

<b>创建观察者参数五:CFRunLoopObserverCallBack callout</b>

回调:需要真实执行的任务,就靠这个回调了!

The callback function invoked when the observer runs.

改怎么写这个回调函数呢?只好点进去看声明(因为是C语法,之前没看声明,所以遇到了C语法无法和OC语法需要执行的任务无法顺利转换的问题):

typedef void (*CFRunLoopObserverCallBack)(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info);

又是三个参数-_-!

前两个解释过了,第三个,是这整个CFRunLoop核心中的核心,因为void *info其实就是OC语法中的id info

<b>创建观察者参数六:CFRunLoopObserverContext *context</b>

CFRunLoopObserverContext:

A structure that contains program-defined data and callbacks with which you can configure a CFRunLoopObserver object’s behavior.

这个CFRunLoopObserverContext是一个结构体:

typedef struct {
    CFIndex version;
    void *  info;
    const void *(*retain)(const void *info);
    void    (*release)(const void *info);
    CFStringRef (*copyDescription)(const void *info);
} CFRunLoopObserverContext;

以上void * info是关键!

既然这个结构体中有一个void *也就是任意类型,那我们就可以完成与OC的桥梁了。

结构体其余参数简述:CFIndex version,显然说的是版本。第三个第四个参数,看到retain和release就知道是和内存相关的了。第五个参数看到一个单词Description,那就是描述的字符串吧。

那这个完整的结构体就清晰了。我写的代码如下:

    CFRunLoopObserverContext context = {
        0,
        (__bridge void*)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };

版本,直接填了0
因为C与OC转换需要__bridge桥接一下,因为可以接收任意类型,我直接把self也就是控制器传过去
CF内存CFRetain
CF内存释放CFRelease
描述直接给个NULL

至此,观察者所有参数填完也就创建好了。

 static CFRunLoopObserverRef defaultModeObserver;
 defaultModeObserver =CFRunLoopObserverCreate(NULL,kCFRunLoopBeforeWaiting,YES,NSIntegerMax-999, &Callback, &context);

2.6 参数三:CFRunLoopMode mode

模式:kCFRunLoopCommonModes

综上所述:添加观察者也就完成了

CFRunLoopAddObserver(runloop, defaultModeObserver,kCFRunLoopCommonModes);

因为是C语法,需要手动释放内存!

CFRelease(defaultModeObserver);

总结以上所有代码,写出来如下:

static void Callback (CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) {
    NSLog(@"Runloop callback");
}
- (void)addObserverByRunloop {
    CFRunLoopRef runloop = CFRunLoopGetCurrent();
    CFRunLoopObserverContext context = {
        0,
        (__bridge void*)(self),
        &CFRetain,
        &CFRelease,
        NULL
    };
    static CFRunLoopObserverRef defaultModeObserver;
    defaultModeObserver = CFRunLoopObserverCreate(NULL,kCFRunLoopBeforeWaiting,YES,NSIntegerMax-999, &Callback, &context);
    CFRunLoopAddObserver(runloop, defaultModeObserver,kCFRunLoopCommonModes);
    CFRelease(defaultModeObserver);
}

下一篇:002-CocoaPods简析

推荐阅读更多精彩内容