iOS 开发之RunLoop


RunLoop

从字面意思看:
1.运行循环
2.跑圈

基本作用(重要)

1.保持程序的持续运行。
2.处理App中的各种事件(比如触摸事件、定时器事件、Selector事件)。
3.节省CPU资源,提高程序性能:该做事时做事,该休息时休息。
......

  • 一、如果没有RunLoop
    runloop.png

没有RunLoop的情况下:第3行后程序就结束了。

  • 二、如果有了RunLoop
    runloop1.png

有RunLoop的情况下:
由于main函数里面启动了个RunLoop,所以程序并不会马上退出,保持持续运行状态。

// 用DefaultMode启动
// RunLoop的主函数,是一个死循环
void CFRunLoopRun(void) {   /* DOES CALLOUT */
    int32_t result;
    do {
        
        //CFRunLoopRunSpecific具体处理runloop的运行情况
        //CFRunLoopGetCurrent()  当前runloop对象
        //kCFRunLoopDefaultMode  runloop的运行模式的名称
        //1.0e10                 runloop默认的运行时间,即超时为10的九次方
        //returnAfterSourceHandled 回调处理
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        CHECK_FOR_FORK();
        
        //如果runloop没有停止且没有结束则继续循环
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}
  • 三、main函数中的RunLoop
    runloop2.png

第14行代码的UIApplicationMain函数内部就启动了一个RunLoop
1.所以UIApplicationMain函数一直没有返回,保持了程序的持续运行。
2.这个默认启动的RunLoop是跟主线程相关联的。

RunLoop对象
  • iOS中有2套API来访问和使用RunLoop:

1.Foundation:NSRunLoop
2.Core Foundation:CFRunLoopRef

1.NSRunLoopCFRunLoopRef都代表着RunLoop对象。
2.NSRunLoop是基于CFRunLoopRef的一层OC包装,所以要了解RunLoop内部结构,需要多研究CFRunLoopRef层面的API(Core Foundation层面)。

RunLoop与线程

1.每条线程都有唯一的一个与之对应的RunLoop对象(字典:线程作为key, RunLoop作为value)
2.主线程RunLoop已经自动创建好了,子线程的RunLoop需要主动创建
3RunLoop第一次获取时创建,在线程结束时销毁。

  • 主线程对应的runloop默认已经创建并且开启了,而子线程对应的runloop需要手动创建并开启
//获得runloop(创建runloop)

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
    t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
        // 创建字典
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        // 创建主线程对应的runloop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        // 使用字典保存主线程-主线程对应的runloop
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
        CFRelease(dict);
    }
    CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 从字典中获取子线程的runloop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    if (!loop) {
        // 如果子线程的runloop不存在,那么就为该线程创建一个对应的runloop
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        // 把当前子线程和对应的runloop保存到字典中
    if (!loop) {
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFUnlock(&loopsLock);
    CFRelease(newLoop);
    }
    if (pthread_equal(t, pthread_self())) {
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    return loop;
}
#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //OC语言的API
    //01 获得主线程对应的runloop对象 主运行循环
    NSRunLoop *mainRunloop = [NSRunLoop mainRunLoop];
    //NSLog(@"%@",mainRunloop);
    
    //02 获得当前的runloop对象
    NSRunLoop *currentRunloop = [NSRunLoop currentRunLoop];
    NSLog(@"%p---%p",currentRunloop,mainRunloop);
    
    
    //C语言的API
    //01 主运行循环
    CFRunLoopRef mainRunloopRef = CFRunLoopGetMain();
    //02 当前的运行循环
    CFRunLoopRef currentRunloopRef = CFRunLoopGetCurrent();
    NSLog(@"%p-%p",mainRunloopRef,currentRunloopRef);
    
    //转换
    NSLog(@"%p--%p",mainRunloop.getCFRunLoop,mainRunloopRef);
    
    [NSThread detachNewThreadSelector:@selector(run) toTarget:self withObject:nil];
}

