-
RunLoop
- 一个运行循环
- 保持程序的持续运行
- 监听处理 APP 各种事件(触摸,定时器,selector)
- 节省 CPU 资源,提高程序性能(有事做的时候做事,没事做就休息)
-
main函数中的RunLoop
int main(int argc, char * argv[]) {
@autoreleasepool {
int a = UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
NSLog(@"哈哈哈哈");
return a;
}
}
如果我么在 main.m 文件中添加一句输出
运行以后"哈哈哈哈"是不会打印的
因为系统默认在 UIApplicationMain 函数中为主线程开启了 RunLoop
RunLoop 一直在运行处理各种事件或者等待事件的到来
UIApplicationMain 就不会返回
-
RunLoop结构:
iOS中有2套API来访问和使用 RunLoop
分别是:
Foundation 框架中的 NSRunLoop
Core Foundation 框架中的 CFRunLoopRef
NSRunLoop和CFRunLoopRef 都代表着 RunLoop对象
NSRunLoop 是基于 CFRunLoopRef的一层 OC 包装
所以要了解 RunLoop 内部结构
需要多研究 CFRunLoopRef 层面的 API(Core Foundation 层面)
-
RunLoop中的Mode
kCFRunLoopDefaultMode:App的默认 Mode,通常主线程是在这个 Mode 下运行
UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,当界面有scrollView滚动, 切入到该模式, 保证界面滑动时不受其他 Mode 影响,
UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
上代码 :
创建项目, 在控制器中添加定时器:
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer *time = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:time forMode:NSDefaultRunLoopMode];
}
- (void)run{
NSLog(@"run");
}
运行程序后
run是正常打印的
可是当我们在控制器的 view 添加一个TextView再运行
run 还是正常打印
一旦开始滚动TextView
run 就停止打印了
因为这时候定时器被暂停了
为什么呢?
我们来打印一下 TextView 在滚动和不滚动两种状态下的RunLoopMode:
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"一开始我的模式是: %@",[[NSRunLoop currentRunLoop] currentMode]);
}
- (void)scrollViewDidScroll:(UIScrollView *)scrollView {
NSLog(@"当我开始滚动时的模式: %@",[[NSRunLoop currentRunLoop] currentMode]);
}
打印结果如下:
打印中可以看出
TextView 一开始滚动
RunLoop 的 Mode 就由 kCFRunLoopDefaultMode 切换到 UITrackingRunLoopMode
由于定时器是添加到 NSDefaultRunLoopMode
所以 TextView 滚动进入 UITrackingRunLoopMode 模式后
定时器就停止了
如果我们想在scrollView滚动的时候做一些事情
可以使用这中方法
如果我们想让定时器在两种模式下都工作
就要让定时器加入到 kCFRunLoopCommonModes 模式下
kCFRunLoopCommonModes 模式不是一个具体的模式
他只是一个标记
加入该模式后, 会运行在所有标记为 kCFRunLoopCommonModes 模式的模式下
打印一下 RunLoo 可以看出
标记为 kCFRunLoopCommonModes 的模式分别是
kCFRunLoopDefaultMode 和 UITrackingRunLoopMode
就是默认模式和滚动模式
-
RunLoop中的Source
CFRunLoopSourceRef是事件源(输入源)
根据苹果文档的分类 :
Port-Based Sources : 来自内核或者其他线程的一些事件
Custom Input Sources : 自定义输入源
Cocoa Perform Selector Sources : 处理performSelector方法中的事件(触摸 , 点击等等)
根据函数调用栈的分类 :
Source0:非基于Port的
Source1:基于Port的, 接收内核或者其他线程发过来的事件, 分发给Source0处理
-
RunLoop中的Observer
CFRunLoopObserverRef 是观察者
能够监听 RunLoop 的状态改变
可以监听的时间点有一下几点:
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
// 即将进入 RunLoop
kCFRunLoopEntry = (1UL << 0),
//即将处理 Timer
kCFRunLoopBeforeTimers = (1UL << 1),
//即将处理Source
kCFRunLoopBeforeSources = (1UL << 2),
//即将进入休眠
kCFRunLoopBeforeWaiting = (1UL << 5),
//即将被唤醒
kCFRunLoopAfterWaiting = (1UL << 6),
//即将退出
kCFRunLoopExit = (1UL << 7),
// 所有都监听
kCFRunLoopAllActivities = 0x0FFFFFFFU
};
上代码 :
// 创建observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
});
// 添加观察者:监听RunLoop的状态
CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
最后需要自己释放:
CFRelease(observer);
-
RunLoop运转流程 :
首先,进入 RunLoop (顺便通知Observer我要进入了)
然后就去处理 Timer (顺便通知 Observer 我要处理 Timer)
然后处理 Source (顺便通知 Observer 我要处理 Source)
然后判断 Source1 还有没有没有分发的任务?
--- 有的话就去处理 Source1 收到的任务包括 Timer 和Source
--- 没有的话, 就要去睡觉了
任务都处理完了,进入休眠(顺便通知 Observer 我要休眠)
休眠的时候如果有新的任务进入, RunLoop被唤醒....
-
RunLoop实践 :
-
1:tableView滚动的时候不进行耗时事件 :
例如进行大图显示的时候,可能系统会渲染事件过长, 如果这时候用户正在拖动tableView,会造成卡顿
可以利用RunLoop来处理
只在 NSDefaultRunLoopMode 模式下显示图片
tableView滚动的时候是 UITrackingRunLoopMode 模式
不会产生冲突
[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];
-
2:常驻线程 :
子线程默认情况下, 在任务执行结束后, 线程就会死掉
如果想开启新的任务, 就要重新创建线程
如果想经常在子线程处理一些耗时操作
频繁的创建线程是不可取的
那么就需要一条不会死掉的常驻线程
上代码 :
- (void)viewDidLoad {
[super viewDidLoad];
//保住线程的命
self.thread = [[XMGThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[self.thread start];
}
//在子线程让Runloop跑起来 保住线程的命
- (void)run
{
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
NSLog(@"------------");
}
在子线程中的 RunLoop 跑起来以后
RunLoop 会一直监听子线程的事件
"------------" 这句是不会打印的
因为 RunLoop 中的do-while循环一直在运行
需要注意的是 : 线程一定要加一些东西, 例如Port
Port 就是 RunLoop 中的 Source
有了 Source 以后
RunLoop就会一直等待 Source 给他事件
如果不添加的话
RunLoop 中没有 Source 也没有 Timer
RunLoop就会自动退出
-
3:子线程添加定时器 :
上代码 :
- (void)viewDidLoad {
[super viewDidLoad];
//子线程添加定时器
self.thread = [[XMGThread alloc] initWithTarget:self selector:@selector(execute2) object:nil];
[self.thread start];
}
- (void)execute2
{
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
}
和第二种实践方法一样
在子线程中启动 RunLoop
因为 RunLoop 中已经有了 Timer
所有 RunLoop 不会死掉
这样线程不会死, 定时器也正常运行
-
4:自动释放池 :
作用 :
将一些对象扔到池子中去
当池子释放的时候
让池中所有对象调用一次 release 方法
自动释放池什么时候死 :
在 RunLoop 睡觉之前死
因为睡觉可能睡很久
如果不让自动释放池死掉
会占用很多内存
当 RunLoop 被再次唤醒的时候
刚刚所有调用 release 的对象又会放到释放池中
在子线程开 RunLoop 的时候
可以添加释放池 :
//在子线程添加定时器
- (void)execute
{
@autoreleasepool {
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
}
还有一个问题需要记录一下
就是当旧的释放池在销毁以后,新的释放池什么时候创建呢?
我们可以打印一下主线程 RunLoop 看释放池都监听了 RunLoop 那些状态
- (void)viewDidLoad {
[super viewDidLoad];
NSLog(@"%@",[NSRunLoop currentRunLoop]);
}
找出相关 autoreleasepool 的部分 :
observers = (
"{valid = Yes, activities = 0x1, repeats = Yes, order = -2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e3e6ede), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f8a7c802048>\n)}}",
"{valid = Yes, activities = 0xa0, repeats = Yes, order = 2147483647, callout = _wrapRunLoopWithAutoreleasePoolHandler (0x10e3e6ede), context = {type = mutable-small, count = 1, values = (\n\t0 : <0x7f8a7c802048>\n)}}"
),
autoreleasepool 利用 observers 监听了以下状态:
activities = 0x1 (相当于十进制的 1 )
activities = 0xa0 (相当于十进制的 160)
这是 C 语言的位移枚举 (位移枚举的解释)
这两个 activities 对应 RunLoop 以下三个状态:
kCFRunLoopEntry = (1UL << 0), //即将进入
kCFRunLoopBeforeWaiting = (1UL << 5), //即将休眠
kCFRunLoopExit = (1UL << 7), //即将推出
可以看出 autoreeaseepool 利用 observers 监听了RunLoop三个状态
所以工作流程应该是这样 :
刚进入 RunLoop 的时候创建释放池
监听到即将进入休眠销毁释放池释放变量,并创建新的释放池以供下次被唤醒时使用
因为 autoreleasepool 没有监听 RunLoop 即将唤醒的状态
所以在休眠之前创建好
但是如果 RunLoop 不被唤醒了 , 最后一次创建的释放池就不会被销毁
所以监听 kCFRunLoopExit 状态
在最后退出的时候销毁最后一次创建的释放吃
感谢阅读
你的支持是我写作的唯一动力