RunLoop

iOS开发过程中,RunLoop对于我们平常开发一般很少用到,一般在定时器使用时候我们可能使用一下定时器。但是却不能否认其重要性,App在运行过程中一直等待接收用户的事件,在没有事件触发的时候,App没有动作,但是有事件时,他就能响应,这就是RunLoop做的事。

我是从问题出发对RunLoop进行理解。

什么是RunLoop
  • (翻译)事件循环
  • 一般来说,一个线程执行任务是有起点和终点,执行完任务结束线程的生命周期。而RunLoop的则能让线程处理完任务不退出,还能随时处理任务。RunLoop的本质就是do while循环。能让线程处于 "接受消息->等待->处理" 的循环中,直到这个循环结束。
RunLoop的作用
  • RunLoop可以让线程在没有消息响应的时候休眠以避免资源的浪费,在有消息时被唤醒做任务。
RunLoop和线程
  • 线程的创建
    1 主线程中RunLoop默认开启,可以使用[NSRunLoop mainRunLoop]来获取RunLoop
    2 非主线程,可以调用[NSRunLoop currentRunLoop]来获取当前线程的RunLoop
  • RunLoop和线程是一一对应的,RunLoop保证了线程做完一个不会立即退出,等待下一个消息让线程进入休眠,接收消息时唤醒线程处理任务。是基于线程进行操作的一个对象。
RunLoop 对外的接口
  • CoreFoundationRunLoop的5个类
    1 CFRunLoopRef
    2 CFRunLoopModeRef
    3 CFRunLoopSourceRef
    4 CFRunLoopTimerRef
    5 CFRunLoopObserverRef
  • 关系
    关系.png

    解释: 一个RunLoop中有多个mode,一个mode中有多个Source/Timer/Observer。但是一个RunLoop只能指定一个mode,要切换mode,只能退出当前RunLoop,重新设置mode
RunLoop中的 Mode
  • 三种开发中常用的mode

1 kCFRunLoopDefaultMode(CFRunLoop)/NSDefaultRunLoopMode(NSRunLoop)
默认mode,通常主线程是在这个 Mode 下运行的

2 UITrackingRunLoopMode(CFRunLoop)/UITrackingRunLoopMode(NSRunLoop)
ScrollView滚动的时候的模式,保证滑动的时候不受其他mode影响

3 kCFRunLoopCommonModes(CFRunLoop)/NSRunLoopCommonModes(NSRunLoop)
是一个mode组合,平常使用过程相当于NSDefaultRunLoopModeNSEventTrackingRunLoopMode组合。

RunLoop中的 Source
  • RunLoop中的Source是输入源事件,包括Source0Source1
  • Source0: event事件,只含有回调,需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用CFRunLoopWakeUp(runloop) 来唤醒RunLoop
  • Source1: 包含了一个 mach_port 和一个回调,被用于通过内核和其他线程相互发送消息,能主动唤醒RunLoop 的线程。

我们可以在- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event方法中添加断点,可以看出

屏幕快照 2018-07-05 下午4.56.59.png

在button的点击事件中添加断点

// 按钮响应
- (void)click:(UIButton *)button {
    NSLog(@"点击");
}

可以看出


屏幕快照 2018-07-05 下午5.05.16.png

包括KVO的回调

屏幕快照 2018-07-05 下午5.28.02.png

我们平常的大部分事件,都是通过Source0进行回调处理的。

RunLoop中的 Timer

Timer即为定时源事件,包含一个时间长度和回调。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。NSTimer定时器的触发就是基于RunLoop。但是定时器并不是一定准确,NSTimer提供了一个tolerance属性用于设置宽容度,使定时器更加准确。

  • 同样创建NSTimer,在timer的触发打断点


    屏幕快照 2018-07-05 下午5.31.24.png

可以看出NSTimer是定时源事件。

RunLoop中的 Observer

这是观察者,但是和我们平常使用的观察者Observer是两个概念。这里的Observer包含了一个回调,观察的是RunLoop中状态的改变,当状态改变,Observer就能收到通知。可观察的状态是个枚举值为

/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
     kCFRunLoopEntry = (1UL << 0),        即将进入runloop
     kCFRunLoopBeforeTimers = (1UL << 1), 即将处理timer事件
     kCFRunLoopBeforeSources = (1UL << 2),即将处理source事件
     kCFRunLoopBeforeWaiting = (1UL << 5),即将进入睡眠
     kCFRunLoopAfterWaiting = (1UL << 6), 被唤醒
     kCFRunLoopExit = (1UL << 7),         runloop退出
     kCFRunLoopAllActivities = 0x0FFFFFFFU ,所有状态
};
如果NSTimer在分线程中创建,会发生什么,应该注意什么?如何设计一个准确的timer?

