《iOS知识点梳理-Runloop》

什么是 RunLoop?

  • 运行循环
  • 内部就是一个 do-while 循环, 在这个循环里面不断的处理各种任务
  • 一个线程对应有一个 RunLoop, 主线程的 RunLoop 默认已经启动, 子线程的 RunLoop 需要手动去启动 (调用 run 方法)
  • RunLoop 只能选择一个 Mode 启动, 如果当前 Mode 中没有任何Source(Sources0、Sources1)、Timer, 那么就直接退出 RunLoop.
  • 基本的作用就是保持程序的持续运行, 处理 app 中的各种事件. 通过 RunLoop, 有事运行, 没事就休息, 可以节省 cpu 资源, 提高程序性能.

RunLoop对象

iOS 中有2套API来访问和使用RunLoop
  • Foundation: NSRunLoop
  • Core Foundation: CFRunLoopRef
  • NSRunLoop 和 CFRunLoopRef 都代表着 RunLoop对象
  • NSRunLoop 是基于 CFRunLoopRef 的一层 OC 包装, 所以要了解 RunLoop内部结构, 需要多研究 CFRunLoopRef 层面的 API

RunLoop和线程

  • 每条线程都有唯一的一个与之对应的 RunLoop 对象
  • 主线程的 RunLoop 已经自动创建好了, 子线程的 RunLoop 需要主动创建
  • RunLoop 在第一次获取时创建, 在线程结束时销毁

获取 RunLoop 对象

  • Foundation
    [NSRunLoop currentRunLoop]; // 获取当前 RunLoop 对象
    [NSRunLoop mainRunLoop]; // 获取主线程 RunLoop 对象
  • Core Foundation
    CFRunLoopGetCurrent(): // 获取当前 RunLoop 对象
    CFRunLoopGetMain(); // 获取主线程 RunLoop 对象

RunLoop相关类

Core Foundation 中关于 RunLoop 的5个类
  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

CFRunLoopModeRef

  • CFRunLoopModeRef 代表 RunLoop 的运行模式.
  • 一个 RunLoop 包含若干个 Mode, 每个 Model 又包含若干个(set)Source/(array)Timer/(array)Observer
  • 每次 RunLoop 启动时, 只能制定其中一个 Mode, 这个 Mode 被称作 CurrentMode
  • 如果需要切换 Mode, 只能退出 Loop, 重新制定一个 Mode 再进入
  • mode 主要是用来制定事件在运行循环中的优先级, 分为:
    1. NSDefaultRunLoopMode (kCFRunLoopDefaultMode): 默认, 空闲状态
    2. UITrackingRunLoopMode: ScrollView 滑动时会切换到这个Mode
    3. UIInitializationRunLoopMode: run loop 启动时, 会切换到该 Mode
    4. NSRunLoopCommonModes (kCFRunLoopCommonModes) : mode 集合

苹果公开提供的mode有俩个: NSDefaultRunLoopMode (kCFRunLoopDefaultMode), NSRunLoopCommonModes (kCFRunLoopCommonModes)

CFRunLoopTimerRef

  • CFRunLoopTimerRef 是基于时间的触发器
  • CFRunLoopTimerRef 基本上说的就是 NSTimer, 它受 RunLoop 的 Mode 的影响

CFRunLoopSourceRef

  • CFRunLoopSourceRef 是事件源 (输入源)
  • 按照官方的文档, Source 的分类
    1. Port-Based Source
    2. Custom Input Sources
    3. Cocoa Perform Selector
  • 按照函数调用栈, Source 的分类
    1. Source0: 非基于 Port 的
    2. Source1: 基于 Port 的, 通过内核和其他线程通信, 接受、分发系统事件

CFRunLoopObserverRef

  • CFRunLoopObserverRef 是观察者, 能够箭筒 RunLoop 的改变状态
  • 可以监听的时间点有以下几个:
    1. kcfRunLoopEntry (即将进入 loop ) // 1
    2. kcfRunLoopBeforeTimers (即将处理 Timer) // 2
    3. kcfRunLoopBeforeSource (即将处理 source) // 4
    4. kcfRunLoopBeforeWaiting (即将进入休眠) // 32
    5. kcfRunLoopAfterWaiting (刚从休眠中唤醒) // 64
    6. kcfRunLoopExit (即将退出 loop) // 128
  • 添加观察者
CFRunLoopObserverRef observer =
CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(),
kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer,
CFRunLoopActivity activity) {
NSLog(@"----􁍊􀞍􀚩RunLoop􁇫􀮾􀝎􁊞􀶯􀝒---%zd", activity);
});
// 􁂲􀛒􁥡􀩊􁘏􀒓􁍊􀞍RunLoop􁌱􁇫􀮾
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer,
kCFRunLoopDefaultMode);
// 􁯽􀶱Observer
CFRelease(observer);

