RunLoop基础元素解析

runloop

深入理解RunLoop这篇文章写的很好!

简介

RunLoop顾名思义,就是运行循环的意思。

基本作用:

  • 保持程序的持续运行
  • 处理App中的各类事件(触摸事件、定时器事件、Selector事件)
  • 节省CPU资源,提高程序性能:没有事件时就进行睡眠状态

内部实现:

  • do-while循环,在这个循环内部不断地处理各种任务(Source\Timeer\Observer)

注意点:

  • 一个线程对应一个RunLoop(采用字典存储,线程号为key,RunLoop为value
  • 主线程的RunLoop默认已经启动,子线程的RunLoop需要手动启动
  • RunLoop只能选择一个Mode启动,如果当前Mode没有任何Source、Timer、Observer,那么就不会进入RunLoop
    • RunLoop的主要函数调用顺序为:CFRunLoopRun->CFRunLoopRunSpecific->__CFRunLoopRun
  • 注意特殊情况,事实上,在只有Observer的情况,也不一定会进入循环,因为源代码里面只会显式地检测两个东西:Source和Timer(这两个是主动向RunLoop发送消息的);Observer是被动接收消息的
  • RunLoop在第一次获取时创建,在线程结束时销毁

RunLoop循环示意图:(针对上面的__CFRunLoopRun函数,Mode已经判断非空前提)

  • 图1

    RunLoop循环示意图

  • 图2


接触过微处理器编程的基本上都知道,在编写微处理器程序时,我通常会在main函数中写一个无限循环,然后在这个循环里面对外部事件进行监听,比如外部中断,一些传感器的数据等,在没有外部中断时,就让CPU进入低功耗模式。如果接收到了外部中断,就恢复到正常模式,对中断进行处理。

while (1) {
  // 根据中断决定是否切换模式执行任务
}
// 或者
for (;;) {
}

RunLoop和这个相似,也是在线程的main中增加了一个循环:

int main(int argc, char * argv[]) {
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
             // ......
    } while (running);
    return 0;
}

所以线程在这种情况下,便不会退出。

关于MainRunLoop

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

在viewDidLoad中设置断电,然后得到以下主线程栈信息:



可以看到,UIApplicationMain内部启动了一个和主线程相关联的RunLoop(_CFRunLoopRun)。在这里也可以推断,程序进入UIApplicationMain就不会退出了。我稍微对主函数进行了如下修改,并在return语句上打印了断点:



运行程序后,并不会在断点处停下,证实了上面的推断。

上面涉及了一个_CFRunLoopRun函数,接下来说明下iOS中访问和使用RunLoop的API:

  • Foundation--NSRunLoop
  • Core Foundation--CFRunLoopRef(开源)

因为后者是开源的,且前者是在后者上针对OC的封装,所以一般是对CFRunLoopRef进行研究。

两套API对应获取RunLoop对象的方式:

  • Foundation
    • [NSRunLoop currentRunLoop]; // 当前runloop
    • [NSRunLoop mainRunLoop];// 主线程runloop
  • Core Foundation
    • CFRunLoopGetCurrent();// 当前runloop
    • CFRunLoopGetMain();// 主线程runloop

值得注意的是,获取当前RunLoop都是进行懒加载的,也就是调用时自动创建线程对应的RunLoop。

RunLoop相关类:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTimerRef
  • CFRunLoopObserverRef

类之间的关系


以上图片说明了各个类之间的关系。

CFRunLoopModeRef说明:

  • 代表RunLoop的运行模式,一个RunLoop可以包含多个Mode,每个Mode可以包含多个Source、Timer、Observer
  • 每次RunLoop启动时,只能指定其中一个Mode,这个Mode就变成了CurrentMode
  • 当启动RunLoop时,如果所在Mode中没有Source、Timer、Observer,那么将不会进入RunLoop,会直接结束
  • 如果要切换Mode,只能退出Loop,再重新制定一个Mode进入

系统默认注册了5个Mode:

  • NSDefaultRunLoopMode:App的默认Mode,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用
  • GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
  • NSRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode

关于NSRunLoopCommonModes

  • 一个Mode可以将自己标记为“Common”属性,每当 RunLoop 的内容发生变化时,RunLoop会对标记有“Common”属性的Mode进行相适应的切换,并同步Source/Observer/Timer
  • 在主线程中,kCFRunLoopDefaultMode 和 UITrackingRunLoopMode这两个Mode都是被默认标记为“Common”属性的,从输出的主线程RunLoop可以查看。



    - 结合上面两点,当使用NSRunLoopCommonModes占位时,会表明使用标记为“Common”属性的Mode,在一定层度上,可以说是“拥有了两个Mode”,可以在这两个Mode中的其中任意一个进行工作

