RunLoop简介一

RunLoop学习一
RunLoop 是 iOS 和 OS X 开发中非常基础的一个概念,这篇文章将从 CFRunLoop 的源码入手,介绍 RunLoop 的概念以及底层实现原理。之后会介绍一下在 iOS 中,苹果是如何利用 RunLoop 实现自动释放池、延迟回调、触摸事件、屏幕刷新等功能的。
这里写图片描述
0. RunLoop资料
苹果官方文档:
https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html
CFRunLoopRef是开源的
http://opensource.apple.com/source/CF/CF-1151.16/
1. RunLoop对象
iOS中有两套API来访问和使用RunLoop

Foundation框架(OC) –> NSRunLoop
Core Foundation框架(C) –>CFRunLoopRef
NSRunLoop和CFRunLoopRef都代表着RunLoop对象
NSRunLoop是基于CFRunLoopRef的一层OC包装, 所以要了解RunLoop内部结构, 需要多研究CFRunLoopRef层面的API(Core Foundation层面)

  1. main函数中的RunLoop

main函数中的RunLoop.png
main函数中的RunLoop: UIApplicationMain函数内部就启动了一个RunLoop, 所以UIApplicationMain函数一直没有返回,保持了程序的持续运行, 这个默认启动的RunLoop是跟主线程相关联的
由于main函数里面启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态
2. RunLoop与线程
每条线程都有唯一的一个与之对应的RunLoop对象(如果我也想开一个子线成,并且让线程不死,则子线程开一个RunLoop)
主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
RunLoop在第一次获取时创建,在线程结束时销毁
3. 获得RunLoop对象
Foundation
[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
[NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象
Core Foundation
CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
CFRunLoopGetMain(); // 获得主线程的RunLoop对象
如果在主线程中: 当前线程的RunLoop对象和主线程的RunLoop对象取得的是相同的, 代码演示:
- (void)viewDidLoad {
[super viewDidLoad];

// 主线程中打印的mainRunLoop, currentRunLoop内存地址是相同的
NSLog(@"%p---%p", [NSRunLoop mainRunLoop], [NSRunLoop currentRunLoop]);

// 创建一个子线程
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];

}

  • (void)run
    {
    // 开启的子线程默认是没有runloop的

    // 手动创建runloop对象, [NSRunLoop currentRunLoop]创建, 懒加载的创建方法,第一次访问创建,以后就不会创建了
    NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
    NSLog(@”thred–%p”, currentLoop); //打印的内存地址和主线程的不同
    }
    我们打开开源的CFRunLoop.c代码, 里面的有部分核心代码为下面:
    (下面的函数传一个线程进来,返回一个CFRunLoopRef对象, 说明一个线程对应一个runloop)

// should only be called by Foundation
// t==0 is a synonym for “main thread” that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
if (pthread_equal(t, kNilPthreadT)) {
t = pthread_main_thread_np();
}
__CFLock(&loopsLock);
if (!__CFRunLoops) {
__CFUnlock(&loopsLock);
CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
CFRelease(dict);
}
CFRelease(mainLoop);
__CFLock(&loopsLock);
}
CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
__CFUnlock(&loopsLock);
if (!loop) {
CFRunLoopRef newLoop = __CFRunLoopCreate(t);
__CFLock(&loopsLock);
loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
if (!loop) {
CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
loop = newLoop;
}
// don’t release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
__CFUnlock(&loopsLock);
CFRelease(newLoop);
}
if (pthread_equal(t, pthread_self())) {
_CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
_CFSetTSD(__CFTSDKeyRunLoopCntr, (void )(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void ()(void *))__CFFinalizeRunLoop);
}
}
return loop;
}
上面的代码表示: 当调用[NSRunLoop currentRunLoop]/CFRunLoopGetCurrent()时:

它会先创建一个可变字典CFMutableDictionaryRef, 然后创建一个主线程的CFRunLoopRef, 并将主线程pthread_main_thread_np()作为key, 主线程的CFRunLoopRef对象作为value, 放入字典中(也就意味着,在访问其他线程的时候, 系统会先把主线程的runloop创建好, 所以主线程不用我们创建runloop)
然后将当前开辟的线程传进入, 看看当前线程的线程的runloop在不在字典中, 如果不存在, 则创建当前开辟线程的CFRunLoopRef, 然后用新线程作为key, 用新线程的CFRunLoopRef对象作为value, 放入字典中

