RunLoop数据结构
OC中为我们提供了两个Runloop
NSRunloop位于Foundation框架,CFRunloop位于CoreFoundation框架
NSRunloop是CFRunloop的封装,提供了面向对象的API
RunLoop内部数据结构之间的关系
source,observers和timers统称为Item
任何一个item都依赖于mode,任何一个mode都依赖于runloop
线程和RunLoop是一一对应的
RunLoop和Mode是一对多的关系,一个RunLoop可以有多个Mode
mode和对应的Source,Timer,Observer也是一对多的关系
CFRunLoop
下图表示runloop是个结构体对象
- pthead: C级别的线程对象,代表线程,说明Runloop和线程是一一对应的关系
- currentMode: 代表Runloop当前所处的模式,实际上是CFRunLoopMode数据结构
- modes: 多个mode的一个集合,它是NSMutableSet<CFRunLoopMode*>集合,里面的元素是CFRunLoopMode类型的
- commonModes: 也是NSMutableSet<NSString*>集合,里面元素都是字符串
- commonModeItems: 也是一个集合,但这个集合中会有多个元素,包含多个Observer,多个Timer,多个Source
CFRunloopMode
每次运行,只会依赖于一个mode
name: 字符串类型,对应某一个RunLoop的mode的名称,我们经常用的NSDefaultRunLoopMode,当我们调用Runloop的一些API去运行在指定模式下时,是通过模式名称来找到对应的Runloop模式的, commonModes里面的字符串元素就是不同模式的名称
一个RunLoop可以有多个Mode,每个Mode中又可以有多个sources,observers,timers
之所以有多个Mode的原因:
假设当RunLoop运行在Mode1上时,如果此时Mode2的某个observers事件回调了,我们是没办法接收到Mode2中回调来的事件的
之所以RunLoop有多个Mode,就是为了起到这样一个屏蔽效果
当我们运行在Mode1上时,只能接收处理Mode1上的sources, observers以及Timers
其他Mode的回调事件,是不会给与处理的
tableView里面如果有滚动的广告栏,当我们滑动tableView时,广告栏就不滚动了
如果个一Timer想同时加入到两个mode里面,需要怎样做?
NSRunLoopCommonModes
- CommonModex 不是实际存在的一种Mode
- 是同步Source / Timer / Observer到多个Mode中的一种技术解决方案
定时器Demo
#import "ViewController.h"
@interface ViewController ()<UITextViewDelegate>
@property (weak, nonatomic) IBOutlet UITextView *textView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"fire in home -- %@",[[NSRunLoop currentRunLoop] currentMode]);
}];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
self.textView.delegate = self;
}
当把Mode设置为NSRunLoopCommonModes时候,手指滑动textView,计时器仍在计时,如下图可以看到Mode变化了
但如果当我们将mode改为defaultMode,那么在我们手指滑动时计时器就不响应了
CFRunLoopSource
source0 Demo
一旦我们调用了source0Demo, schedule, perform和cancel都会执行
- (void)source0Demo{
CFRunLoopSourceContext context = {
0,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
schedule,
cancel,
perform,
};
/**
参数一:传递NULL或kCFAllocatorDefault以使用当前默认分配器。
参数二:优先级索引,指示处理运行循环源的顺序。这里我传0为了的就是自主回调
参数三:为运行循环源保存上下文信息的结构
*/
CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
CFRunLoopRef rlp = CFRunLoopGetCurrent();
// source --> runloop 指定了mode 那么此时我们source就进入待绪状态
CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode);
// 一个执行信号
CFRunLoopSourceSignal(source0);
// 唤醒 run loop 防止沉睡状态
CFRunLoopWakeUp(rlp);
// 取消 移除
CFRunLoopRemoveSource(rlp, source0, kCFRunLoopDefaultMode);
CFRelease(rlp);
}
void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"准备");
}
void perform(void *info){
NSLog(@"执行");
}
void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"取消");
}
source1 Demo
平时线程通信都是GCD的方法去通信
现在可以用port来通信
GCD通信
dispatch_async(dispatch_get_main_queue(), ^{
NSString *str;
NSLog(@"%@", [NSThread currentThread]);
dispatch_async(dispatch_get_global_queue(0, 0), ^{
//
NSLog(@"%@", [NSThread currentThread]);
});
});
}
运行结果
<NSThread: 0x6000002d3200>{number = 3, name = (null)}
<NSThread: 0x600000282940>{number = 1, name = main}
port通信,和上面的结果一样
- port要加入到runloop里面
- 当有信息时,会响应handlePortMessage代理
#import "ViewController.h"
#import <objc/message.h>
@interface ViewController ()<NSPortDelegate>
@property (nonatomic, strong) NSPort* subThreadPort;
@property (nonatomic, strong) NSPort* mainThreadPort;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self setupPort];
}
#pragma mark - source1: port演示
- (void)setupPort{
self.mainThreadPort = [NSPort port];
self.mainThreadPort.delegate = self;
// port - source1 -- runloop
[[NSRunLoop currentRunLoop] addPort:self.mainThreadPort forMode:NSDefaultRunLoopMode];
[self task];
}
- (void) task {
NSThread *thread = [[NSThread alloc] initWithBlock:^{
self.subThreadPort = [NSPort port];
self.subThreadPort.delegate = self;
[[NSRunLoop currentRunLoop] addPort:self.subThreadPort forMode:NSDefaultRunLoopMode];
//子线程的runloop默认不开启
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
}
- (void)handlePortMessage:(id)message {
NSLog(@"%@", [NSThread currentThread]); // 3 1
//
unsigned int count = 0;
Ivar *ivars = class_copyIvarList([message class], &count);
for (int i = 0; i<count; i++) {
NSString *name = [NSString stringWithUTF8String:ivar_getName(ivars[i])];
NSLog(@"%@",name);
}
sleep(1);
if (![[NSThread currentThread] isMainThread]) {
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"woard" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
[self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
}
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"hello" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
//i点击屏幕时发送消息,子线程向主线程发送消息
[self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}
@end
CFRunLoopTimer
基于事件的定时器
和我们平时使用的NSTimer是具备免费桥转换(toll-free bridged)的
CFRunLoopObserver
可以通过注册Observer来实现对RunLoop的相关时间点的观察
可以监听RunLoop哪些时间点呢
- kCFRunLoopEntry: RunLoop的入口时机,当RunLoop准备启动时,系统会给我们一个回调通知,就是kCFRunLoopEntry
- kCFRunLoopBeforeTimers: 通知观察者,RunLoop将要对Timer的一些相关事件进行处理
- kCFRunLoopBeforeSources: 通知将要处理一些source事件
- kCFRunLoopBeforeWaiting: 通知对应观察者当前RunLoop将要进入休眠状态,即将要发生用户态到内核态的切换
- kCFRunLoopAfterWaiting: 从内核态切换到用户态之后,会发出通知
- kCFRunLoopExit: RunLoop退出的通知
网友评论