RunLoop处理逻辑

  • 通知 Observer: 即将进入 Loop (1)
  • 通知 Observer: 将要处理 Timer (2)
  • 通知 Observer: 将要处理 Source0 (3)
  • 处理 Source0 (4)
  • 如果有 Source0, 跳到第9步(5)
  • 通知 Observer: 线程即将休眠(6)
  • 休眠, 等待唤醒: (7)
    1. Source0(port).
    2. timer 启动
    3. RunLoop 设置的 Timer 已经超时
    4. RunLoop 被外部手动唤醒
  • 通知 Observer: 线程将被唤醒 (8)
  • 处理未处理的时间 (9)
    1. 如果用户定义的定时器启动, 处理定时器时间并重启 RunLoop. 进入步骤 (2)
    2. 如果输入源启动, 传递相应的消息.
    3. 如果 RunLoop 被显示唤醒二时间还没有超时, 重启 RunLoop, 进入步骤 (2)
  • 通知 Observer: 即将退出 Loop

RunLoop的应用

  • NSTimer
  • ImageView 显示
  • PerformSelector
  • 常驻线程
  • 自动释放池

RunLoop 定时源和输入源

  • RunLoop 处理的输入事件有俩种不同的来源: 输入源 (input source) 和定时源 (timer source).
  • 输入源传递异步消息, 通常来自于其他线程或程序.
  • 定时源则传递同步消息, 在特定时间或者一定时间间隔发生.

NSRunLoop 的实现机制, 以及在多线程中如何使用

  • 实现机制: RunLoop 的基本作用, 处理逻辑.
  • 程序创建子程序的时候, 才需要手动启动 runLoop. 主线程的 runLoop 已经默认启动.
  • 在多线程中, 你需要判断是否需要 RunLoop. 如果需要 RunLoop, 那么你要负责配置 RunLoop 并启动. 你不需要在任何情况下都去启动 RunLoop. 比如, 你使用线程去处理一个预先定义好的耗时极长的任务时, 你就可以无需启动 RunLoop. RunLoop 只在你要和线程有交互事才需要.

RunLoop和线程有什么关系?

  • 主线程的 RunLoop 默认是启动的
    iOS的应用程序里面, 程序启动后会有一个如下的main () 函数
    int main(int argc, char * argv[]) {
      @autoreleasepool {
          return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
        }
    }
    
    重点的是 UIApplicationMain()函数, 这个方法会为 mainThread 设置一个 RunLoop 对象.
    这就解释了: 为什么我们的应用可以在无人操作的时候休息, 需要让它干活的时候又能立马响应.
  • 对其他的线程来说, RunLoop 默认是没有启动的, RunLoop 只有你在要和线程有交互的时候才有需要.
  • 在任何一个 coco 程序中, 都可以通过下面的代码来获取当前的 RunLoop.
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    

autorelease 对象在什么情况下会被释放?

  • 分俩种情况: 手动干预释放和系统自动释放.
  • 手动干预释放就是指定 autoreleasePool, Autorelease 对象会在当前的 runLoop 迭代结束时释放.
  • kCFRunLoopEntry(1): 第一次进去自动创建一个 autorelease
  • kCFRunLoopBeforeWaiting(32): 进入休眠状态前会自动销毁一个 autorelease, 然后重新创建一个新的 autorelease
  • kCFRunLoopExit(128): 退出 RunLoop 时会自动销毁最后一个创建的 autorelease

测试, RunLoop 的理解不正确的是?

 A 每一个线程都有其对应的RunLoop
 B 默认非主线程的RunLoop是没有运行的
 C 在一个单独的线程中没有必要去启用RunLoop
 D 可以将NSTimer添加到runloop中
  • 参考答案: C
  • 理由: RunLoop, 它是多线程的法宝, 通常来说一个线程一次只执行一次任务, 执行完任务会退出线程. 但是, 对于主线程是不能退出的, 因此我们需要让主线程即时任务执行完毕, 也可以继续等待接受事件而不退出,那么 RunLoop 就成关键法宝了. 但是非主线程通常来说就是为了执行某一任务的, 执行完毕冀需要归还资源, 因此默认是不运行 RunLoop 的. NSRunLoop 提供了一个添加 NSTimer 的方法, 这个方法是正常状态下就会回调.

RunLoop 的 Mode 作用是什么?

mode 主要是用来指定时间在运行循环中的优先级, 分为:
  • NSDefaultRunLoopMode (kCFRunLoopDefaultMode): 默认, 空闲状态
  • UITrackingRunLoopMode: ScrollView 滑动的时候会切换到这个 mode
  • UIInitializationRunLoopMode: RunLoop 启动时, 会切换到该 mode
  • NSRunLoopCommonModes (kCFRunLoopCommonModes) : Mode 集合