一个key对应一个value, 所以每条线程都有唯一的一个与之对应的RunLoop对象

  1. RunLoop相关的类
    Core Foundation中关于RunLoop的5个类:

CFRunLoopRef
CFRunLoopModeRef
CFRunLoopSourceRef
CFRunLoopTimerRef
CFRunLoopObserverRef

这5个类的关系图.png
由上面的5个类的关系图, 可以看出: runloop要想跑起来, 里面必须有Mode, Mode中必须有Source或Timer (RunLoop包含Mode,Mode包含其他的三个类,其他的这三个类分别用Set,Arrya,Arrya装着)
这里注意: 根据源码,runloop要跑起来先判断mode是否为空,如果为空退出,
然后判断source0是否为空,如果为空退出,然后判断source1是否为空,如果为空退出,然后判断是否有timer,如果没有就退出,并没有判断是否有observer,所以runloop如果要跑起来,必须有source或者timer的其中一个

4.1.1 CFRunLoopModeRef
CFRunLoopModeRef代表RunLoop的运行模式
一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode(可以获取到[NSRunLoop currentRunLoop].currentMode)
如果需要切换Mode,只能退出Loop再重新指定一个Mode进入(因为RunLoop是一个运行循环, 一直在跑圈, 换另一个模式,必须先退出, 然后按照另一个模式跑圈)
这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响(切换模式是为了,让它按照另一个模式的Source,Timer,Observer来跑圈, 互不影响)
RunLoop 启动必须要传入一个模式,RunLoop有多个模式, 但是每次只能运行一种模式
//创建runloop对象
NSRunLoop *currentLoop = [NSRunLoop currentRunLoop];
//启动(必须添加启动模式)
[currentLoop runMode:<#(nonnull NSString )#> beforeDate:<#(nonnull NSDate )#>];
4.1.2 CFRunLoopModeRef的5个模式:
系统默认注册了5个Mode:

kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响 (当我们滑动ScrollView,TableView等继承于ScrollView的控件是, 系统会切换模式为: UITrackingRunLoopMode, 跟踪你的触摸事件, 当停止滚动的时候, 系统会切换模式为: kCFRunLoopDefaultMode)
这个有什么用呢? 答案是这样的:

当ScrollView滚动时候, runloop对象只会处理UITrackingRunLoopMode这个模式下的定时器,Source, 以前添加的事件是不会处理的(以前添加的按钮等的事件是在kCFRunLoopDefaultMode模式下的),所以这个模式专门用来处理滚动的, 使滚动更加流畅,提高性能…举个栗子: 如果有TableView上有轮播图(NSTimer), 则在滚动TableView的时候,定时器是不好使的, 因为添加定时器默认是在kCFRunLoopDefaultMode下的
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用(这个是苹果系统在使用,我们一般用不到)

那么可以得出: 打开APP的时候, RunLoop先进入UIInitializationRunLoopMode模式,然后切换到kCFRunLoopDefaultMode, 如果有滚动,则切换到UITrackingRunLoopMode模式)
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到 (GS 绘图渲染等)

kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode (RunLoop要启动需要传递一个模式才可以启动, 这个模式不能用来启动)
其实RunLoop真正的模式为四个, 最后一个不算真正的模式, 而我们可以使用的基本上就是上面的两个模式, 最后一个kCFRunLoopCommonModes算是一个标签, 打上这个模式的标签就是通用模式, 都可以跑, 下面讲解定时器案例时候说明, 默认的定时器添加到kCFRunLoopDefaultMode模式下,然后我们打上kCFRunLoopCommonModes通用标签, 就解决定时器在TableView滚动时停止的问题了
4.2 CFRunLoopTimerRef
CFRunLoopTimerRef是基于时间的触发器(基本上说的就是NSTimer), 一个模式下可以有多个Timer(Arrar中存放)
4.2.1 CFRunLoopTimerRef –>NSTimer
代码演练:

/**
 *  这个方法内部实现是: 创建timer,添加到RunLoop中的默认的Mode中,RunLoop启动这个mode,取出这个mode中timer来用
 */
[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];

