IOS 之runLoop

先不扯什么概念,因为自己之前对概念理解不深刻,只有在项目中真正用到了才能真正体会。

使用场景一:NSTimer 倒计时

  • 倒计时有两种创建形式
  • 第一种 需要手动将定时器添加到NSRunLoop
[NSThread detachNewThreadSelector:@selector(startSteamTimer) toTarget:self withObject:nil];
- (NSTimer *) startSteamTimer{
return  [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
}
//这样的run方法永远不会调用必须 加入下面的代码
[[NSRunLoop currentRunLoop] addTimer:_streamTimer forMode:NSDefaultRunLoopMode];
//若在非主线程 需要自己启动RunLoop [[NSRunLoop currentRunLoop]  run]
  • 第二种 创建NSTimer 自动添加NSRunLoop
[NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:
                    @selector(_steamTimerAction) userInfo:nil repeats:YES];

使用场景二:用FTP 协议上传和下载文件时 用到了NSRunLoop


 CFReadStreamRef readStreamRef = CFReadStreamCreateWithFTPURL(NULL, ( __bridge CFURLRef) url);
        CFReadStreamSetProperty(readStreamRef,
                                kCFStreamPropertyFTPAttemptPersistentConnection,
                                kCFBooleanFalse);
        
        CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
        CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPUsePassiveMode, kCFBooleanTrue);
        CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPFetchResourceInfo, kCFBooleanTrue);
        CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPUserName, (__bridge CFStringRef) self.ftpUsername);
        CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPPassword, (__bridge CFStringRef) self.ftpPassword);
        //
        CFReadStreamSetProperty(readStreamRef, kCFStreamPropertyFTPProxy, kCFBooleanTrue);
        //
        
        self.dataStream = ( __bridge_transfer NSInputStream *) readStreamRef;
        self.dataStream.delegate = self;
        if (self.dataStream == nil) {
            
            [self.delegate ftpError:self withErrorCode:FTPClientCantReadStream];
            
        }
        
//这里是重点
        [self performSelector:@selector(scheduleInCurrentThread:)
                     onThread:[[self class] networkThread]
                   withObject:self.dataStream
                waitUntilDone:YES];
       
//        [self.dataStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];

        [self.dataStream open];
#pragma thread management
+ (NSThread *)networkThread {
    static NSThread *networkThread = nil;
    static dispatch_once_t oncePredicate;

    dispatch_once(&oncePredicate, ^{
        networkThread =
        [[NSThread alloc] initWithTarget:self
                                selector:@selector(networkThreadMain:)
                                  object:nil];
        [networkThread start];
    });

    return networkThread;
}

+ (void)networkThreadMain:(id)unused {
    do {
        @autoreleasepool {
            [[NSRunLoop currentRunLoop] run];
        }
    } while (YES);
}

- (void)scheduleInCurrentThread:(NSStream*)aStream
{
    [aStream scheduleInRunLoop:[NSRunLoop currentRunLoop]
                           forMode:NSRunLoopCommonModes];
}

看了下方法的含义:Unless the client is polling the stream, it is responsible for ensuring that the stream is scheduled on at least one run loop and that at least one of the run loops on which the stream is scheduled is being run

确保流至少在一个运行循环上调度,并且流调度所在的至少一个运行循环正在运行

在流对象放入run loop且有流事件(有可读数据)发生时,流对象会向代理对象发送stream:handleEvent:消息。在打开流之前,我们需要调用流对象的scheduleInRunLoop:forMode:方法,这样做可以避免在没有数据可读时阻塞代理对象的操作。我们需要确保的是流对象被放入正确的run loop中,即放入流事件发生的那个线程的run loop中。

那为什么要RunLoop

字面意思运行的循环,就像操作系统一样,手机一有电话就有反应,系统里面在循环“跑圈”,也借助do-while 死循环理解

  • RunLoop 实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行。
  • RunLoop 在没有事件处理的时候,会使线程进入睡眠模式,从而节省 CPU 资源,提高程序性能。