-(void)run{
    NSLog(@"run---%@",[NSThread currentThread]);
    //子线程调用
    //NSLog(@"%@",[NSRunLoop currentRunLoop]);
    
    //runloop和线程的关系
    //(1) 一一的对应的关系(字典)
    //(2) 主线程对应的runloop默认已经创建并且开启了,而子线程对应的runloop需要手动创建并开启
    //(3) 线程销毁,那么runloop也将销毁
    
    //获得子线程对应的runloop |currentRunLoop该方法本身是懒加载的,如果是第一调用那么会创建当前线程对应的runloop并保存,以后调用则直接获取
    NSRunLoop *newThreadRunloop = [NSRunLoop currentRunLoop];
    NSLog(@"%@",newThreadRunloop);
    
    [newThreadRunloop run]; //开启runloop (该runloop开启后马上退出了)
因为在该子线程中的运行模式是nil,该runloop开启后马上退出了.
}
@end

注意:runloop在启动时,要选择一种运行模式,之后判断这个运行模式是否为nil判断运行模式中的source、timer是否为nil,如果为nil,则该运行模式就为nil),若所选的运行模式为nil,这个runloop就会马上退出;若所选的运行模式中的source\timer不为nilrunloop就会起来一直运行(死循环)。

  • 获得RunLoop对象

Foundation:
1.[NSRunLoop currentRunLoop]; // 获得当前线程的RunLoop对象
2.[NSRunLoop mainRunLoop];// 获得主线程的RunLoop对象

Core Foundation
1.CFRunLoopGetCurrent(); // 获得当前线程的RunLoop对象
2.CFRunLoopGetMain();// 获得主线程的RunLoop对象

  • RunLoop相关类(核心)

Core Foundation中关于RunLoop5个类
1.CFRunLoopRef(RunLoop本身)
2.CFRunLoopModeRef(RunLoop运行模式)
3.CFRunLoopSourceRef(输入源事件模式)
4.CFRunLoopTimerRef(定时器时间)
5.CFRunLoopObserverRef(观察者)

runloop3.png

图很重要

  • CFRunLoopModeRef

一、CFRunLoopModeRef代表RunLoop运行模式
1.一个 RunLoop 包含若干个 Mode(运行模式),每个Mode(运行模式)又包含若干个Source/Timer/Observer
2.每次RunLoop启动时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode
3.如果需要切换Mode,只能退出Loop再重新指定一个Mode进入(该过程是自动进行)。
4.这样做主要是为了分隔开不同组Source/Timer/Observer,让其互不影响

二、系统默认注册了5Mode:
1.kCFRunLoopDefaultModeApp的默认Mode,通常主线程是在这个Mode下运行。
2.UITrackingRunLoopMode界面跟踪 Mode,用于 ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode 影响。
3.UIInitializationRunLoopMode: 在刚启动 App时,进入的第一个 Mode启动完成后不再使用
4.GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。
5.kCFRunLoopCommonModes: 这是一个占位用的Mode,不是一种真正的Mode。


#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    
    
//    [self timer1];
//    [self timer2];
//子线程中开启定时器
[self performSelectorInBackground:@selector(timer2) withObject:nil];
}

-(void)timer1
{
    //01 创建定时器对象
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    
    //02 添加到runloop中
    //Mode :runloop的运行模式(5-默认|界面跟踪|占位)
    //把定时器对象添加到runloop中,并指定运行模式为默认:(只有当运行模式为NSDefaultRunLoopMode的时候,定时器才会工作)
    //[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    //当滚动textView的时候,主运行循环会切换运行模式(默认->界面追踪运行模式)
    //把定时器对象添加到runloop中,并指定运行模式为界面跟踪:(只有当运行模式为NUITrackingRunLoopMode的时候,定时器才会工作)
    //[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    //要求不管有没有在滚动控件,定时器都能够正常工作
    //[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    //[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];
    
    //把定时器对象添加到runloop中,并指定运行模式为commonModes:
    //只有当运行模式为被标记为commonModes的运行模式的时候,定时器才会工作
    //被标记为commonModes运行模式:UITrackingRunLoopMode | kCFRunLoopDefaultMode
    /*
    common modes = <CFBasicHash 0x7fa4c9d00d70 [0x10a3f17b0]>{type = mutable set, count = 2,
        entries =>
        0 : <CFString 0x10b2ed270 [0x10a3f17b0]>{contents = "UITrackingRunLoopMode"}
        2 : <CFString 0x10a411b60 [0x10a3f17b0]>{contents = "kCFRunLoopDefaultMode"}
    }*/
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];
}

