今天在学习线程安全的时候,看到了多线程编程指南,提到了关于runloop的部分,再学习一下runloop。
1.什么是runloop
runloop其实是线程相关的基础框架的一部分,如果没有runloop的话一个线程在执行完一个任务以后就会退出,而使用了runloop就可以让线程随时处理事物。然而为了节省资源,线程也不是一直在工作,runloop让线程在有工作的时候工作,没工作的时候休息(不占用内存资源)。
2.CFRunLoopRef和NSRunLoop
CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。
NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。
3.runloop与多线程
系统不允许我们直接创建runloop,只提供了两个自动获取runloop的函数:CFRunLoopGetMain()和CFRunLoopGetCurrent()。
每条线程都有唯一一个与之对应的Runloop对象(其关系保存在一个全局字典里)。主线程的Runloop已经自动创建好了,子线程的Runloop需要主动创建,如果在子线程里不主动创建runloop那就这个线程里就一直不会有runloop。
除了主线程以外,只能在线程的内部获取他的runloop。
Runloop在第一次获取时创建(获取runloop的时候就自动创建了),在线程结束时销毁(主线程的runloop除外)。⚠️:runloop不是直接创建的,而是在我们获取的时候自动创建的。
4.runloop的mode(模式)
每个mode里包括了若干个source、timer、observer。每个runloop里又包含了若干个mode。每次调用runloop的时候要指定一个mode,这个mode就是currentMode。只有和这个currentMode相关联的源才会被执行,然而其他的源要等到他们所在的mode被执行的时候才会被执行。
如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。
mode的名字是可以自定义的,但是mode里的内容必须有source、timer、observer否则就没有意义了。
source+timer+observer = mode item,一个mode item可以被同时加入多个mode,但是如果一个modeitem多次加入同一个mode是无效的,如果一个mode里没有modeitem,那么runloop就会退出循环。
应用:
在kCFRunLoopDefaultMode模式下添加一个NSTimer,不拖动scrollview的时候定时器正常工作,拖动scrollview了以后,相当于切换到了UITrackingRunLoopMode模式,但是定时器还在原来的模式里,定时器停止工作。等到停止拖拽以后,定时器继续工作。
NSRunLoopCommonModes可以做到让定时器在不拖拽和拖拽的时候都能工作。NSRunLoopCommonModes =kCFRunLoopDefaultMode +UITrackingRunLoopMode。
NSRunLoopCommonModes其实不是一种真正的运行模式,他其实是一种标签(每当runloop发生变化都会把mode里的mode item添加到带有common标签的mode里)。
5.runloop的内部实现
CFRunLoopSourceRef 产生事件,source0和source1
Source0 只包含了一个回调,他并不能主动出发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程。
CFRunLoopTimerRef 是基于时间的触发器。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
CFRunLoopObserverRef 观察者用来观察runloop状态的改变。
📓:CFRunLoopTimerRef和NSTimer是toll-free-bridged的。
什么是toll-free-bridged呢,也叫做免费桥,就是说把Core Foundation框架对象和Foundation框架对象进行转换,由于这种转换不需要使用额外的cpu资源所以叫免费桥。
❓:Core Foundation框架和Foundation框架有什么区别呢?
这两个框架都提供了相同功能的api,不同的是cf框架提供的是c语言的接口,Foundation框架提供的是oc接口。无论是哪个框架生成的对象都可以在两个框架里使用,就是用toll-free-bridged去转换。
三种转换方式:
1. __bridge,什么也不做,仅仅是转换。此种情况下:
i). 从Cocoa转换到Core,需要人工CFRetain,否则,Cocoa指针释放后, 传出去的指针则无效。
ii). 从Core转换到Cocoa,需要人工CFRelease,否则,Cocoa指针释放后,对象引用计数仍为1,不会被销毁。
2. __bridge_retained,转换后自动调用CFRetain,即帮助自动解决上述i的情形。相当于CFBridgingRetain方法。
3. __bridge_transfer,转换后自动调用CFRelease,即帮助自动解决上述ii的情形。相当于CFBridgingRelease方法。
6.runloop事件执行过程
每次运行 runloop,你线程的 run loop 对会自动处理之前未处理的消息,并通知相关的观察者。具体的顺序如下:
1.通知观察者 runloop 已经启动
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
调用 _objc_autoreleasePoolPush();方法,创建新的自动释放池。
2.通知观察者任何即将触发timer回调
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
3.通知观察者即将启动的非基于端口的源source0回调
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
4.启动任何准备好的非基于端口的源source0回调
5.如果基于端口的源source1准备好并处于等待状态,立即启动;并进入步骤 9。
⚠️:source1可以主动唤醒runloop。
6.通知观察者线程进入休眠kCFRunLoopBeforeWaiting
⚠️:观察者即将进入休眠时会释放旧的runloop,autoreleasePoolPop。并且创建新的runloop,autoreleasePoolPush。
7.将线程置于休眠直到任一下面的事件发生:
mach_msg() -> mach_msg_trap();休眠状态
某一事件到达基于端口的源source1port
定时器启动(定时器会设定一个时间,到了那个时间以后runloop会执行定时器的回调)
Run loop 设置的时间已经超时
run loop 被显式唤醒
8.通知观察者线程将被唤醒。kCFRunLoopAfterWaiting
9.处理未处理的事件handle_msg
如果用户定义的定时器启动,处理定时器事件并重启 run loop。进入步骤 2
如果输入source1,启动
如果 run loop 被显式唤醒而且时间还没超时,重启 run loop。进入步骤 2
⚠️:runloop一直在循环里执行,那么什么时候才会退出这个循环呢。就是在超时或者手动停止的时候。runloop退出的时候状态时这样的:kCFRunLoopExit。这个时候会释放自动释放池, _objc_autoreleasePoolPop()。
7.runloop与autoreleasePool
我们都知道,当把一个对象添加到autoreleasePool之后相当于延迟了对象的销毁,自动释放池中的对象在自动释放池被销毁(pool drain)的时候会被发送release方法,也就是说引用计数会减1。引用计数减1并不代表这个对象被销毁!
在MRC环境下,可以用NSAutoreleasePool这个类声明一个autoreleasePool,给一个对象发送autorelease消息就相当于把这个对象添加到autoreleasePool对象里。
也就是说这个方法:
[obj autorelease];
就相当于:
-(id)autorelease{
[NSAutoreleasePool addObject:self];
}
那么addObject方法是这样实现的:
-(void)addObject:(id)anObj{
[array addObject:anObj];
}也就是把这个被发送了autorelease消息的对象添加到当前正在使用的NSAutoreleasePool的数组里。
当前正在使用的NSAutoreleasePool被销毁的时候会调用drain方法,drain方法相当于给自动释放池里的每个对象发送了release消息。
-(void)drain{ [self dealloc];}
-(void)dealloc{
[self emptyPool];
[array release];
}
-(void)emptyPool{
for(id obj in array){[obj release];}
}
那么autoreleasePool是怎么实现的呢?
其实NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
相当于调用了objc_autoreleasePoolPush();方法。
[obj autorelease];
相当于调用了objc_autorelease(obj);方法。
[pool drain];
相当于调用了objc_autoreleasePoolPop();方法。
⚠️:不能给一个NSAutoreleasePool对象发送autorelease消息,因为这个对象的autorelease方法已经被重载了,运行时报错。
!!!那么在arc环境下是不能使用NSAutoreleasePool去创建自动释放池的,也不能调用autorelease方法,那么是怎么实现自动释放池的呢❓
用@autorelease{}来代替NSAutoreleasePool对象的生命和销毁。
❓但是,并不能说自动释放池就是在大括号结束的位置销毁的!参考:黑幕背后的autoreleasePool
那么自动释放池是在什么时候被销毁的呢?
在没有手加Autorelease Pool(@autoreleasepool{})的情况下,Autorelease对象是在当前的runloop迭代结束时释放的,而它能够释放的原因是系统在每个runloop迭代中都加入了自动释放池Push和Pop。
在每一个runloop的迭代过程中,在runloop里会注册两个observer,第一个observer监测即将进入runloop状态(kCFRunLoopEntry),并且调用_objc_autoreleasePoolPush()方法,创建一个自动释放池,这个方法的优先级最高,确保创建自动释放池的操作在其他所有操作之前执行。
第二个observer监测runloop的两个状态,一个是BeforeWaiting。在监测到这个状态以后调用_objc_autoreleasePoolPop()方法,销毁当前的自动释放池。并且调用_objc_autoreleasePoolPush()方法,创建新的自动释放池。另一个是exit,即将退出runloop这个状态。这个时候调用_objc_autoreleasePoolPush()方法,销毁自动释放池。
⚠️:什么时候需要我们手动创建自动释放池呢?
比如说,我们创建了大量的autorelease对象,都放在一个自动释放池里,这些对象占据的内存很大,如果一直不主动销毁自动释放池就会引起内存的暴增。这个时候就需要我们手动创建一个自动释放池。
🙋☝️🌰
for(int i = 0;i<count;<i++){
@autoreleasePool{
//读入图像并对图像进行处理
}
}//这样就避免了内存暴增
8.runloop和UI刷新
当UI发生变化的时候,比如说视图的frame变化,图层的层级变化,或者调用了setNeedsLayout/setNeedsDisplay方法的时候就需要刷新UI,这些需要修改的控件被标记为待处理,runloop的观察者观察到runloop即将进入睡眠状态或者即将退出的时候会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。
🐶:关于runloop对于tableView的优化思路。
场景1:
有些时候我们希望在TableView滑动的时候不去加载图片,而等到TableView停止滑动的时候再去加载。
解决方案1:
设置TableView的代理方法,判断TableView滑动的情况。
- (void)scrollViewDidScroll:(UIScrollView *)scrollView{}
解决方案2:
把设置图片放到一个方法里,设置图片的时候调用这个方法并且设置在runloop的defaultmode下执行,也就是说在滑动的时候就不会调用设置图片的方法。
场景2:UITableView里加载多张图片时非常容易造成界面的卡顿,可以利用预加载,缓存等方法来进行优化,监听runloop也可以作为一个优化的思路。
参考:RunLoop UITableViewCell加载高清大图的速度优化
这里面提到的一种思路是,我们加载图片的时候之所以会卡顿是因为我们在一个runloop里加载了多张图片,如果我们换一种做法,在一个runloop里一次加载一张图片,那么就肯定u会卡顿了。
1,因为这里用到了Runloop循环,那么我们可以监听到runloop的每次循环,在每一次循环当中我们考虑去进行一次图片下载和布局。
2,既然要在每次循环执行一次任务,我们可以先把所有图片加载的任务代码块添加到一个数组当中,每次循环取出第一个任务进行执行。
3,因为runloop在闲置的时候会自动休眠,所以我们要想办法让runloop始终处于循环中的状态。
关键的代码就是要创建一个监听者,监听runloop即将进入休眠的状态。
-(void)addRunloopObserver{
//获取当前的RunLoop
CFRunLoopRef runloop = CFRunLoopGetCurrent();
//定义一个centext
CFRunLoopObserverContext context = {
0,
( __bridge void *)(self),
&CFRetain,
&CFRelease,
NULL
};
//定义一个观察者
static CFRunLoopObserverRef defaultModeObsever;
//创建观察者
defaultModeObsever = CFRunLoopObserverCreate(NULL,
kCFRunLoopBeforeWaiting,
YES,
NSIntegerMax - 999,
&Callback,
&context
);
//添加当前RunLoop的观察者
CFRunLoopAddObserver(runloop, defaultModeObsever, kCFRunLoopDefaultMode);
//c语言有creat 就需要release
CFRelease(defaultModeObsever);
}
还有就是需要设置一个nstimer让runloop一直处于运行状态。
//定时器,保证runloop一直处于循环中
@property (nonatomic, weak) NSTimer *timer;
self.timer = [NSTimer scheduledTimerWithTimeInterval:0.001 target:self selector:@selector(setRunLoop) userInfo:nil repeats:YES];
//此方法主要是利用计时器事件保持runloop处于循环中,不用做任何处理
-(void)setRunLoop{}
在runloop循环里处理事件
//MARK: 回调函数
//定义一个回调函数 一次RunLoop来一次
static void Callback(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info){
ViewController * vcSelf = (__bridge ViewController *)(info);
if (vcSelf.saveTaskMarr.count > 0) {
//获取一次数组里面的任务并执行
SaveFuncBlock funcBlock = vcSelf.saveTaskMarr.firstObject;
funcBlock();
[vcSelf.saveTaskMarr removeObjectAtIndex:0];
}
}
9.关于runloop和AFNetworking
网上有很多资料说在AFNetworking里使用runloop创建了一个常驻线程,让后台线程不退出,一直处于运行状态,这是在AFNetworking2.x版本里采用的方法,在AFNetworking3.x版本里已经用NSURLSession替换了NSURLConnection,也就没有这种操作了。
首先要说明为什么需要加一个常驻的runloop。
首先了解一下使用NSURLConnection代理方法监听下载任务时的几种情况。
回顾一下NSURLConnection发送请求的方法:
发送同步请求:
[NSURLConnectionsendSynchronousRequest:requestreturningResponse:&responseerror:nil];⚠️:发送同步请求的问题在于会阻塞当前线程,并且请求到的数据一次性回传,放到一个对象里,会引起内存的暴增。
发送异步请求:
[NSURLConnectionsendAsynchronousRequest:requestqueue:[NSOperationQueuemainQueue]completionHandler:^(NSURLResponse *_Nullableresponse,NSData*_Nullabledata,NSError*_NullableconnectionError) {执行的block块}⚠️:发送异步请求的问题在于请求到的数据会被放到一个对象里,引起内存的增加。
通过代理发送异步请求:
NSURLConnection*connect =[[NSURLConnectionalloc]initWithRequest:requestdelegate:self];
⚠️:在代理方法里可以将下载的数据拼接起来并显示进度,但是内存也会飙升
1.在主线程调异步接口
当在主线程调用[[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:YES]时,请求发出,侦听任务(代理方法,回调函数)会加入到主线程的Runloop下,RunloopMode会默认为NSDefaultRunLoopMode。那也就意味着,当runloop的mode改变了以后就不会执行侦听任务。也就是说当用户在滑动tableview的时候,不会执行代理回调方法,只有停止滑动,mode切换成defaultMode以后才能执行代理方法。
解决方法:手动把runloopMode设置为NSRunLoopCommonModes。
⚠️:NSURLConnection类型的对象connect是一个局部变量,函数执行完就应该被销毁了为什么还可以执行代理方法呢?
因为这个方法内部其实会将connect作为一个source添加到当前的runloop中,并且执行运行模式为默认,等所有的代理方法执行完以后再销毁。
2.在子线程调同步接口
若在子线程调用同步接口,一条线程只能处理一个请求,因为请求一发出去线程就阻塞住等待回调,需要给每个请求新建一个线程,这是很浪费的,这种方式唯一的好处应该是易于控制请求并发的数量。
3.在子线程调异步接口⚠️
在子线程发送异步请求就需要把这个connection放到子线程的runloop里面去,但是子线程的runloop是需要手动开启的,如果每次在一个子线程里执行一个connection就开启一个子线程对应的runloop是非常不好的,所以AFNetworking采取的办法就是开启一个常驻的子线程,所有的请求共用一个响应线程,创建了一条常驻线程专门处理所有请求的回调事件。
⚠️那为什么在使用NSURLSession的时候就不需要创建常驻子线程了呢?
NSURLSessionTask会在子线程工作,不会阻碍主线程。在AF3.0没有再使用runloop来接受回调事件,而是创建NSURLSession时,传入了一个子操作队列NSOperationQueue,所有的回调事件都在这个操作队列中处理。Runloop的模式不会影响它的回调。
sessionWithConfiguration:delegate:delegateQueue:xxx
AF3.0在xxx这里传入了一个子操作队列,让所有的回调都在这个操作队列执行。