修正一点: 根据源码,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
其中任何一个,那么就直接退出runloop2.在开发中如何使用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,优先级最高,保证创建释放池发生在其他所有回调之前。
1.png
第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。
2.png在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。
在自己创建线程时,需要手动创建自动释放池AutoreleasePool
学习 RunLoop
最新推荐文章于 2020-07-11 17:15:00 发布