-(void)timer2
{
    NSLog(@"timer++++++%@",[NSThread currentThread]);
    
    //定时器工作
    //该方法内部会自动将创建的定时器对象添加到当前的runloop,并且指定运行模式为默认
    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

    //手动创建子线程对应的runloop
    [[NSRunLoop currentRunLoop] run];
}

-(void)run
{
    NSLog(@"run----%@",[NSRunLoop currentRunLoop].currentMode);
}
  • CFRunLoopSourceRef是事件源(输入源)

1、以前的分法:
Port-Based Sources
Custom Input Sources
Cocoa Perform Selector Sources

runloop4.png

2、现在的分法
Source0非基于Port的(用户手动触发的:如按钮的点击事件)。
Source1基于Port的(系统触发的事件)。

  • CFRunLoopTimerRef(定时器)

1.CFRunLoopTimerRef是基于时间的触发器。
2.基本上说的就是NSTimer

  • CFRunLoopObserverRef(观察者)

1.CFRunLoopObserverRef是观察者,能够监听RunLoop状态改变。
2.可以监听的时间点有以下几个:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
kCFRunLoopEntry = (1UL << 0), //即将进入RunLoop
kCFRunLoopBeforeTimers = (1UL << 1),//即将处理Timer
kCFRunLoopBeforeSources = (1UL << 2),//即将处理Source
kCFRunLoopBeforeWaiting = (1UL << 5),//即将进入休眠
kCFRunLoopAfterWaiting = (1UL << 6),//刚从休眠中唤醒
kCFRunLoopExit = (1UL << 7)//即将退出RunLoop
};


#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

-(void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    //01 创建观察者对象
  
    /**
     * 参数说明
     * 第一个参数:分配存储空间(默认:CFAllocatorGetDefault())
     * 第二个参数:要监听的状态
     * 第三个参数:是否要持续监听 YES
     * 第四个参数:默认0
     * 第五个参数:block回调,当RunLoop状态改变的时候会调用
     */
    
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {

        /*
         kCFRunLoopEntry = (1UL << 0),
         kCFRunLoopBeforeTimers = (1UL << 1),
         kCFRunLoopBeforeSources = (1UL << 2),
         kCFRunLoopBeforeWaiting = (1UL << 5),
         kCFRunLoopAfterWaiting = (1UL << 6),
         kCFRunLoopExit = (1UL << 7),
         kCFRunLoopAllActivities = 0x0FFFFFFFU
         */
        switch (activity) {
            case kCFRunLoopEntry:
                NSLog(@"runloop进入");
                break;
            case kCFRunLoopBeforeTimers:
                NSLog(@"即将处理timer事件");
                break;
            case kCFRunLoopBeforeSources:
                NSLog(@"即将处理source事件");
                break;
            case kCFRunLoopBeforeWaiting:
                NSLog(@"即将进入休眠");
                break;
            case kCFRunLoopAfterWaiting:
                NSLog(@"被唤醒");
                break;
            case kCFRunLoopExit:
                NSLog(@"runloop退出");
                break;

            default:
                break;
        }
    });

    //02 监听当前runloop的运行状态
    /**
        * 参数说明
        * 第一个参数:RunLoop对象
        * 第二个参数:监听者
        * 第三个参数:RunLoop在哪种运行模式下的状态
        *          NSDefaultRunLoopMode == kCFRunLoopDefaultMode
        *          NSRunLoopCommonModes == kCFRunLoopCommonModes
        */
    
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    [NSTimer scheduledTimerWithTimeInterval:4.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
}