/**
 *  上面的代码等同于下面的
 */
// 创建Timer
NSTimer *timer = [NSTimer timerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 定时器只运行在 NSDefaultRunLoopMode 模式下, 一旦RunLoop进入其他模式,这个定时器就不会工作
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];



// 如果拖动时, 我们将定时器添打上这个NSRunLoopCommonModes的标记
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

 NSLog(@"-----------%@", [NSRunLoop currentRunLoop]);

/**
* 定时器会跑在标记为common modes的模式下(这个模式只是个标记)
* RunLoop会寻找带有common标签的模式,有这个标签的,都可以跑
* 打印当前的RunLoop信息输出为:(有common modes标签的有两个,UITrackingRunLoopMode和kCFRunLoopDefaultMode),所以定时器可以在这两个模式下跑, RunLoop只会运行一种模式

 common modes = <CFBasicHash 0x7fb8b2700490 [0x10ec6ba40]>{type = mutable set, count = 2,
 entries =>
 0 : <CFString 0x10fba2210 [0x10ec6ba40]>{contents = "UITrackingRunLoopMode"}
 2 : <CFString 0x10ec8c5e0 [0x10ec6ba40]>{contents = "kCFRunLoopDefaultMode"}
 }
 */

我们可以用这个来做什么呢:
比如: 有些事情,我拖拽的时候,你不能做, 我手松开才可以做, 那么我们可以除了监听ScrollView的滚动来判断外, 我们也可以将想做的事情放到默认的模式kCFRunLoopDefaultMode中 ,滚动时,事情不做,停止滚动,才做事情

如果我们将定时器放到UITrackingRunLoopMode模式下, 则只有在拖动的时候,定时器才可以工作, 代码如下:

[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
// 调用了scheduledTimer返回的NSTimer的定时器对象,已经被自动添加到当前的runLoop中(一个线程对应一个runloop,如果在子线程中添加定时器..添加到子线程的runloop中),默认为NSDefaultRunLoopMode模式
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 如果需要更改模式, 直接这样就可以
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
4.2.2 CFRunLoopTimerRef –>CADisplayLink
CADisplayLink如果NSTimer一样, 也是添加到模式中
CADisplayLink的方法:

  • (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
  • (void)addToRunLoop:(NSRunLoop )runloop forMode:(NSString )mode;
  • (void)removeFromRunLoop:(NSRunLoop )runloop forMode:(NSString )mode;
  • (void)invalidate;
    5 CFRunLoopSourceRef
    CFRunLoopSourceRef是事件源(输入源): 一些触摸事件,点击事件等都是在source中, 由source来触发的, 所以有一些事件过来,就是有一些source过来了, 处理source就是处理事件

以前的分法:(官方文档,source的分类)(理论)
Port-Based Sources: 基于端口的,和其他线程进行交互的或者说是一些内核过来的消息
Custom Input Sources:自定义输入源
Cocoa Perform Selector Sources: [self performSelector:@selector(xxx)];
现在的分法:(按照源码,函数的调用栈来看,source的分类)(实践)
Source0:非基于Port的 处理事件
Source1:基于Port的, 通过内核和其他线程通信,接收,分发系统事件
我们添加一个按钮,看一下函数的调用栈:

调用栈.png
source1是用来接收事件的, 虽然调用栈里面没有source1, 我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event, Event先告诉source1, 然后source1将事件Event分发给source0,然后由source0来处理
所以点击屏幕,触摸到硬件也会唤醒runloop..

performSelector和按钮的点击事件等等事件都是由source来处理的,这些事件一旦发生了,这个事件就会来到runloop,runloop里面就会把source0,source1处理一下,如果没有事件,也没有timer,则runloop就会睡眠, 如果有,则runloop就会被唤醒,然后跑一圈

6 CFRunLoopObserverRef
CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
可以监听的时间点有以下几个:

说明:

1UL <<0 , 2的0次方 1
2UL <<5, 2的5次方 32
代码演练

// 这个方法是KVO,并不是观察runloop的状态,下面的方法才是监听状态

// [NSRunLoop currentRunLoop] addObserver:<#(nonnull NSObject )#> forKeyPath:<#(nonnull NSString )#> options:<#(NSKeyValueObservingOptions)#> context:<#(nullable void *)#>

/**
 *  创建runloop观察着
 *
 *  @param allocator#>  默认的allocator 点进去选择默认的即可
 *  @param activities#> 监听的状态(即将进入runloop,即将处理timer,即将处理source,即将进入休眠,刚从休眠中唤醒,即将退出runloop,监听所有状态)
 *  @param repeats#>    是否重复监听 YES 重复监听 description#>
 *  @param order#>      传0即可, description#>
 *  @param observer     runloop观察着
 *  @param activity     监听到的状态(枚举值)
 *
 *  @return runloop观察着
 */
CFRunLoopObserverRef observe = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

    NSLog(@"监听到runloop状态发生改变---%zd", activity);
    // 这个方法也挺有用的,比如我们想在处理timer之前做一些事情,处理事件之前做一些事情
});