CFRunLoopTimerRef说明:

  • CFRunLoopTimerRef是基于时间的触发器,它包含了一个时间长度和一个回调函数指针。当它加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调

  • CFRunLoopTimerRef大部分指的是NSTimer,它受RunLoop的Mode影响

  • 由于NSTimer在RunLoop中处理,所以受其影响较大,有时可能会不准确。还有一种定时器是GCD定时器,它并不在RunLoop中,所以不受其影响,也就比较精确
    接下来说明各种Mode下,NSTimer的工作情况:

  • 情况1

    • 在对创建的定时器进行模式修改前,scheduledTimerWithTimeInterval创建的定时器只在NSDefaultRunLoopMode模式下可以正常运行,当滚动UIScroolView时,模式转换成UITrackingRunLoopMode,定时器就失效了。
    • 修改成NSRunLoopCommonModes后,定时器在两个模式下都可以正常运行
// 创建的定时器默认添加到当前的RunLoop中(没有就创建),而且是NSDefaultRunLoopMode模式
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 可以通过以下方法对模型进行修改
[[NSRunLoop mainRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
  • 情况2
    • timerWithTimeInterval创建的定时器并没有手动添加进RunLoop,所以需要手动进行添加。当添加为以下模式时,定时器只在UITrackingRunLoopMode模式下进行工作,也就是滑动UIScrollView时就会工作,停止滑动时就不工作
    • 如果把UITrackingRunLoopMode换成NSDefaultRunLoopMode,那么效果就和情况1没修改Mode前的效果一样
NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

// 在UITrackingRunLoopMode模式下定时器才会运行
[[NSRunLoop mainRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

CFRunLoopSourceRef说明:

  • Source分类
    • 按官方文档
      • Port-Based Sources
      • Custom Input Sources
      • Cocoa Perform Selector Sources
    • 按照函数调用栈
      • Source0:非基于Port的
        • Source0本身不能主动触发事件,只包含了一个回调函数指针
      • Source1:基于Port的,通过内核和其他线程通信,接收、分发系统事件
        • 包含了mach_port和一个回调函数指针,接收到相关消息后,会分发给Source0进行处理

CFRunLoopObserverRef说明:

  • CFRunLoopObserverRef是观察者,能够监听RunLoop的状态改变
  • 能够监听的状态
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
        kCFRunLoopEntry = (1UL << 0),        // 进入RunLoop
        kCFRunLoopBeforeTimers = (1UL << 1), //即将处理timer
        kCFRunLoopBeforeSources = (1UL << 2),//即将处理Sources
        kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
        kCFRunLoopAfterWaiting = (1UL << 6), //即将唤醒
        kCFRunLoopExit = (1UL << 7),         //即将退出RunLoop
        kCFRunLoopAllActivities = 0x0FFFFFFFU//所有活动
    };
  • 添加监听者步骤
    // 创建监听着
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"%ld", activity);
    });

    //    [[NSRunLoop currentRunLoop] getCFRunLoop]
    // 向当前runloop添加监听者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 释放内存
    CFRelease(observer);

CF的内存管理(Core Foundation):

  • 1.凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release
  • 比如CFRunLoopObserverCreate
  • 2.release函数:CFRelease(对象);

自动释放池释放的时间和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

综合上面,可以得到以下结论:

@autoreleasepool {}内部实现

有以下代码:

int main(int argc, const char * argv[]) {
    @autoreleasepool {
    }

    return 0;
}

查看编译转换后的代码:

int main(int argc, const char * argv[]) {
    /* @autoreleasepool */ { __AtAutoreleasePool __autoreleasepool;
    }

    return 0;
}

__AtAutoreleasePool是什么呢?找到其定义:

struct __AtAutoreleasePool {
  __AtAutoreleasePool() {atautoreleasepoolobj = objc_autoreleasePoolPush();}
  ~__AtAutoreleasePool() {objc_autoreleasePoolPop(atautoreleasepoolobj);}
  void * atautoreleasepoolobj;
};

可以看到__AtAutoreleasePool是一个类:

  • 其构造函数使用objc_autoreleasePoolPush创建了一个线程池,并保存给成员变量atautoreleasepoolobj。
  • 其析构函数使用objc_autoreleasePoolPop销毁了线程池

结合以上信息,main函数里面的__autoreleasepool是一个局部变量。当其创建时,会调用构造函数创建线程池,出了{}代码块时,局部变量被销毁,调用其析构函数销毁线程池。

RunLoop实际应用

常驻线程

当创建一个线程,并且希望它一直存在时,就需要使用到RunLoop,否则线程一执行完任务就会停止。
要向线程存在,需要有强指针引用他,其他的代码如下:

// 属性
@property (strong, nonatomic) NSThread *thread;

// 创建线程
_thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[_thread start];

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
    // 点击时使线程_thread执行test方法
    [self performSelector:@selector(test) onThread:_thread withObject:nil waitUntilDone:NO];
}

//
- (void)test
{
    NSLog(@"__test__");
}

就单单以上代码,是不起效果的,因为线程没有RunLoop,执行完test后就停止了,无法再让其执行任务(强制start会崩溃)。

