RunLoop

2018-07-16  本文已影响26人  Maj_sunshine

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

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

什么是RunLoop
RunLoop的作用
RunLoop和线程
RunLoop 对外的接口
RunLoop中的 Mode

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

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

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

RunLoop中的 Source

我们可以在- (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是定时源事件。

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;
 _timer = [NSTimer scheduledTimerWithTimeInterval:0.1 target:self selector:@selector(timerEvent) userInfo:nil repeats:YES];
屏幕快照 2018-07-06 下午5.29.40.png
_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的速度调用,还是有偏差的。

// 将定时器的创建放在子线程中
[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];
当scrollView滑动时,同页面上的定时器为什么会暂停?
怎么在tableview滑动时延迟加载图片来提高流畅度?
[contentImage performSelector:@selector(sd_setImageWithURL:) withObject:[NSURL URLWithString:[NSString stringWithFormat:@"%@!360", dic[@"min"]]] afterDelay:0 inModes:@[NSDefaultRunLoopMode]];

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

常说的AFNetworking常驻线程保活是什么原理?
 //创建一个线程,
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模式的原理和使用注意点?
如果程序启动就需要执行一个耗时操作,你会怎么做?
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{

}
RunLoop与autoreleasepool的关系
  1. Objective-C Autorelease Pool 的实现原理
  2. 自动释放池的前世今生 ---- 深入解析 Autoreleasepool
  3. 黑幕背后的Autorelease
    我也是参考这几个大神的文章总结
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指针指向的位置。
  1. POOL_SENTINEL 只是 nil 的别名,代表的意义是一个 AutoreleasePool的边界。这个在objc_autoreleasePoolPushobjc_autoreleasePoolPop方法中有大作用。
  1. 当调用objc_autoreleasePoolPush,会创建一个新的AutoreleasePool。即向当前的AutoreleasePoolPage插入一个哨兵对象(POOL_SENTINEL),可以理解为一个runloop开始的边界。并且返回插入的POOL_SENTINEL的内存地址。
  1. 当调用objc_autoreleasePoolPop,就会向自动释放池中的对象发送 release 消息,直到 POOL_SENTINEL所在的page。
  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
  1. 方法内部设置了一个timer,运行在当前RunLoop中,运行模式为NSDefaultRunLoopMode
  2. 如果当前RunLoop为子线程,RunLoop默认不开启,则添加performSelector会无法执行。
  3. 如果运行循环在NSDefaultRunLoopMode下运行,则成功;否则,计时器将等待,直到运行循环处于默认模式。
  4. 总结的来说方法类似在运行时添加了个NSTimer,NSTimer指定了一个SEL,和NSTimer一样默认运行在NSDefaultRunLoopMode,当ScrollView滚动时因为运行模式的切换,会出现无法调用定时器的情况。
[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;

还有很多问题,留着更新

上一篇 下一篇

猜你喜欢

热点阅读