RunLoop 和线程

RunLoop 和线程是息息相关的,我们知道线程的作用是用来执行特定的一个或多个任务,在默认情况下,线程执行完之后就会退出,就不能再执行任务了。这时我们就需要采用一种方式来让线程能够不断地处理任务,并不退出。所以,我们就有了 RunLoop。

一条线程对应一个RunLoop对象,每条线程都有唯一一个与之对应的 RunLoop 对象。
RunLoop 并不保证线程安全。我们只能在当前线程内部操作当前线程的 RunLoop 对象,而不能在当前线程内部去操作其他线程的 RunLoop 对象方法。
RunLoop 对象在第一次获取 RunLoop 时创建,销毁则是在线程结束的时候。
主线程的 RunLoop 对象系统自动帮助我们创建好了(原理如 1.3 所示),而子线程的 RunLoop对象需要我们主动创建和维护。

官方模型


runloop.png

通过 Input sources(输入源)和 Timer sources(定时源)两种来源等待接受事件;然后对接受到的事件通知线程进行处理,并在没有事件的时候让线程进行休息

RunLoop 相关类

下面我们来了解一下Core Foundation框架下关于 RunLoop 的 5 个类,只有弄懂这几个类的含义,我们才能深入了解 RunLoop 的运行机制。

CFRunLoopRef:代表 RunLoop 的对象
CFRunLoopModeRef:代表 RunLoop 的运行模式
CFRunLoopSourceRef:就是 RunLoop 模型图中提到的输入源 / 事件源
CFRunLoopTimerRef:就是 RunLoop 模型图中提到的定时源
CFRunLoopObserverRef:观察者,能够监听 RunLoop 的状态改变

一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。

  • 每次 RunLoop 启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作当前运行模式(CurrentMode)。
  • 如果需要切换运行模式(CFRunLoopModeRef),只能退出当前 Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。
  • 这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响

CFRunLoopRef 类 代表 RunLoop 的对象

获取方式

  • Core Foundation

CFRunLoopGetCurrent(); // 获得当前线程的 RunLoop 对象
CFRunLoopGetMain(); // 获得主线程的 RunLoop 对象

当然,在Foundation 框架下获取 RunLoop 对象类的方法如下:

  • Foundation

[NSRunLoop currentRunLoop]; // 获得当前线程的 RunLoop 对象
[NSRunLoop mainRunLoop]; // 获得主线程的 RunLoop 对象

CFRunLoopModeRef. 运行模式

  • 1、kCFRunLoopDefaultMode:App的默认运行模式,通常主线程是在这个运行模式下运行

  • 2、UITrackingRunLoopMode:跟踪用户交互事件(用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他Mode影响)

  • 3、UIInitializationRunLoopMode:在刚启动App时第进入的第一个 Mode,启动完成后就不再使用

  • 4、GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到

  • 5、kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式(后边会用到)

其中kCFRunLoopDefaultMode、UITrackingRunLoopMode、kCFRunLoopCommonModes是我们开发中需要用到的模式

CFRunLoopTimerRef

CFRunLoopTimerRef是定时源(RunLoop模型图中提到过),理解为基于时间的触发器,基本上就是NSTimer(哈哈,这个理解就简单了吧)
下面我们来演示下CFRunLoopModeRef和CFRunLoopTimerRef结合的使用用法,从而加深理解

UITextView *tv = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, self.view.frame.size.width/2, self.view.frame.size.height/2)];

    tv.text =@"放很多字出现滚动条";

    tv.backgroundColor = UIColor.redColor;
    [self.view addSubview:tv];
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:_streamTimer forMode:NSDefaultRunLoopMode];

然后运行,这个时候我们发现如何我们不拖动UITextView的滚动条,定时器会稳定的每隔2秒调用run方法打印

但拖动的时候,我们发现没有打印

