学习 RunLoop (二)

作者: 东方_未明 | 来源:发表于2016-06-19 18:31 被阅读236次

上一节主要讲了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

相关文章

  • 学习 RunLoop (二)

    上一节主要讲了RunLoop的理论的基础知识, 这一节讲一讲实践:修正一点: 根据源码,runloop要跑起来先判...

  • NSRunLoop

    深入理解RunLoop RunLoop深度探究(一) RunLoop深度探究(二) RunLoop深度探究(三) ...

  • 14-RunLoop-01

    一、RunLoop的认识 二、RunLoop对象 RunLoop源码: 三、RunLoop相关类 切换mode不会...

  • RunLoop的介绍

    本文介绍的RunLoop包含以下几个点: 一、什么是RunLoop二、RunLoop对象三、 RunLoop相关的...

  • IOS runloop 学习笔记

    这次学习 的内容是 runloop 1.runloop 是什么2.runloop 的作用3.runloop 和 线...

  • 第一篇:RunLoop的一些理论知识

    目录一、什么是RunLoop二、RunLoop和线程的关系三、RunLoop的Mode四、RunLoop的内部逻辑...

  • RunLoop

    一、RunLoop是什么二、RunLoop的内部结构 1、RunLoop和线程的关系 2、RunLoop和Mode...

  • RunLoop学习资料

    非常好的runloop学习系列 CoreFoundation源码 RunLoop系列之源码分析 关于Runloop...

  • 深入浅出 RunLoop

    前言 文章主要分为四个部分 一、RunLoop 简介 二、RunLoop 相关接口 三、RunLoop 相关逻辑流...

  • 7.Runloop

    一.RunLoop本质(回答runloop一定要答状态切换) 二.RunLoop数据结构 NSRunLoop位于F...

网友评论

本文标题:学习 RunLoop (二)

本文链接:https://www.haomeiwen.com/subject/lwubdttx.html