我们要先明白NSTimer创建的两个类方法的影响

+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti target:(id)aTarget selector:(SEL)aSelector userInfo:(nullable id)userInfo repeats:(BOOL)yesOrNo;
  • 我们使用方法一创建NSTimer,发现不加入RunLoop也能打印正常
 _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
屏幕快照 2018-07-06 下午5.29.40.png
  • 我们使用方法二创建NSTimer,发现不加入runloop不能循环打印
_timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];

通过xcode的方法注释

屏幕快照 2018-07-06 下午5.34.55.png

我们可以看出,通过scheduledTimerWithTimeInterval :方法创建的NSTimer会自动以defaultMode模式加入到当前的RunLoop中。而timerWithTimeInterval :方法需要手动调用[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes]方式加入RunLoop

同时我们注意到,打印出来的值不是以绝对0.1的速度调用,还是有偏差的。

  • 子线程中创建NSTimer
// 将定时器的创建放在子线程中
[self performSelectorInBackground:@selector(addTimer) withObject:nil];
- (void)addTimer {
// 创建定时器,将定时器加入当前线程的RunLoop中
//    _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
    _timer = [NSTimer timerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes];
}

结果控制台啥也没打印。
原因 : 子线程的RunLoop不是默认开启的,主线程的RunLoop是默认开启的,需要程序手动调用run方法

[[NSRunLoop currentRunLoop] run];
  • 我们可以得出,NSTimer在分线程中创建,如果不主动开启RunLoop,定时器不会调用。如果想要准确的定时器。可以使用GCD定时器。GCD定时器不受RunLoop约束,比NSTimer更加准时
当scrollView滑动时,同页面上的定时器为什么会暂停?
  • scheduledTimerWithTimeInterval方式创建的NSTimer,默认是以NSDefaultRunLoopMode加入到当前线程中,但是当页面滚动时,当前线程RunLoop的mode会自动切换成UITrackingRunLoopMode。以NSDefaultRunLoopMode注册的定时器是不会执行的。

  • 添加[[NSRunLoop currentRunLoop] addTimer:_timer forMode:NSRunLoopCommonModes],将NSTimer加到NSRunLoopCommonModes中。这个模式等NSDefaultRunLoopModeUITrackingRunLoopMode的结合

怎么在tableview滑动时延迟加载图片来提高流畅度?
  • 在滑动过程中,RunLoop会自动切换到UITrackingRunLoopMode中,我们可以将图片的加载运行在NSDefaultRunLoopMode模式中,这样在滑动过程中就不会进行图片的加载,提高流畅性。

  • 我们可以利用performSelector方法

[contentImage performSelector:@selector(sd_setImageWithURL:) withObject:[NSURL URLWithString:[NSString stringWithFormat:@"%@!360", dic[@"min"]]] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

但是这个方法在tableView这样可以复用的视图上,会出现每次滑动屏幕外的都会先加载默认图,当停止滑动时开始图片加载。当取消复用或者在UIScrollView这样的还可以。

常说的AFNetworking常驻线程保活是什么原理?
  • 常驻线程,保持线程RunLoop一直在跑,一直处理事件。
    为了保证线程长期运转,可以在子线程中加入RunLoop,并且给Runloop设置item,防止Runloop自动退出
 //创建一个线程,
static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(createRunloop) object:nil] ;
        [self.thread start];
    });
- (void)createRunloop{
    @autoreleasepool {

   /*
    添加一个Source1事件的监听端口
    RunLoop对象会一直监听这个端口,由于这个端口不会有任何事件到来所以不会产生影响
    */
  [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
        
   //开启runloop
  [[NSRunLoop currentRunLoop] run];
    }
}
RunLoop模式的原理和使用注意点?
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source、Observer、Timer
  • 每次RunLoop启动,只能指定一个Mode,这个Mode被称为CurrentMode
  • 如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入, 以使不同组之间的Source、Observer、Timer互不受影响
如果程序启动就需要执行一个耗时操作,你会怎么做?
  • 新建一个子线程,开启子线程的runloop去操作耗时操作。
  • 使用GCD异步执行耗时操作。
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

}
RunLoop与autoreleasepool的关系
  • 要知道RunLoopautoreleasepool的关系,首先要知道autoreleasepool的内部原理。推荐几篇文章
  1. Objective-C Autorelease Pool 的实现原理
  2. 自动释放池的前世今生 ---- 深入解析 Autoreleasepool
  3. 黑幕背后的Autorelease
    我也是参考这几个大神的文章总结
  • 主要了解的知识点。
    1 AutoreleasePoolPage : 看下源码中的定义。
