上一节主要讲了RunLoop的理论的基础知识, 这一节讲一讲实践:
修正一点: 根据源码,runloop要跑起来先判断mode是否为空,如果为空退出,
然后判断source0是否为空,如果为空退出,然后判断source1是否为空,如果为空退出,然后判断是否有timer,如果没有就退出,并没有判断是否有observer,所以runloop如果要跑起来,必须有source或者timer的其中一个
源码如下:
static Boolean __CFRunLoopModeIsEmpty(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFRunLoopModeRef previousMode) {
CHECK_FOR_FORK();
if (NULL == rlm) return true;
#if DEPLOYMENT_TARGET_WINDOWS
if (0 != rlm->_msgQMask) return false;
#endif
Boolean libdispatchQSafe = pthread_main_np() && ((HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && NULL == previousMode) || (!HANDLE_DISPATCH_ON_BASE_INVOCATION_ONLY && 0 == _CFGetTSD(__CFTSDKeyIsInGCDMainQ)));
if (libdispatchQSafe && (CFRunLoopGetMain() == rl) && CFSetContainsValue(rl->_commonModes, rlm->_name)) return false; // represents the libdispatch main queue
if (NULL != rlm->_sources0 && 0 < CFSetGetCount(rlm->_sources0)) return false;
if (NULL != rlm->_sources1 && 0 < CFSetGetCount(rlm->_sources1)) return false;
if (NULL != rlm->_timers && 0 < CFArrayGetCount(rlm->_timers)) return false;
struct _block_item *item = rl->_blocks_head;
while (item) {
struct _block_item *curr = item;
item = item->_next;
Boolean doit = false;
if (CFStringGetTypeID() == CFGetTypeID(curr->_mode)) {
doit = CFEqual(curr->_mode, rlm->_name) || (CFEqual(curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
} else {
doit = CFSetContainsValue((CFSetRef)curr->_mode, rlm->_name) || (CFSetContainsValue((CFSetRef)curr->_mode, kCFRunLoopCommonModes) && CFSetContainsValue(rl->_commonModes, rlm->_name));
}
if (doit) return false;
}
return true;
}
1. imageView
如果我们想让图片延时加载, 我们一般这样写:
[self.imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:2.0];
如果界面上有个TextView等滚动的控件, 然后我们一直滚动他, 发现2秒过去,图片还不加载, 松手后才加载..那么结合上一节的知识, 我们知道performSelector也是默认在runloop的NSDefaultRunLoopMode
模式下
也就是说,上面的代码写全其实是:
[self.imgView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"123"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode]];
应用场景: 如果我们在滚动tableView,如果想让图片显示在tableView的imageView上,如果图片比较大,渲染时间长,那时候就tableView滚动就会比较卡, 所以有的解决方案是:推迟image的显示,滚动tableView的时候,虽然图片下载完了,但是图片暂时不让它显示,等手指松开,停止滚动,再显示图片
2. 常驻线程
例如:想创建一个子线程,一直在后台监控用户的一些行为,所以我们需要创建的这个线程一直不能死
首先我们看看线程是怎么工作的:
先继承于NSThread, 创建一个我自己的线程(GYThread), 重写dealloc方法,这样这个线程如果被销毁了,我们可以打印监听到
#import "GYThread.h"
- (void)dealloc
{
NSLog(@"%@-------dealloc",self);
}
我们看看下面线程的执行:
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
GYThread *thread = [[GYThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run
{
NSLog(@"----执行任务run----");
}
打印结果如下:
2016-06-19 15:26:29.001 runloopDemo[14322:161704] ----执行任务run----
2016-06-19 15:26:29.003 runloopDemo[14322:161704] <GYThread: 0x7fafc3705490>{number = 2, name = (null)}-------dealloc
2016-06-19 15:26:30.478 runloopDemo[14322:161711] ----执行任务run----
2016-06-19 15:26:30.479 runloopDemo[14322:161711] <GYThread: 0x7fafc3424e40>{number = 3, name = (null)}-------dealloc
则 发现每次执行完任务, Thread就会被dealloc, 而每次开启内存地址都不同
那我弄一个strong的全局变量记录这个Thread,不让他释放, 每次点击调用一下线程开始的方法怎么样? 答案是否定的,第一次点击完,任务执行完,确实Thread不会被dealloc, 但是点击第二次让他直接开启时,就会崩溃,因为执行完任务,虽然Thread没有被释放,还处于内存中,但是它处于消亡状态, 苹果不允许线程这样做..会报错attempt to start the thread again
(尝试重新开启线程)
// 下面这三句代码是等价的, 这样runloop跑起来会立刻退出,因为我们还要往runloop中添加observe,timer,source,否则runloop跑起来会立刻退出
// 如果不传模式,不传时间,默认为NSDefaultRunLoopMode,过期时间为distantFuture(遥远的未来,不过期)
[[NSRunLoop currentRunLoop] run];
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
正确的添加常驻线程的做法
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
GYThread *thread = [[GYThread alloc] initWithTarget:self selector:@selector(run) object:nil];
[thread start];
}
- (void)run
{
NSLog(@"----执行任务run----");
// 创建RunLoop,并让runloop常驻
// 给runloop添加source或timer,才可以让线程常驻
// 添加port就相当于添加source,事件
[[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] run];
// 这句打印就不会执行了
NSLog(@"----任务结束run----");
}
关闭runloop
/* 应用场景:
一直在后台检测用户的行为,扫描用户的操作,检查操作,更新操作,检查联网状态
*/
// 如果想退出runloop, 只要关闭这条线程,或者让runloop中没有port,source
// 方式一:
[NSThread exit];
// 方式二:
[[NSRunLoop currentRunLoop] removePort:[NSPort port] forMode:NSDefaultRunLoopMode];
奇葩的添加常驻线程的做法(不推荐)
// 在子线程的任务中添加, 想关闭的时候,让flag=0即可
int flag = 1;
while (flag) {
[[NSRunLoop currentRunLoop] run];
NSLog(@"----runloop退出----");
}
缺点: 上面的代码会一直打印
----runloop退出----
,说明子线程的runloop一直进入,然后退出,再进入再退出, 因为这个runloop中没有timer,source的其中任何一个, 只有点击了给他下达了任务(比如上面的-(void)run方法
, 或者[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];
),才会给它一个事件(source),在这个时刻
, 就不会一直打印----runloop退出----
了, 这时候相当于给这个runloop,添加了source,所以这个runloop会进入循环, 就不会停止了,不会退出了
3. 给子线程添加NSTimer
- (void)viewDidLoad {
[super viewDidLoad];
// 给子线程添加NSTimer
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadAddTimer) object:nil];
[thread start];
}
// 给子线程添加NSTimer
- (void)threadAddTimer
{
@autoreleasepool {
// 方法一:
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(addTimer) userInfo:nil repeats:YES];
// 添加到当前线程中(子线程)
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
// 当前的runloop中有timer了, 所以这个子线程的runloop可以常驻了,不会退出了
[[NSRunLoop currentRunLoop] run];
// 方法二:
// 这个方法说明NSTimer加入到当前的runloop中的NSDefaultRunLoopMode的模式中,所以再加上一句runloop启动就和上面的方法一样了
// [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(addTimer) userInfo:nil repeats:YES];
// [[NSRunLoop currentRunLoop] run];
}
}
- (void)addTimer
{
NSLog(@"----这是子线程的定时器----");
}
给子线程添加了NSTimer, 如果我再滑动TableView,则子线程的NSTimer还是正常运行的..这种方式也解决了以前滑动定时器不好使的问题
子线程的定时器的模式跑在NSDefaultRunLoopMode
模式下,
滑动TableView是使主线程跑在了UITrackingRunLoopMode
模式下, 两个线程影响
4. 自动释放池
自动释放池: 将一些对象扔到这个池子中, 当这个池子被释放的时候, 让这个池子的所有对象都调用release方法
面试的时候经常会问到自动释放池什么时候死呢(被释放呢)?
答案就是: runloop在睡眠之前会被释放,因为runloop睡眠可能会睡很长时间,时间不定,如果睡眠时间很长,也不让自动释放池释放掉,则内存会堆扎,所以runloop在每次睡觉之前会被清理一次..
在runloop进入下一次循环被唤醒之前,又会创建一个新的释放池, 中间创建的临时变量就会放到这个池子中
一个runloop对应一个线程, 所以我们在子线程中创建runloop的时候,最好用创建一个自动释放池包裹住创建的runloop,如上面的代码..
因为我们看main.m中 就是用一个自动释放池包裹住的主线程的runloop, 这是一个安全的做法
说的详细一点:
5. runloop面试题:
一些面试官会问一些runloop的问题- -!
比如:
-
1.什么是runloop?
- 从字面意思说是: 运行循环, 跑圈
- 其实它的内部是一个高级的do-while循环, 在这个循环内部不断的处理各种任务(source, timer, observe)
- 一个线程对应一个runloop, 源码中有一个可变字典,key是线程,value是runloop对象
- 主线程的runloop默认已经启动,在main函数中, 子线程需要自己手动启动(调用run方法), 子线程的创建
[NSRunLoop currentRunLoop]
- runloop只能选择一个模式启动, 如果想用其他模式,只能退出当前循环,再进入新的模式, 如果当前模式中, 没有
source
,timer
其中任何一个,那么就直接退出runloop
-
2.在开发中如何使用runloop, 使用场景:
- 开启一个常驻线程(让一个子线程不进入消亡状态, 等待其他线程发来的消息,处理其他事件)
- 在子线程中开启一个定时器
- 在子线程中长期监控一些行为(比如沙盒的检测扫描)
- 可以控制定时器在那种模式下运行(Tranking,Default)
- 可以让某些事件(行为,任务),在特定模式下执行
- 可以添加observe监听runloop的一些状态(我们可以在处理所有点击事件,UI事件之前做一些事情)
- 我们可以自定义源(source)给他发送消息, CFRunLoopSourceCreate(..)函数创建source源 , 这个和
[self performSelector:@selector(run) onThread:thread withObject:nil waitUntilDone:NO];
比较相似
3.自动释放池什么时候释放
自动释放池释放的时间和RunLoop的关系:
注意,这里的自动释放池指的是主线程的自动释放池,我们看不见它的创建和销毁。自己手动创建@autoreleasepool {}是根据代码块来的,出了这个代码块就释放了。
App启动后,苹果在主线程 RunLoop 里注册了两个 Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()。
第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush()创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
在自己创建线程时,需要手动创建自动释放池AutoreleasePool