java runloop_Runloop总结和应用(附Demo)

关于Runloop的原理或者源码分析,网上有很多文章。本文意在总结一下自己能想到的一些Runloop的知识点,并举一些自己遇到的相关例子。如有错误地方,请大家指教。我总结的Runloop知识点可能还有很多遗漏的地方,欢迎大家补充,大家一起学习和进步。。。。

一.自我总结一下Runloop

Runloop说到底就是一个死循环。Runloop被唤醒线程处理事件,事件处理完毕以后,回到睡眠状态,等待下次唤醒。

1.Runloop的作用或者目的:

1)保证Runloop所在的线程不退出。

2)负责监听事件(UI事件、时钟、网络等)。

Runloop的数据结构:

fa19d4b45bd9

屏幕快照 2018-09-26 下午4.43.58.png

2.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 *)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 ()

{

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 ()

{

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];

}

哈哈哈哈哈哈哈,虽然这个例子举得不是很巧当。但其确实是对Runloop Observer的一次巧妙的应用。可以为我们在解决其他类似问题的时候提供启发。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值