/**
 *  添加观察着,监听runloop的状态
 *
 *  @param rl#>       runloop对象 description#>
 *  @param observer#> runloop观察着 description#>
 *  @param mode#>     runloop的模式 description#>
 */
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observe, kCFRunLoopDefaultMode);

/**
 *  CF CoreFounction框架的东西不受ARC控制,需要自己手动释放
 *  释放observe
 */
CFRelease(observe);

运行程序,打印结果如下: (对照上面的图查看runloop的状态)

2016-06-18 23:36:19.421 RunLoop[61612:760980] 监听到runloop状态发生改变—1
2016-06-18 23:36:19.422 RunLoop[61612:760980] 监听到runloop状态发生改变—2
2016-06-18 23:36:19.422 RunLoop[61612:760980] 监听到runloop状态发生改变—4
2016-06-18 23:36:19.423 RunLoop[61612:760980] 监听到runloop状态发生改变—2
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变—4
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变—2
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变—4
2016-06-18 23:36:19.424 RunLoop[61612:760980] 监听到runloop状态发生改变—32
2016-06-18 23:36:19.425 RunLoop[61612:760980] 监听到runloop状态发生改变—64
……..
这时候那么问题来了, runloop的处理逻辑是什么呢?
看下图:

官方.png

整理.png
测试点击按钮事件:打印情况如下

2016-06-19 00:04:54.843 RunLoop[63122:782041] 监听到runloop状态发生改变—64 被唤醒
2016-06-19 00:04:54.843 RunLoop[63122:782041] 监听到runloop状态发生改变—2 即将处理timer
2016-06-19 00:04:54.844 RunLoop[63122:782041] 监听到runloop状态发生改变—4 即将处理source
2016-06-19 00:04:54.845 RunLoop[63122:782041] 监听到runloop状态发生改变—2
2016-06-19 00:04:54.846 RunLoop[63122:782041] 监听到runloop状态发生改变—4
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变—2
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变—4
2016-06-19 00:04:54.847 RunLoop[63122:782041] 监听到runloop状态发生改变—32
2016-06-19 00:04:54.921 RunLoop[63122:782041] 监听到runloop状态发生改变—64
2016-06-19 00:04:54.922 RunLoop[63122:782041] 监听到runloop状态发生改变—2 即将处理timer
2016-06-19 00:04:54.922 RunLoop[63122:782041] 监听到runloop状态发生改变—4 即将处理source
2016-06-19 00:04:54.923 RunLoop[63122:782041] —ButtonClick— 按钮点击
2016-06-19 00:04:54.923 RunLoop[63122:782041] 监听到runloop状态发生改变—2
2016-06-19 00:04:54.924 RunLoop[63122:782041] 监听到runloop状态发生改变—4
2016-06-19 00:04:54.924 RunLoop[63122:782041] 监听到runloop状态发生改变—32
….后面还有, 最后变为即将进入休眠(32)
这里说明一点: runloop启动,先判断有没有模式,如果模式为空,则退出runloop,如果有模式,然后看看模式中有没有timer,source,observe,如果都没有,则也退出runloop..
这个可以根据runloop的源代码查看, 查看run的调用顺序得出…

7 runloop应用
NSTimer
ImageView显示
PerformSelector
常驻线程
自动释放池

文/东方_未明(简书作者)
原文链接:http://www.jianshu.com/p/3676065e5d5d#comments
著作权归作者所有,转载请联系作者获得授权,并标注“简书作者”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值