学习 RunLoop (二)

上一节主要讲了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,优先级最高,保证创建释放池发生在其他所有回调之前。


1.png

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop() 和 _objc_autoreleasePoolPush()释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。


2.png

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,开发者也不必显示创建 Pool 了。

在自己创建线程时,需要手动创建自动释放池AutoreleasePool

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容