这是因为:

  • 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下

  • 而当我们拖动UITextView 的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这种模式下没有添加NSTimer,所有我们定时器不工作了
    但当我送松开滚动条,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了

  • 你可以试着将上述代码中的[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];语句换为[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];,也就是将定时器添加到当前RunLoop的UITrackingRunLoopMode下,你就会发现定时器只会在拖动Text View的模式下工作,而不做操作的时候定时器就不工作。
    那难道我们就不能在这两种模式下让NSTimer都能正常工作吗?

  • 当然可以,这就用到了我们之前说过的伪模式(kCFRunLoopCommonModes),这其实不是一种真实的模式,而是一种标记模式,意思就是可以在打上Common Modes标记的模式下运行

CFRunLoopSourceRef

CFRunLoopSourceRef是事件源(RunLoop模型图中提到过),CFRunLoopSourceRef有两种分类方法。

  • 第一种按照官方文档来分类(就像RunLoop模型图中那样):

Port-Based Sources(基于端口)
Custom Input Sources(自定义)
Cocoa Perform Selector Sources

  • 第二种按照函数调用栈来分类:

Source0 :非基于Port
Source1:基于Port,通过内核和其他线程通信,接收、分发系统事件

这两种分类方式其实没有区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。

备注:断点可以看到函数调用栈“Source0”

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者,用来监听RunLoop的状态改变
CFRunLoopObserverRef可以监听的状态改变有以下几种:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即将处理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即将进入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒:64
    kCFRunLoopExit = (1UL << 7),                // 即将从Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
};
  • 1、在ViewController.m中添加如下代码
 // 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
        NSLog(@"监听到RunLoop发生改变---%zd",activity);
    });

    // 添加观察者到当前RunLoop中
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 释放observer,最后添加完需要释放掉
    CFRelease(observer);

可以看到RunLoop的状态在不断的改变,最终变成了状态 32,也就是即将进入睡眠状态,说明RunLoop之后就会进入睡眠状态

RunLoop原理

好了,五个类都讲解完了,下边开始放大招了。这下我们就可以来理解RunLoop的运行逻辑了。


RunLoop运行逻辑图.png

这张图对于我们的理解RunLoop来说太有帮助了,下边我们可以理解RunLoop逻辑
每次在开启RunLoop的时候,所在线程所在线程的RunLoop会自动处理之前未处理的事件,并且通知相关的观察者

具体的顺序如下:

  • 1、通知观察者RunLoop已经启动
  • 2、通知观察者即将要开始的定时器
  • 3、通知观察者任何即将启动的非基于端口的源
  • 4启动任何准备好的非基于端口的源
  • 5、如果基于端口的源准备好并处于等待状态,立即启动;并进入步骤9
  • 6、通知观察者线程进入休眠状态
  • 7、将线程置于休眠知道任一下面的事件发生:

某一事件到达基于端口的源
定时器启动
RunLoop设置的时间已经超时
RunLoop被显示唤醒

  • 8、通知观察者线程将被唤醒
  • 9、处理未处理的事件

如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤2
如果输入源启动,传递相应的消息
如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

  • 10、通知观察者RunLoop结束。

runLoop一般的使用

我们在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些好事的操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存
那么怎么做呢?其实开头案例我有使用
添加一条常驻内存的子线程,在该线程的RunLoop下添加一Sources,开启RunLoop
具体实现过程如下:

    _threadP = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    [_threadP start];
}


- (void)run1 {
    
    NSLog(@"runn1");
    
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
   
    [[NSRunLoop currentRunLoop] run];
    
    NSLog(@"runloop没启动成功");
    NSLog(@"---run:%@",[NSThread currentThread]);
    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [self performSelector:@selector(run2222) onThread:self.threadP withObject:nil waitUntilDone:NO];
    NSLog(@"runw222");
    
}
- (void)run2222 {
    NSLog(@"我在这个线程想干啥就可以干啥");
    NSLog(@"---run:%@",[NSThread currentThread]);
}

运行之后发现打印了----run1-----,而未开启RunLoop则未打印

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

推荐阅读更多精彩内容