苹果公开提供的 Mode 有俩个:
  • NSDefaultRunLoopMode (kCFRunLoopDefaultMode)
  • NSRunLoopCommonModes (kCFRunLoopCommonModes)
    如果我们把一个 NSTimer 对象以 NSDefaultRunLoopMode (kCFRunLoopDefaultMode) 添加到主运行循环中的时候, ScrollView 的滑动会导致 Mode 的切换, 而导致 NSTimer 将不再被调度, 如果希望滑动的时候也能够被调度, 我们就可以是用 NSRunLoopCommonMode (包含, NSDefaultRunLoopMode 和 NSTrackingRunLoopMode 俩个状态)

测试, 请写出 NSTimer 使用时的注意事项

思路: 如果想要销毁 timer , 应该先把 timer 置为失效, 否则 timer 就一直占用内存而不会释放. 造成逻辑上的内存泄漏. 而且这种泄漏不能用 Xcode 和 instruments 测出来. 未将 timer 置为失效, 每次创建一次, 则之前的不能得到释放, 那么同时存在多个 timer 的实例在内存中.

参考答案:
  • 注意 timer 添加到 runloop 时应该设置什么 mode.
  • 注意timer 在不需要时, 一定要调用 invalidate 方法使定时器失效, 否则得不到释放.

测试, UITableViewCell 上有个 UILabel, 显示 NSTimer 实现的秒表时间, 手指滚动 cell 过程中, label 是否刷新, 为什么?

思路同上, 自己作答.

测试, 为什么 UIScrollView 的滚动会导致 NSTimer 失效?

思路同上, 自己作答.

测试, 在滑动页面上的列表, timer 会暂停回调, 为什么? 如何解决?

思路同上, 自己作答.

在开发中如何使用 RunLoop? 什么应用场景?

  • 开启一个常驻线程 (让一个子线程不进入消亡状态, 等待其他线程发来消息, 处理其他事情)
  • 在子线程开启一个定时器
  • 在子线程中进行一些长期监控
  • 可以控制定时器在特定模式下执行
  • 可以让某些事件 (行为, 任务) 在特定模式下执行
  • 可以添加 Observer 监听 RunLoop 的状态, 比如监听点击事件的处理 (在所有点击事件之前做一些事情)

你在开发过程中常用到哪些定时器,定时器时间会有误差吗,如果有,为什么会有误差?

iOS中常NSTimer、CADisplayLink、GCD定时器,其中NSTimer、CADisplayLink基于NSRunLoop实现,故存在误差,GCD定时器只依赖系统内核,相对一前两者是比较准时的。

误差原因是:与NSRunLoop机制有关, 因为RunLoop每跑完一次圈再去检查当前累计时间是否已经达到定时设置的间隔时间,如果未达到,RunLoop将进入下一轮任务,待任务结束之后再去检查当前累计时间,而此时的累计时间可能已经超过了定时器的间隔时间,故会存在误差。

参考《iOS常见三种定时器-NSTimer、CADisplayLink、GCD定时器》

2. NSTimer、CADisplayLink会产生循环引用吗?如果会,你是如何解决的?

如果直接使用,会产生循环引用问题。可以增加一个中间类,给这个类添加一个用weak修饰的id 类型target属性,并重写中间类的消息转发方法。实现如下代码:

声明文件.h:

#import <Foundation/Foundation.h>

@interface LXProxy : NSProxy
+ (instancetype)proxyWithTarget:(id)target;

@end

复制代码

实现文件.m

#import "LXProxy.h"

@interface LXProxy ()

/** weak target*/
@property (nonatomic, weak) id target;

@end

@implementation LXProxy

+ (instancetype)proxyWithTarget:(id)target{
LXProxy *proxy = [LXProxy alloc];
proxy.target = target;

return proxy;

}

- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel{
return [self.target methodSignatureForSelector:sel];

}

- (void)forwardInvocation:(NSInvocation *)invocation{
[invocation invokeWithTarget:self.target];

}

@end
复制代码

调用代码:

 _timer = [NSTimer scheduledTimerWithTimeInterval:2 target:[LXProxy proxyWithTarget:self] selector:@selector(test) userInfo:nil repeats:YES];

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

推荐阅读更多精彩内容

  • Run loop 剖析:Runloop 接收的输入事件来自两种不同的源:输入源(intput source)和定时...
    Mitchell阅读 12,360评论 17 111
  • 前言 最近离职了,可以尽情熬夜写点总结,不用担心第二天上班爽并蛋疼着,这篇的主角 RunLoop 一座大山,涵盖的...
    zerocc2014阅读 12,340评论 13 67
  • 转自http://blog.ibireme.com/2015/05/18/runloop 深入理解RunLoop ...
    飘金阅读 937评论 0 4
  • 一、什么是runloop 字面意思是“消息循环、运行循环”。它不是线程,但它和线程息息相关。一般来讲,一个线程一次...
    WeiHing阅读 8,031评论 11 109
  • 文/白衡 我努力保持微笑的样子 只是因为 不想让眼泪有空隙可钻 当我习惯了微笑 竟忘记了还有哭 这个真实而又简单的...
    白衡阅读 236评论 2 2