class AutoreleasePoolPage {
    magic_t const magic;
    id *next;
    pthread_t const thread;
    AutoreleasePoolPage * const parent;
    AutoreleasePoolPage *child;
    uint32_t const depth;
    uint32_t hiwat;
};

其中

`magic` : 用于对当前 AutoreleasePoolPage 完整性的校验
`next` : 指向最新加入栈顶的下一个对象的位置。初始化时指向 begin()
`thread` : 指向当前线程
`parent` : 指向父结点,第一个结点的 parent 值为 nil ;
`child` : 指向子结点,最后一个结点的 child 值为 nil ;

概念了解:

  1. AutoreleasePool并没有单独的结构,而是由若干个AutoreleasePoolPage以双向链表的形式组合而成。parent指针为AutoreleasePoolPage的前驱节点,child为后继节点。
  2. 一个 AutoreleasePool对应一个线程。
  3. AutoreleasePoolPage会开辟4096字节内存,当一个AutoreleasePoolPage空间满了后,会新建一个AutoreleasePoolPage,通过child指针指向新的AutoreleasePoolPage连接,之后需要autorelease的对象就会在最新的page中添加。
  4. 向一个对象发送autorelease,就是将对象加入到栈顶的next指针指向的位置。
  • POOL_SENTINEL(哨兵对象)
  1. POOL_SENTINEL 只是 nil 的别名,代表的意义是一个 AutoreleasePool的边界。这个在objc_autoreleasePoolPushobjc_autoreleasePoolPop方法中有大作用。
  • objc_autoreleasePoolPush
  1. 当调用objc_autoreleasePoolPush,会创建一个新的AutoreleasePool。即向当前的AutoreleasePoolPage插入一个哨兵对象(POOL_SENTINEL),可以理解为一个runloop开始的边界。并且返回插入的POOL_SENTINEL的内存地址。
  • objc_autoreleasePoolPop
  1. 当调用objc_autoreleasePoolPop,就会向自动释放池中的对象发送 release 消息,直到 POOL_SENTINEL所在的page。
  • autoreleasepool什么时候释放
  1. App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是 _wrapRunLoopWithAutoreleasePoolHandler()。

  2. 第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

  3. 第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

  4. 在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

  5. 注意:每个线程中都可以有多个AutoreleasePool。

RunLoop与PerformSelecter

我们可以看系统RunLoop.h中有关于performSelector的方法。。

@interface NSObject (NSDelayedPerforming)

- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray<NSRunLoopMode> *)modes;
- (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget;

@end

我们可以通过文档来初步了解,performSelector延时调用的方法。


屏幕快照 2018-07-15 下午6.56.55.png
  • - (void)performSelector:(SEL)aSelector withObject:(nullable id)anArgument afterDelay:(NSTimeInterval)delay
  1. 方法内部设置了一个timer,运行在当前RunLoop中,运行模式为NSDefaultRunLoopMode
  2. 如果当前RunLoop为子线程,RunLoop默认不开启,则添加performSelector会无法执行。
  3. 如果运行循环在NSDefaultRunLoopMode下运行,则成功;否则,计时器将等待,直到运行循环处于默认模式。
  4. 总结的来说方法类似在运行时添加了个NSTimer,NSTimer指定了一个SEL,和NSTimer一样默认运行在NSDefaultRunLoopMode,当ScrollView滚动时因为运行模式的切换,会出现无法调用定时器的情况。
  • 测试
    我们通过,performSelectorInBackground在后台线程添加方法。
[self performSelectorInBackground:@selector(addTimer) withObject:nil];

在这个后台线程中执行performSelector的延时调用方法。

- (void)addTimer {
     [self performSelector:@selector(timerEvent) withObject:nil afterDelay:1]; 
}

- (void)timerEvent {
    NSLog(@"时间回调");
}

发现控制台无法打印。

当我们主动开启runloop时,

[self performSelector:@selector(timerEvent) withObject:nil afterDelay:1];
[[NSRunLoop currentRunLoop] run];

打印

2018-07-16 10:59:38.311387+0800 runloop[41589:5309173] 时间回调
  • 当然还是取消延时执行的方法。
/**
 *  取消延迟执行
 *
 *  @param aTarget    一般填self
 *  @param aSelector  延迟执行的方法
 *  @param anArgument 设置延迟执行时填写的参数(必须和上面performSelector方法中的参数一样)
 */
+ (void)cancelPreviousPerformRequestsWithTarget:(id)aTarget selector:(SEL)aSelector object:(nullable id)anArgument;

还有很多问题,留着更新

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,165评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,720评论 1 298
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,849评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,245评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,596评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,747评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,977评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,708评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,448评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,657评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,141评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,493评论 3 258
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,153评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,890评论 0 198
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,799评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,685评论 2 272