Runloop总结和应用(附Demo)
2018-09-26 本文已影响35人
木格措的天空
关于Runloop的原理或者源码分析,网上有很多文章。本文意在总结一下自己能想到的一些Runloop的知识点,并举一些自己遇到的相关例子。如有错误地方,请大家指教。我总结的Runloop知识点可能还有很多遗漏的地方,欢迎大家补充,大家一起学习和进步。。。。
一.自我总结一下Runloop
Runloop说到底就是一个死循环。Runloop被唤醒线程处理事件,事件处理完毕以后,回到睡眠状态,等待下次唤醒。
1.Runloop的作用或者目的:
1)保证Runloop所在的线程不退出。
2)负责监听事件(UI事件、时钟、网络等)。
Runloop的数据结构:
屏幕快照 2018-09-26 下午4.43.58.png2.Runloop的Mode,主要是用来指定事件在运行循环中的优先级。Runloop有五种Mode,但我们经常接触到的就只有以下的前面三种:
1)kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
2)UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode 影响
3)kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode
4)UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
5)GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
kCFRunLoopCommonModes:runloop在同一时间,只能处理一种mode下的事件。我们在主线程使用timer的时候,滑动tableview时timer会停止,这是因为timer处于default模式 Runloop优先去执行UI事件去了,即UI模式的优先级比Default模式的优先级高。此时如果把timer的mode改为kCFRunLoopCommonModes,timer就不会受滑动的影响,等效于timer被同时加进了UI模式和Default模式。kCFRunLoopCommonModes是个占位模式,等同于(UI模式&&Default模式)。
3.tableView怎样保证子线程数据请求回来后更新UI的时候,不打断用户的滑动操作。
1.tableView在滑动时Runloop是处于UITrackingRunloopMode模式下的。
2.子线程请求完数据,在回到主线程处理的时候,我们将更新的逻辑加载default模式下。那么default模式下的操作是不会执行的。
3.滑动结束了,Runloop由UITrackingRunloopMode又回到default模式,那么default模式下的更新操作就能执行了 。
4.Runloop的Source,即Runloop的事件输入源:
1)主要有Source事件源和Timer事件源(定时源)。Source分为Source0和Source1。
2)Source1用于处理系统内核事件。例1:硬件事件(触摸/锁屏/摇晃等),先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收,随后用 mach port 转发给需要的App进程,注册的 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。例2:底层CFSocket的socket事件也是通过Source1分发到应用层的。
3)Source0:即非Source1
5.Runloop的启动和退出
Runloop有以下三种启动方式
- (void)run;
- (void)runUntilDate:(NSDate *)limitDate;
- (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
Runloop的退出:
1)用run启动时,当没有输入源或者timer附加于Runloop上时,runloop就会立刻退出。虽然这样可以将runloop退出,但是苹果并不建议我们这么做,因为系统内部有可能会在当前线程的runloop中添加一些输入源,所以通过手动移除input source或者timer这种方式,并不能保证runloop一定会退出。所以如果想退出runloop,不应该使用第一种启动方式来启动runloop。
2)启动方式用runUntilDate,可以通过设置超时时间来退出Runloop。
3)通过runMode:beforeDate:方式启动,Runloop会运行一次,当超时时间到达或者第一个输入源被处理,Runloop就会退出。
6.关于Runloop和GCD
实际上 RunLoop 底层也会用到 GCD 的东西。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。
当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的
RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__() 里执行这个 block。
但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。
7.关于Runloop和线程
Runloop与线程是一一对应的,Runloop是来管理线程的,当线程的Runloop被开启后,线程会在执行完任务后进入休眠状态,有了任务就会被唤醒去执行任务。Runloop在第一次获取时被创建,在线程结束时被销毁。
对于主线程来说,Runloop在程序一启动就默认创建好了。
对于子线程来说,Runloop是懒加载的,只有当我们使用的时候才会创建,所以在子线程用定时器要注意:确保子线程的Runloop被创建,不然定时器不会回调。
8.关于Runloop和Autorelease
1)App启动后,苹果在主线程 RunLoop 里注册了两个 Observer
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其优先级最高,保证创建释放池发生在其他所有回调之前。
2)第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的优先级最低,保证其释放池子发生在其他所有回调之后。
3)在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
4)GCD开辟子线程不需要手动创建autoreleasepool,因为GCD的每个队列都会自行创建autoreleasepool
5)thread是不会自动创建autoreleasepool的,所以我们在子线程中会有手动写autorelease pool代码
6)我们来看一个例子,如下
- (void)mytTest{
for (int i = 0; i < 2000000; i++) {
[NSString stringWithFormat:@"你们好 -%04d", i];
}
}
首先我们要知道stringWithFormat是个类方法,它的内存管理方式是autorelease。autoreleasepool会等到runloop的当前循环结束后才会对释放池中的每个对象发送release消息,而runloop的当前循环结束的前提是要等for循环执行完,所以for循环内创建的对象就会在for循环执行完之前一直存在在内存中,导致暴增。经过以下的修改后就不会出现这个问题,保证每次for循环都对对象release一次。
- (void)mytTest{
for (int i = 0; i < 2000000; i++) {
@autoreleasepool {
[NSString stringWithFormat:@"你们好 -%04d", i];
}
}
}
二.Runloop的应用,本文列举的demo只是自己遇到的一些情况,除了这些Runloop还有很多其他的应用,比如AFnetworking、NSURLConnection等。
例子1.子线程中使用Timer。
在子线程用定时器要注意:确保子线程的Runloop被创建,不然定时器不会回调
- (void)viewDidLoad {
[super viewDidLoad];
NSThread*thred = [[NSThread alloc]initWithTarget:self selector:@selector(myMethod) object:nil];
[thred start];
}
-(void)myMethod{
if (![NSThread isMainThread]) {
// 第1种方式
//此种方式创建的timer已经添加至runloop中
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(timerAction) userInfo:nil repeats:YES];
//保持线程为活动状态,才能保证定时器执行
[[NSRunLoop currentRunLoop] run];//已经将nstimer添加到NSRunloop中了
//第2种方式
//此种方式创建的timer没有添加至runloop中
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self
selector:@selector(timerAction) userInfo:nil repeats:YES];
//将定时器添加到runloop中
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
//子线程的runloop需要手动开启
[[NSRunLoop currentRunLoop] run];
NSLog(@"dada------");
}
}
- (void)timerAction{
NSLog(@"00000");
}
总结:上面的写法有个缺点:不能方便得去停止runloop,退出timer。后面例子3会做出优化。
例子2.解决视图滑动时主线程timer无效的问题,当然你可以把timer放在子线程中也可以解决这个问题。我们这里就把timer放在CommonModes模式下来解决问题。
- (void)viewDidLoad {
[super viewDidLoad];
NSTimer*timer = [NSTimer timerWithTimeInterval:1.0 target:self
selector:@selector(timerAction) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop]addTimer:timer forMode:NSRunLoopCommonModes];
}
但是这样做有个小问题,如果timerAction比较耗时的话,会影响视图滑动的流畅性。所以还是建议在子线程下使用timer。
例子3.对例子1的优化,用runUntilDate启动runloop,通过isFinish标识来停止runloop。当用户在点击屏幕时isFinish变为YES,就可以停止Runloop。
#import "ViewController.h"
@interface ViewController ()
@property(nonatomic,assign)BOOL isFinished;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
self.isFinished = NO;
NSThread*thred = [[NSThread alloc]initWithTarget:self selector:@selector(myMethod) object:nil];
[thred start];
}
-(void)myMethod{
if (![NSThread isMainThread]) {
//此种方式创建的timer没有添加至runloop中
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0f target:self
selector:@selector(timerAction) userInfo:nil repeats:YES];
//将定时器添加到runloop中
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
while (!_isFinished) {
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.0001]];
}
NSLog(@"dada------");
}
}
- (void)timerAction{
NSLog(@"定时器");
}
-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
self.isFinished = YES;
}
例子4.使用Runloop的Observer来优化tableView列表加载大量的大尺寸图片,使其更流畅。
为了方便阅读,我把所有代码尽量写在ViewController一个类中
1)首先我们来看看优化之前的代码,Demo链接。代码如下:
#import "ViewController.h"
#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width)
#define SCREEN_HEIGHT ([[UIScreen mainScreen] bounds].size.height)
static NSString*TIDENTIFY = @"TIDENTIFY";
static CGFloat PICRATIO = 1.5;//图片的比例(宽:高)
static CGFloat PERCELLPICNUMBER = 4;//每排有多少张图片
static CGFloat PICGAP = 5.0;//图片之间的间隙
static CGFloat TITLELABELHEIGHT = 20.0;//标题label的高度
@interface ViewController ()<UITableViewDelegate,UITableViewDataSource>
{
CGFloat perPicWidth;//每张图片宽度
CGFloat perPicHeight;//每张图片高度
CGFloat cellHeight;//cell的高度
}
@property(nonatomic,strong)UITableView*myTableView;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//计算图片的宽高和cell高度
perPicWidth = (SCREEN_WIDTH - PICGAP*(PERCELLPICNUMBER+1))/PERCELLPICNUMBER;
perPicHeight = perPicWidth/PICRATIO;
cellHeight = perPicHeight + TITLELABELHEIGHT + 5.0;
//初始化tableview
[self.view addSubview:self.myTableView];
}
-(UITableView *)myTableView{
if (!_myTableView) {
self.myTableView = [[UITableView alloc]initWithFrame:CGRectMake(0, 44.0, SCREEN_WIDTH, SCREEN_HEIGHT-44.0) style:(UITableViewStylePlain)];
[self.myTableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TIDENTIFY];
self.myTableView.delegate = self;
self.myTableView.dataSource = self;
self.myTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
return _myTableView;
}
#pragma -- UITableViewDelegate
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 120;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell*cell = [tableView dequeueReusableCellWithIdentifier:TIDENTIFY];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
//干掉cell上的子控件,节约内存
for(UIView*v in cell.contentView.subviews){
[v removeFromSuperview];
}
//添加标题
[self addCellTitleLabel:cell andIndex:indexPath.row];
//添加图片
[self addCellImgs:cell];
return cell;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return cellHeight;
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}
/*
加载cell的控件
*/
//加载标题
-(void)addCellTitleLabel:(UITableViewCell*)cell andIndex:(NSUInteger)index{
UILabel*label = [[UILabel alloc]initWithFrame:CGRectMake(PICGAP, 0, SCREEN_WIDTH, TITLELABELHEIGHT)];
label.tag = 0;
label.textAlignment = NSTextAlignmentLeft;
label.font = [UIFont systemFontOfSize:17.0];
label.textColor = [UIColor greenColor];
label.text = [NSString stringWithFormat:@"高清黄山风景图片--%d",(int)(index+1)];
[cell.contentView addSubview:label];
}
//加载cell的图片
-(void)addCellImgs:(UITableViewCell*)cell{
NSString*imgPath = [[NSBundle mainBundle]pathForResource:@"MYPIC" ofType:@"jpeg"];
UIImage*img = [UIImage imageWithContentsOfFile:imgPath];
for (int num = 0; num < PERCELLPICNUMBER; num++) {
CGRect imgVFrame =CGRectMake((num+1)*PICGAP + num*perPicWidth, TITLELABELHEIGHT, perPicWidth, perPicHeight);
UIImageView*imgView = [[UIImageView alloc]initWithFrame:imgVFrame];
imgView.tag = num+1;
imgView.image = img;
[cell.contentView addSubview:imgView];
}
}
我们运行项目滑动列表时会发现有一点不流畅。这是因为cellForRowAtIndexPath函数块里面的这句代码[self addCellImgs:cell];导致的。我以iphone6为例,一排4张,屏幕最多显示8排,共32张。也就是说主线程的runloop一次循环除了要处理滑动ui事件之外,还要最多加载32张图片。这就是不流畅的原因。
现在我们利用Runloop的observer来监察runloop的kCFRunLoopBeforeWaiting(进入等待之前即每次循环结束的时候)。思路如下:
1)监听runloop循环,runloop循环一次就加载一张图片
2)用timer让runloop不进入睡眠,解决不滑动时图片不加载的问题。因为主线程的runloop不处理事件时就会进入睡眠。即让Runloop跑起来。
3)创建一个数组,用于装任务(block代码),监听到runloop循环一次就取一个任务执行
这样每次循环我们就只加载一个cell的4张图片,减少了一次循环的负担,使其不影响滑动等UI事件的执行。Demo链接。优化后的代码如下:
#import "ViewController.h"
#define SCREEN_WIDTH ([[UIScreen mainScreen] bounds].size.width)
#define SCREEN_HEIGHT ([[UIScreen mainScreen] bounds].size.height)
static NSString*TIDENTIFY = @"TIDENTIFY";
static CGFloat PICRATIO = 1.5;//图片的比例(宽:高)
static CGFloat PERCELLPICNUMBER = 4;//每排有多少张图片
static CGFloat PICGAP = 5.0;//图片之间的间隙
static CGFloat TITLELABELHEIGHT = 20.0;//标题label的高度
typedef void(^RunloopBlock)(void);
@interface ViewController ()<UITableViewDelegate,UITableViewDataSource>
{
CGFloat perPicWidth;//每张图片宽度
CGFloat perPicHeight;//每张图片高度
CGFloat cellHeight;//cell的高度
}
@property(nonatomic,strong)UITableView*myTableView;
@property(nonatomic,strong)NSMutableArray*tasksArr;//创建一个数组,用于装任务(block代码)
@property(nonatomic,assign)int maxTaksLength;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//计算图片的宽高和cell高度
perPicWidth = (SCREEN_WIDTH - PICGAP*(PERCELLPICNUMBER+1))/PERCELLPICNUMBER;
perPicHeight = perPicWidth/PICRATIO;
cellHeight = perPicHeight + TITLELABELHEIGHT + 5.0;
//初始化tableview
[self.view addSubview:self.myTableView];
_maxTaksLength = 32;//以iphone6为例,一排4张,屏幕最多显示8排,共32张。
_tasksArr = [NSMutableArray new];
//用timer让runloop不进入睡眠,解决不滑动时(主线程runloop不处理事件就会进入睡眠)图片不加载的问题。
[NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(unuselessMethod) userInfo:nil repeats:YES];
//添加观察者
[self addRunloopObserver];
}
-(void)unuselessMethod{
//空事件什么都不做,目的是为了配合timer让runloop不进入睡眠
}
-(UITableView *)myTableView{
if (!_myTableView) {
self.myTableView = [[UITableView alloc]initWithFrame:CGRectMake(0, 44.0, SCREEN_WIDTH, SCREEN_HEIGHT-44.0) style:(UITableViewStylePlain)];
[self.myTableView registerClass:[UITableViewCell class] forCellReuseIdentifier:TIDENTIFY];
self.myTableView.delegate = self;
self.myTableView.dataSource = self;
self.myTableView.separatorStyle = UITableViewCellSeparatorStyleNone;
}
return _myTableView;
}
#pragma -- UITableViewDelegate
-(NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section{
return 120;
}
-(UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath{
UITableViewCell*cell = [tableView dequeueReusableCellWithIdentifier:TIDENTIFY];
cell.selectionStyle = UITableViewCellSelectionStyleNone;
//干掉cell上的子控件,节约内存
for(UIView*v in cell.contentView.subviews){
[v removeFromSuperview];
}
//添加标题
[self addCellTitleLabel:cell andIndex:indexPath.row];
//添加图片
__weak typeof(self) weakSelf = self;
[self addTask:^{
[weakSelf addCellImgs:cell];
}];
return cell;
}
-(CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath{
return cellHeight;
}
-(NSInteger)numberOfSectionsInTableView:(UITableView *)tableView{
return 1;
}
/*
加载cell的控件
*/
//加载标题
-(void)addCellTitleLabel:(UITableViewCell*)cell andIndex:(NSUInteger)index{
UILabel*label = [[UILabel alloc]initWithFrame:CGRectMake(PICGAP, 0, SCREEN_WIDTH, TITLELABELHEIGHT)];
label.tag = 0;
label.textAlignment = NSTextAlignmentLeft;
label.font = [UIFont systemFontOfSize:17.0];
label.textColor = [UIColor greenColor];
label.text = [NSString stringWithFormat:@"高清黄山风景图片--%d",(int)(index+1)];
[cell.contentView addSubview:label];
}
//加载cell的图片
-(void)addCellImgs:(UITableViewCell*)cell{
NSString*imgPath = [[NSBundle mainBundle]pathForResource:@"MYPIC" ofType:@"jpeg"];
UIImage*img = [UIImage imageWithContentsOfFile:imgPath];
for (int num = 0; num < PERCELLPICNUMBER; num++) {
CGRect imgVFrame =CGRectMake((num+1)*PICGAP + num*perPicWidth, TITLELABELHEIGHT, perPicWidth, perPicHeight);
UIImageView*imgView = [[UIImageView alloc]initWithFrame:imgVFrame];
imgView.tag = num+1;
imgView.image = img;
[cell.contentView addSubview:imgView];
}
}
#pragma mark -- Runloop
//添加任务
-(void)addTask:(RunloopBlock)block{
[self.tasksArr addObject:block];
//保证数组只放32个任务
if (self.tasksArr.count > _maxTaksLength) {
[self.tasksArr removeObjectAtIndex:0];
}
}
//添加观察者
-(void)addRunloopObserver{
//获取runloop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//定义观察者
static CFRunLoopObserverRef defaultModeObserver;
//创建上下文,由于CallBack是C函数不允许使用OC对象,所以要依靠上下文的传递来解决这个问题
CFRunLoopObserverContext context = {
0,
(__bridge void *)(self),//OC对象转C
&CFRetain,
&CFRelease,
NULL,
};
//创建
defaultModeObserver = CFRunLoopObserverCreate(NULL, kCFRunLoopBeforeWaiting, YES, 0, &CallBack, &context);
//添加到当前runloop中
CFRunLoopAddObserver(runloop, defaultModeObserver, kCFRunLoopCommonModes);
}
/*
CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info
这三个参数是观察的时候上下文传递过来的
*/
static void CallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
//取出任务执行,一次runloop循环执行一个任务
ViewController*self = (__bridge ViewController*)info;
if (self.tasksArr.count == 0) {
return;
}
RunloopBlock task = self.tasksArr.firstObject;
task();
//执行完毕移除任务
[self.tasksArr removeObjectAtIndex:0];
}