-(void)timerRun
{
    NSLog(@"处理收到的事件--timer--RUN");
}

@end

  • 网友分析RunLoop处理逻辑图


    runloop5.png
  • 官方RunLoop处理逻辑过程


    runloop6.png
  • 代码模拟RunLoop

#import <Foundation/Foundation.h>

void msg(int n)
{
    NSLog(@"runloop被唤醒");
    NSLog(@"<<<<<<< 处理收到的任务 >>>>>>>>");
}

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

        do {

            NSLog(@"有事件需要我处理吗?");
            NSLog(@"没有的话我就休息了!");
            NSLog(@"Runloop进入休眠.....zzz");

            int number = 0;
            scanf("%d",&number);
            msg(number);

        } while (1);

    }
    return 0;
}
  • RunLoop在iOS开发中的应用
    1、该方法会受到RunLoop的影响。
 [ performSelector:<#(nonnull SEL)#> withObject:<#(nullable id)#> afterDelay:<#(NSTimeInterval)#> ];

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation ViewController

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{

    //该方法内部会自动把事件添加到当前的RunLoop中,并且指定运行模式为默认模式(kCFRunLoopDefaultMode),在滚动TextView的时候没有切换到滚动模式(UITrackingRunLoopMode)
    
//    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0];
    
//    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSRunLoopCommonModes]];
    
    [self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"placeholder"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode,UITrackingRunLoopMode]];
    
}

@end

2、线程保活(开启一条常驻的子线程)

#import "ViewController.h"

@interface ViewController ()

@property (strong, nonatomic)NSThread *thread;

@end

@implementation ViewController

- (IBAction)createThreadBtnClick:(id)sender {
    
    //01创建线程
    NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    
    //02启动线程
    [thread start];
    
    self.thread = thread;
    
}

- (IBAction)goOnBtnClick:(id)sender {

    //让之前创建的子线程继续执行任务,主线程->子线程
    
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:YES];
    
}

//该方法执行完毕,线程对象就会进入到死亡状态
- (void)run1{
    
    NSLog(@"run1 ------ %@",[NSThread currentThread]);
    
    //01子线程的runLoop需要手动创建 + 启动
    //02runLoop启动之后,选择运行模式(默认),内部会判断运行模式是否为空
    NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
    
    //03往运行模式中添加事件(source|timer)
//    [NSTimer scheduledTimerWithTimeInterval:3.0 target:self selector:@selector(timerRun) userInfo:nil repeats:YES];
    //    source(port|timer|selector)
    
    //04为默认的运行模式添加port事件,目的是让运行模式不为空,把RunLoop开启起来
//    NSPort *port= [[NSPort alloc] init];
    NSPort *port = [NSPort port];
    [runLoop addPort:port forMode:NSRunLoopCommonModes];
    
    [runLoop run];//内部指定的原型模式为默认(NSDefaultRunLoopMode)
    
    NSLog(@"end ---------");
    
}

- (void)run2{
    
    NSLog(@"run2 ------ %@",[NSThread currentThread]);
}

- (void)timerRun{
    
    NSLog(@"%s",__func__);
    
}

@end

  • 有关RunLoop内部的自动释放池

a.自动释放池第一次创建:是在 RunLoop 启动的时候。
b.自动释放池最后一次销毁:是在 RunLoop 退出的时候。
c.自动释放池其他事件的创建和销毁:是在 RunLoop 即将进入到休眠的时候,会把之前的自动释放池销毁,重新创建一个自动释放池。

RunLoop相关知识希望给朋友们带来一丢丢的帮助,如有问题敬请批正!!

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