IOS 之runLoop

2019-07-17  本文已影响0人  NextStepPeng

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

使用场景一:NSTimer 倒计时

[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 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 和线程

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)。

CFRunLoopRef 类 代表 RunLoop 的对象

获取方式

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

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

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

CFRunLoopModeRef. 运行模式

其中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方法打印

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

这是因为:

CFRunLoopSourceRef

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

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       // 监听全部状态改变  
};
 // 创建观察者
    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会自动处理之前未处理的事件,并且通知相关的观察者

具体的顺序如下:

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

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

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则未打印

上一篇 下一篇

猜你喜欢

热点阅读