iOS学习笔记【三】——RunLoop
只做简单笔记📝 详细请戳标题链接🔗
key point:一个RunLoop对象,这个对象在循环中用来处理程序运行过程中出现的各种事件,从而保持程序的持续运行。在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能。主要包含了一个线程,若干个Mode,若干个commonMode,还有一个当前运行的Mode。
struct __CFRunLoop {
CFRuntimeBase _base;
pthread_mutex_t _lock; /* locked for accessing mode list */
__CFPort _wakeUpPort; // used for CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
Boolean _unused;
volatile _per_run_data *_perRunData; // reset for runs of the run loop
pthread_t _pthread; //RunLoop对应的线程
uint32_t _winthread;
CFMutableSetRef _commonModes; //存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; //当前运行的mode
CFMutableSetRef _modes; //存储的是CFRunLoopModeRef
struct _block_item *_blocks_head;//doblocks的时候用到
struct _block_item *_blocks_tail;
CFTypeRef _counterpart;
};
RunLoop与线程
- Runloop 和线程一一对应,RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
- 主线程的RunLoop会在初始化全局字典时创建;子线程的RunLoop会在第一次获取的时候创建;RunLoop会在线程结束时销毁。
- RunLoop 就是线程中的一个循环,RunLoop 会在循环中会不断检测,通过 Input sources(输入源)和 Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候让线程进行休息。
RunLoop Mode
- Mode可以视为事件的管家,一个Mode管理着各种事件,Runloop总是运行在某种特定的mode下,每种Runloop都可以包含若干个Mode。
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _source0;
CFMutableSetRef _source1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
};
NSDefaultRunLoopMode // App的默认运行模式
NSEventTrackingRunLoopMode // 跟踪用户交互事件
NSRunLoopCommonModes // Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode
NSConnectionReplyMode
NSModalPanelRunLoopMode
RunLoop Source
CFRunLoopSource分source0和source1两个版本。source0是非基于Port的,负责App内部事件,由App负责管理触发,例如UITouch事件;source1由RunLoop和内核管理,可以接收内核消息并触发回调。
上图中的Input Source Port是对应的Source1。Source1和Timer都属于端口事件源,不同的是所有的Timer都共用一个Port,而每个Source1都有不同的对应Port。
RunLoop执行逻辑
实际应用
- 后台常驻线程:添加一条用于常驻内存的强引用的子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop。
- (void)viewDidLoad {
[super viewDidLoad];
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil]; // 创建线程
[self.thread start]; // 开启线程
}
- (void) run1 {
NSLog(@"----run1-----"); // 这里写任务
// 因为runloop如果没有CFRunLoopSourceRef事件源输入或者定时器,就会立马消亡
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"未开启RunLoop"); // 测试是否开启了RunLoop
}
- 怎样保证子线程数据回来更新UI的时候不打断用户的滑动操作
可以将更新UI事件放在主线程的NSDefaultRunLoopMode上执行即可,这样就会等用户不再滑动页面,主线程RunLoop由UITrackingRunLoopMode切换到NSDefaultRunLoopMode时再去更新UI。
NSTimer
- 开启timer有两种方式:以scheduled开头的方法默认创建了一个NSTimer并自动添加到当前线程的Runloop中的defaultMode;以timer开头的方法需要调用addTimer:forMode:方法手动将其添加到运行循环中。
- 创建的定时器必须添加到RunLoop中才能触发。
- 定时器与NSRunLoop对象一起工作,它的准确性有限。如果定时器的触发时间到了,而运行循环处于阻塞状态,此时定时器的触发时间就会推迟到下一个运行循环周期。
fire():立即触发定时器或者停用的定时器需要再次被触发。对于重复定时器,立即触发之后会在指定的触发日期或者设定的间隔时间过后再次重复触发;对于非重复定时器,立即触发之后定时器会自动失效,即使到了指定的触发时间也不再触发。
invalidate():停止定时器再次触发,并使得定时器从其RunLoop中删除,删除其相关的target和userInfo等信息
fireDate:指定的触发日期,定时器首次触发的时间
timeInterval:以秒为单位,如果小于或等于0.0,则按0.1毫秒处理
tolerance:NSTimer触发时间受RunLoop调度影响,过时不候。tolerance属性让用户可以设置可以容忍的触发的时间范围
NSTimer常见问题
- 无法释放 内存泄漏
- 如果repeats为NO,则不需要手动调用invalidate:当定时器执行的时候一直是强引用target,当定时器执行一次结束后,系统自动调用invalidate方法,从而解除强引用
- timer强引用target,在我们将NSTimer加入到RunLoop中之后,RunLoop就会维持对NSTimer的强引用。不管我们是否在target中对NSTimer进行强引用,在我们没有将NSTimer进行invalidate之前,NSTimer都无法被释放。timer运行的时候释放不了,导致被强引用的target也无法释放,并非循环引用导致不释放。
- 解决办法:
- 手动释放NSTimer
- 使用block解决循环引用
- 使用中间件YYWeakProxy解决循环引用。使NSTimer强引用中间件,而中间件弱引用target
- 调用苹果系统API(iOS10以后支持)。NSTimer类多了如下的两个接口,这两个接口不用传target和selector。这不就意味着NSTimer不会强持有target。
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block;
- 以scheduledTimerWithTimeInterval的方式触发的timer,在滑动页面上的列表时,timer会暂停。
- 原因:RunLoop的mode从default切换到Tracking,导致计时器失效。
- 解决:创建时将timer加入到CommonModes中。
- NSTimer由于RunLoop的原因,不够精确
- 解决办法:使用CADisplayLink或GCD定时器替代
- 延时函数
- 如果performSelector系列函数,会在内部创建一个 NSTimer,然后添加到当前线程的RunLoop中。如果当前线程没有开启RunLoop,该方法会失效。
- 若使用dispatch_after,由于其内部使用的是dispatch_time_t管理时间,而不是NSTimer。所以如果在子线程中调用,相比performSelector:afterDelay,不用关心runloop是否开启。
[self performSelector:@selector(test) withObject:nil afterDelay:10];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2 * NSEC_PER_SEC)));