RunLoop应用篇
基础理论请移步这两篇:
RunLoop介绍篇
RunLoop内部调用过程
一. runloop下timer,observer,source演练
我们在 RunLoop介绍篇 中介绍了Core Foundation框架下关于RunLoop的5个类:
- CFRunLoopRef:代表RunLoop的对象
- CFRunLoopModeRef:RunLoop的运行模式
- CFRunLoopSourceRef:就是RunLoop模型图中提到的输入源/事件源
- CFRunLoopTimerRef:就是RunLoop模型图中提到的定时源
- CFRunLoopObserverRef:观察者,能够监听RunLoop的状态改变
下面5个类的关系图。
接着来讲解这5个类的相互关系。
一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。
- 每次RunLoop启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作CurrentMode。
- 如果需要切换运行模式(CFRunLoopModeRef),只能退出Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。
- 这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响。
用一张图来总结它们就是这种关系:
1.1 timer
- (void)cfTimerDemo {
// 定义runloop timer上下文
CFRunLoopTimerContext context = {
0,
((__bridge void *)self),
NULL,
NULL,
NULL
};
// 获取当前的runloop
CFRunLoopRef rlp = CFRunLoopGetCurrent();
/**
参数一:用于分配对象的内存
参数二:在什么是触发 (距离现在)
参数三:每隔多少时间触发一次
参数四:未来参数
参数五:CFRunLoopObserver的优先级 当在Runloop同一运行阶段中有多个CFRunLoopObserver 正常情况下使用0
参数六:回调,比如触发事件,我就会来到这里
参数七:上下文记录信息
*/
// 创建runloop timer
CFRunLoopTimerRef timerRef = CFRunLoopTimerCreate(kCFAllocatorDefault, 0, 1, 0, 0, sp_RunLoopTimerCallBack, &context);
// 添加到当前的runloop
CFRunLoopAddTimer(rlp, timerRef, kCFRunLoopDefaultMode);
}
void sp_RunLoopTimerCallBack(CFRunLoopTimerRef timer, void *info){
NSLog(@"%@---%@",timer,info);
}
运行结果每秒打印一次。
1.2 observer
- (void)cfObserverDemo {
CFRunLoopObserverContext context = {
0,
((__bridge void *)self),
NULL,
NULL,
NULL
};
CFRunLoopRef rlp = CFRunLoopGetCurrent();
/**
参数一:用于分配对象的内存
参数二:你关注的事件
kCFRunLoopEntry=(1<<0),
kCFRunLoopBeforeTimers=(1<<1),
kCFRunLoopBeforeSources=(1<<2),
kCFRunLoopBeforeWaiting=(1<<5),
kCFRunLoopAfterWaiting=(1<<6),
kCFRunLoopExit=(1<<7),
kCFRunLoopAllActivities=0x0FFFFFFFU
参数三:CFRunLoopObserver是否循环调用
参数四:CFRunLoopObserver的优先级 当在Runloop同一运行阶段中有多个CFRunLoopObserver 正常情况下使用0
参数五:回调,比如触发事件,我就会来到这里
参数六:上下文记录信息
*/
CFRunLoopObserverRef observerRef = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, sp_RunLoopObserverCallBack, &context);
CFRunLoopAddObserver(rlp, observerRef, kCFRunLoopDefaultMode);
}
void sp_RunLoopObserverCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
NSLog(@"%lu-%@",activity,info);
}
我发送一个通知来测试以下observer,发现observer观察到runloop的状态变化,打印结果如下:
RunLoopTest[6149:679231] 64-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] gotNotification = NSConcreteNotification 0x6000024acc30 {name = helloMyNotification; object = cooci}
RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 32-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 64-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 2-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 4-<ModeSourceTimerViewController: 0x7f8807c036d0>
RunLoopTest[6149:679231] 32-<ModeSourceTimerViewController: 0x7f8807c036d0>
1.3 source
source0:事件源非基于Port
source1:基于Port,通过内核和其他线程通信,接收、分发系统事件
1.3.1 source0
- (void)source0Demo {
//初始runloopSource上下文(点进去看知道是结构体对象)
CFRunLoopSourceContext context = {
0,
NULL,
NULL,
NULL,
NULL,
NULL,
NULL,
schedule,
cancel,
perform,
};
/**
参数一:传递NULL或kCFAllocatorDefault以使用当前默认分配器。
参数二:优先级索引,指示处理运行循环源的顺序。这里我传0为了的就是自主回调
参数三:为运行循环源保存上下文信息的结构
*/
CFRunLoopSourceRef source0 = CFRunLoopSourceCreate(CFAllocatorGetDefault(), 0, &context);
CFRunLoopRef rlp = CFRunLoopGetCurrent();
CFRunLoopAddSource(rlp, source0, kCFRunLoopDefaultMode);
// 发送一个执行信号
CFRunLoopSourceSignal(source0);
// 唤醒 runloop 防止沉睡状态
CFRunLoopWakeUp(rlp);
// 取消,移除
// CFRunLoopRemoveSource(rlp, source0, kCFRunLoopDefaultMode);
// CFRelease(rlp);
}
void schedule(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"准备代发");
}
void perform(void *info){
NSLog(@"代发ing...");
}
void cancel(void *info, CFRunLoopRef rl, CFRunLoopMode mode){
NSLog(@"取消了,终止了!!!!");
}
1.3.2 source1
source1 port线程之间的通讯演示
@property (nonatomic, strong) NSPort* subThreadPort;
@property (nonatomic, strong) NSPort* mainThreadPort;
- (void)source1Demo {
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"hello" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
// 子线程向主线程发送数据
[self.subThreadPort sendBeforeDate:[NSDate date] components:components from:self.mainThreadPort reserved:0];
}
#pragma mark - NSPortDelegate
- (void)handlePortMessage:(id)message {
NSLog(@"%@", [NSThread currentThread]); // 子线程 - 主线程
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); // -- components
}
sleep(1);
if (![[NSThread currentThread] isMainThread]) {
NSMutableArray* components = [NSMutableArray array];
NSData* data = [@"world" dataUsingEncoding:NSUTF8StringEncoding];
[components addObject:data];
// 主线程向子线程发送数据
[self.mainThreadPort sendBeforeDate:[NSDate date] components:components from:self.subThreadPort reserved:0];
}
}
/*
配置
*/
- (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];
[[NSRunLoop currentRunLoop] run];
}];
[thread start];
}
二、runloop在开发中的应用
上面rooploop的timer,observer,source演练完毕之后,下面讲一下RunLoop的几种应用。
1. NSTimer的使用
NSTimer的使用方法在 runloop介绍篇 讲解CFRunLoopTimerRef类的时候详细讲解过,具体参考文章的 2.3 CFRunLoopTimerRef。
很简单,发现NSTimer不准的问题就是在runloopModel切换时产生的问题,两种解决办法:
(1)timer的runloopModel改为NSRunLoopCommonModes
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
(2)在子线程运行timer(即子线程处理耗时操作且常驻线程)
- (void)timerAddSubThreadTest {
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(displayCount) object:nil];
[self.thread start];
}
- (void)displayCount {
[NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(log) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] run];
}
- (void)log {
NSLog(@"hello world");
}
或者用block方法这样写,但都别忘了子线程的runloop需要手动开启:
[NSThread detachNewThreadWithBlock:^{
NSLog(@"%@", [NSThread currentThread]);
[NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
NSLog(@"hello world");
}];
[[NSRunLoop currentRunLoop] run];
}];
2. 后台常驻线程(很常用)
我们在开发应用程序的时候,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好让这条线程永远常驻内存。
那么怎么做呢?
添加一条用于常驻内存强引用的子线程,在该线程的RunLoop下添加一个Sources,开启RunLoop。
具体实现过程如下:
- 创建子线程并分配任务
@property (nonatomic, strong) NSThread *thread;
- (void)viewDidLoad {
[super viewDidLoad];
// 创建线程,并调用run1方法执行任务
self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
// 开启线程
[self.thread start];
}
- (void)run1
{
// 这里写任务
NSLog(@"----run1-----");
// 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
NSLog(@"未开启RunLoop");
}
- 运行之后发现打印了 ----run1----,而 未开启RunLoop 则未打印。
这时,我们就开启了一条常驻线程,下面我们来试着添加其他任务,除了之前创建的时候调用了run1方法,我们另外在点击的时候调用run2方法
那么,我们在touchesBegan中调用PerformSelector,从而实现在点击屏幕的时候调用run2方法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
// 利用performSelector,在self.thread的线程中调用run2方法执行任务
[self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}
- (void)run2
{
NSLog(@"----run2------");
}
经过运行测试,除了之前打印的----run1----,每当我们点击屏幕,都能调用----run2----。
这样我们就实现了常驻线程的需求。
3. 加载大量图片的性能优化
有时候,我们会遇到这种情况:
当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这时候当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能会出现卡顿的现象,我们需要保持流畅度和加载速度。
怎么解决这个问题呢?做一个简单的分析:
1,因为这里用到了Runloop循环,那么我们可以监听到runloop的每次循环,在每一次循环当中我们考虑去进行一次图片下载和布局。
2,既然要在每次循环执行一次任务,我们可以先把所有图片加载的任务代码块添加到一个数组当中,每次循环取出第一个任务进行执行。
3,因为runloop在闲置的时候会自动休眠,所以我们要想办法让runloop始终处于循环中的状态。
好了,下面开始考虑代码实现
第一步,UITableView的创建和基本效果
创建UITableView 并实现必要的代理,代码略
第二步,初始化可变数组用来存储任务
typedef void(^SaveFuncBlock)(void);
// 存放任务的数组
@property (nonatomic, strong) NSMutableArray *saveTaskMarr;
// 最大任务数(超过最大任务数的任务就停止执行)
@property (nonatomic, assign) NSInteger maxTaskNumber;
// 任务执行的代码块
@property (nonatomic, copy) SaveFuncBlock saveFuncBlock;
- (NSMutableArray *)saveTaskMarr {
if (!_saveTaskMarr) {
_saveTaskMarr = [NSMutableArray array];
}
return _saveTaskMarr;
}
第三步,将任务添加到数组保存
// 添加任务进数组保存
- (void)addTasks:(SaveFuncBlock)taskBlock {
[self.saveTaskMarr addObject:taskBlock];
// 超过每次最多执行的任务数就移除当前数组
if (self.saveTaskMarr.count > self.maxTaskNumber) {
[self.saveTaskMarr removeObjectAtIndex:0];
}
}
第四步,在cellForRow方法中,添加方法
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellID forIndexPath:indexPath];
// 添加任务到数组
[self addTasks:^{
// 下载图片的任务
[cell.icon1 setImage:[UIImage imageNamed:@"1.jpg"]];
[cell.icon2 setImage:[UIImage imageNamed:@"2.jpeg"]];
[cell.icon3 setImage:[UIImage imageNamed:@"3.jpg"]];
}];
return cell;
}
第五步,监听runloop
//添加一个监听者RunloopObserver
-(void)addRunloopObserver{
//获取当前的RunLoop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//定义一个centext
CFRunLoopObserverContext context = {
0,
( __bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
//定义一个观察者
static CFRunLoopObserverRef defaultModeObsever;
//创建观察者
defaultModeObsever = CFRunLoopObserverCreate(NULL,
kCFRunLoopBeforeWaiting,
YES,
0,
&Callback,
&context
);
//添加当前RunLoop的观察者
CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
//c语言有creat 就需要release
CFRelease(defaultModeObsever);
}
第六步,使用定时器,保持runloop处于循环中
@property (nonatomic, weak) NSTimer *timer;
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 repeats:self block:^(NSTimer * _Nonnull timer) {
// 此方法主要是利用计时器事件保持runloop处于循环中,不用做任何处理
}];
第七步,在runloop循环中去处理事件
//定义一个回调函数 一次RunLoop来一次
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
DelayLoadImageViewController * vcSelf = (__bridge DelayLoadImageViewController *)(info);
if (vcSelf.saveTaskMarr.count > 0) {
//获取一次数组里面的任务并执行
SaveFuncBlock funcBlock = vcSelf.saveTaskMarr.firstObject;
funcBlock();
[vcSelf.saveTaskMarr removeObjectAtIndex:0];
}
}
写在最后
本文中所有的示例都可以在这里下载,如果您喜欢,可以动动手指给个☆哦
https://github.com/SPIREJ/RunLoopTest