通过在子线程中给RunLoop添加监听者,可以了解下performSelector:onThread:内部做的事情:

  • 调用performSelector:onThread: 时,实际上它会创建一个Source0加到对应线程的RunLoop里去,所以,如果对应的线程没有RunLoop,这个方法就会失效
    // 这句在主线程中调用
    // _thread就是下面的线程

    [self performSelector:@selector(run) onThread:_thread withObject:nil waitUntilDone:NO];

  • performSelecter:afterDelay:也是一样的内部操作方法,只是创建的Timer添加到当前线程的RunLoop中了
    // 创建RunLoop即将唤醒监听者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopBeforeTimers, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        // 打印唤醒前的RunLoop
        NSLog(@"%ld--%@", activity, [NSRunLoop currentRunLoop]);
    });

    // 向当前runloop添加监听者
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 释放内存
    CFRelease(observer);

    [self performSelector:@selector(setView:) withObject:nil afterDelay:2.0];

      // 使model不为空
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];

综合上面的解释,可以知道performSelector:onThread:没有起作用,是因为_thread线程内部没有RunLoop,所以需要在线程内部创建RunLoop。

创建RunLoop并使对应线程成为常驻线程的常见方式有2:

  • 方式1
    • 向创建的RunLoop添加NSPort(Sources),让Mode不为空,RunLoop能进入循环不会退出

      [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
      [[NSRunLoop currentRunLoop] run];
      
  • 方式2
    • 让RunLoop一直尝试运行,判断Mode是否为空,不是为空就进入RunLoop循环

      while (1) {
          [[NSRunLoop currentRunLoop] run];
      }
      
      

AFNetWorking就使用到了常驻线程:

  • 创建常驻线程
+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];

        // 创建RunLoop并向Mode添加NSMachPort,使RunLoop不会退出
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });

    return _networkRequestThread;
}
  • 使用常驻线程
- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;

        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

给子线程开启定时器

_thread = [[NSThread alloc] initWithTarget:self selector:@selector(test) object:nil];
[_thread start];


// 子线程添加定时器
- (void)subTimer
{
    // 默认创建RunLoop并向其model添加timer,所以后续只需要让RunLoop run起来即可
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    // 貌似source1不为空,source0就不为空
//    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
}

让某些事件(行为、任务)在特定模式下执行

比如图片的设置,在UIScrollView滚动的情况下,我不希望设置图片,等停止滚动了再设置图片,可以用以下代码:

// 图片只在NSDefaultRunLoopMode模式下会进行设置显示
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"Snip20150712_39"] afterDelay:2.0 inModes:@[NSDefaultRunLoopMode]];

先设置任务在NSDefaultRunLoopMode模式在执行,这样,在滚动使RunLoop进入UITrackingRunLoopMode时,就不会进行图片的设置了。

控制定时器在特定模式下执行

上文的《CFRunLoopTimerRef说明:》中已经指出

添加Observer监听RunLoop的状态

监听点击事件的处理(在所有点击事件之前做一些事情)

具体步骤在《CFRunLoopObserverRef说明:》中已写明

GCD定时器

注意:

  • dispatch_source_t是个类,这点比较特殊
//    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_global_queue(0, 0));
    dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

    dispatch_source_set_event_handler(timer, ^{
        NSLog(@"__");

        NSLog(@"%@", [NSThread currentThread]);

        static NSInteger count = 0;

        if (count++ == 3) {
            // 为什么dispatch_cancel不能用_timer?/
            // Controlling expression type '__strong dispatch_source_t' (aka 'NSObject<OS_dispatch_source> *__strong') not compatible with any generic association type
            // 类型错误,可能dispatch_cancel是宏定义,需要的就是方法调用,而不是变量
//            dispatch_cancel(self.timer);

            dispatch_source_cancel(_timer);
        }
    });
    // 定时器默认是停止的,需要手动恢复
    dispatch_resume(timer);

    // 需要一个强引用保证timer不被释放
    _timer = timer;

最后一点需要说明的是,SDWebImage框架的下载图片业务中也使用到了RunLoop,老确保图片下载成功后才关闭任务子线程。

参考文档

深入理解RunLoop

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

推荐阅读更多精彩内容

  • Run loop 剖析:Runloop 接收的输入事件来自两种不同的源:输入源(intput source)和定时...
    Mitchell阅读 12,355评论 17 111
  • 转载:http://www.cocoachina.com/ios/20150601/11970.html RunL...
    Gatling阅读 1,417评论 0 13
  • 原文地址:http://blog.ibireme.com/2015/05/18/runloop/ RunLoop ...
    大饼炒鸡蛋阅读 1,121评论 0 6
  • 深入理解RunLoop 由ibireme| 2015-05-18 |iOS,技术 RunLoop 是 iOS 和 ...
    橙娃阅读 817评论 1 2
  • 有一首歌轻轻唱过在我们的年青岁月中,有一个梦静静流过,在我们的心中。过去时光带走一切,拥有过的季节,但我...
    远岫清扬阅读 515评论 0 0