美文网首页
RunLoop与线程

RunLoop与线程

作者: name007 | 来源:发表于2017-08-18 14:52 被阅读0次
    - (void)viewDidLoad {
        [super viewDidLoad];
        
        //每个线程 结束的时候 都会发送一个NSThreadWillExitNotification的通知
        //也可以点击调试窗口的暂停按钮,查看所有线程情况
        //首先 注册一个观察者对象 监听线程 是否结束(线程只要把 相关的函数执行完就结束)
        //通知中心 内部会起一个子线程 专门监听
        [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(threadWillEnd:) name:NSThreadWillExitNotification object:nil];
        [self test];
    }
    
    - (void)test
    {
        for (int i = 0; i < 4; ++i) {
            if (i==0) {
                self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run0) object:nil];
                [self.thread start];
                NSLog(@"创建需长期存活子线程:%@",self.thread);
            }else{
                //这样声明 运行完任务 线程就退出了
                NSThread *tread = [[NSThread alloc] initWithTarget:self selector:@selector(run) object:nil];
                [tread start];
                NSLog(@"创建子线程:%@",tread);
            }
        }
    }
    
    //需要长期存活的线程
    - (void)run0 {
        if (!_emptyPort) {
            self.emptyPort = [NSMachPort port];
        }
        
        NSLog(@"current thread = %@", [NSThread currentThread]);
        NSRunLoop *runloop = [NSRunLoop currentRunLoop];
        //    NSLog(@"----------currentRunLoop:%@",runloop);
        
        [self creatObserve];
        
        //添加端口并且不主动退出runloop可以使线程长期存活
        [runloop addPort:self.emptyPort forMode:NSDefaultRunLoopMode];//添加端口
        
        //单纯添加定时任务或者源并不能使线程长期存活,必须将runloop的超时时间设置成永远或者超级大的数1.0e10。并且不主动退出runloop,才能使线程长期存活
        //CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        //下边两个定时任务二选一
        //1  repeats:NO 定时任务执行一次,执行完runloop退出 线程结束
        
    //    self.timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(doTimerTask1:) userInfo:nil repeats:YES];
    //    [runloop addTimer:_timer forMode:NSDefaultRunLoopMode];//NSRunLoopCommonModes可以使timer在UITrackingRunLoopMode也响应
        //如果是通过scheduledTimerWithTimeInterval创建的NSTimer, 默认就会添加到RunLoop得DefaultMode中
        //    self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(doTimerTask1:) userInfo:nil repeats:YES];
        
    //    NSTimeInterval fireDate = CFAbsoluteTimeGetCurrent();
    //    self.cfTimer = CFRunLoopTimerCreateWithHandler(kCFAllocatorDefault, fireDate, 0.5f, 0, 0, ^(CFRunLoopTimerRef timer) {
    //        NSLog(@"cftimer task");
    //        _count++;
    //        if (_count == 2) {
    //            if (_cfTimer) {
    //                CFRunLoopRemoveTimer(CFRunLoopGetCurrent(), _cfTimer, kCFRunLoopCommonModes);
    //                CFRelease(_cfTimer);
    //                self.cfTimer = nil;//这里要将指针设置为nil,否则第二次调用handleRemoveCFTimer会出现EXC_BAD_ACCESS
    //            }
    //        }
    //    });
    //    CFRunLoopAddTimer(CFRunLoopGetCurrent(), _cfTimer, kCFRunLoopCommonModes);
    
        
        //2
        //    [self performSelector:@selector(printSomething) withObject:nil afterDelay:1];
        
        
        //输入源启动runloop
    //    [self creatSourceRunLoop];
        
        //3种启动方式
        //    - (void)run;
        //    - (void)runUntilDate:(NSDate *)limitDate;
        //    - (void)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
        
        //    这三种方式无论通过哪一种方式启动runloop,如果没有一个输入源或者timer附加于runloop上,runloop就会立刻退出。
        //      (1) 使用第一种启动方式,runloop会一直运行下去,在此期间会处理来自输入源的数据,并且会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
        //      (2) 使用第二种启动方式,可以设置超时时间,在超时时间到达之前,runloop会一直运行,在此期间runloop会处理来自输入源的数据,并且也会在NSDefaultRunLoopMode模式下重复调用runMode:beforeDate:方法;
        //      (3) 使用第三种启动方式,runloop会运行一次,超时时间到达或者第一个input source被处理且returnAfterSourceHandled==YES,则runloop就会退出。
        
        [self observeReturn];
        
        
    //    NSDate *nowDate = [NSDate date];
    //    NSDate *futureDate = [NSDate distantFuture];
    //    NSInteger delaySecond = 4000;
    //    NSDate *delayDate = [NSDate dateWithTimeIntervalSinceNow:delaySecond];
    //    NSTimeInterval future = [futureDate timeIntervalSinceDate:nowDate];
    //    [runloop runMode:NSDefaultRunLoopMode beforeDate:delayDate];
        
    }
    
    1E06A343-F5B7-46E9-B701-0D047C2A5D39.png

    两次定时任务后停止timer

    0E5C79E7-876F-4E1C-AA1B-E018EFF2301D.png

    returnAfterSourceHandled参数==NO


    6BDFF9E3-D37F-4596-A4E0-874058FC1C59.png

    returnAfterSourceHandled参数==YES

    0CE2EF15-9985-442B-9C54-F1F730DB5DD2.png

    苹果文档
    There are two categories of sources. Version 0 sources, so named because the version field of their context structure is 0, are managed manually by the application. When a source is ready to fire, some part of the application, perhaps code on a separate thread waiting for an event, must call
    CFRunLoopSourceSignal(_:)
    to tell the run loop that the source is ready to fire. The run loop source for CFSocket is currently implemented as a version 0 source.
    Version 1 sources are managed by the run loop and kernel. These sources use Mach ports to signal when the sources are ready to fire. A source is automatically signaled by the kernel when a message arrives on the source’s Mach port. The contents of the message are given to the source to process when the source is fired. The run loop sources for CFMachPort and CFMessagePort are currently implemented as version 1 sources.

    直接退出线程

    [NSThread exit];//内存泄漏,直接退出线程,不会有退出runloop操作


    A6788085-DA3C-47F1-97FC-46AB4171F253.png

    停止RunLoop

    除了用[[NSRunLoop currentRunLoop] run];和CFRunLoopRun();启动的runloop,是可以主动停止的

    停止方式有
    1、移除NSPort或者timer
    [runloop removePort:self.emptyPort forMode:NSDefaultRunLoopMode];//还可以停止timer

    2、主动掉停止方法
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    CFRunLoopRef cfRunLoop= runloop.getCFRunLoop;
    CFRunLoopStop(cfRunLoop);
    或直接
    CFRunLoopStop(CFRunLoopGetCurrent());

    Demo

    RunLoop问题与应用

    1.NSTimer

    Timer Source作为事件源,上层对应就是NSTimer(其实就是CFRunloopTimerRef),不添加到RunLoop中的NSTimer是无法正常工作的,注意如果滚动UIScrollView(UITableView、UICollectionview是类似的)RunLoopMode处于UITrackingRunLoopMode,造成处于NSDefaultRunLoopMode的NSTimer得不到响应,需要将NSTimer由添加时的NSDefaultRunLoopMode改为NSRunLoopCommonModes。

    2.AutoreleasePool

    子线程默认不会开启 Runloop,那出现 Autorelease 对象如何处理?不手动处理会内存泄漏吗?

    iOS 各个线程 Autorelease 对象的内存管理
    参考了 StackOverFlow 的回答:does NSThread create autoreleasepool automatically now?does NSThread create autoreleasepool automaticly now?。我再来简单阐述下,在子线程你创建了 Pool 的话,产生的 Autorelease 对象就会交给 pool 去管理。如果你没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法。在这个方法中,会自动帮你创建一个 hotpage(hotPage 可以理解为当前正在使用的 AutoreleasePoolPage,如果你还是不理解,可以先看看 Autoreleasepool 的源代码,再来看这个问题 ),并调用page->add(obj)将对象添加到 AutoreleasePoolPage 的栈中,也就是说你不进行手动的内存管理,也不会内存泄漏啦!StackOverFlow 的作者也说道,这个是 OS X 10.9+和 iOS 7+ 才加入的特性。

    黑幕背后的Autorelease

    NSAutoreleasePool
    The Application Kit creates an autorelease pool on the main thread at the beginning of every cycle of the event loop, and drains it at the end, thereby releasing any autoreleased objects generated while processing an event. If you use the Application Kit, you therefore typically don’t have to create your own pools. If your application creates a lot of temporary autoreleased objects within the event loop, however, it may be beneficial to create “local” autorelease pools to help to minimize the peak memory footprint.

    Each thread (including the main thread) maintains its own stack of NSAutoreleasePool objects (see Threads). As new pools are created, they get added to the top of the stack. When pools are deallocated, they are removed from the stack. Autoreleased objects are placed into the top autorelease pool for the current thread. When a thread terminates, it automatically drains all of the autorelease pools associated with itself.

    Threads
    If you are making Cocoa calls outside of the Application Kit’s main thread—for example if you create a Foundation-only application or if you detach a thread—you need to create your own autorelease pool.

    小结

    RunLoop不会创建和销毁autorelease pool,主线程每个event loop系统自动创建和销毁autorelease pool,子线程没有创建 Pool ,但是产生了 Autorelease 对象,就会调用 autoreleaseNoPage 方法,但是每个event loop不会自动创建和销毁autorelease pool。

    3.AFNetworking 的工作原理

    RunLoop的前世今生
    在AFNetworking2.6.3版本之前是有 AFURLConnectionOperation 这个类的,
    AFNetworking 3.0 版本开始已经移除了这个类,AFN没有自己创建线程,而是采用的下面的这种方式

    [inputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [outputStream scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];  
    SCNetworkReachabilityUnscheduleFromRunLoop(self.networkReachability, CFRunLoopGetMain(), kCFRunLoopCommonModes);
    

    2.6.3版本AFN自己创建线程并添加RunLoop,在 AFURLConnectionOperation 类中可以找到下面的代码

    + (void)networkRequestThreadEntryPoint:(id)__unused object {
        @autoreleasepool {
            [[NSThread currentThread] setName:@"AFNetworking"];
            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;
    }
    

    4.UI更新

    iOS刨根问底-深入理解RunLoop
    如果打印App启动之后的主线程RunLoop可以发现另外一个callout为_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv的Observer,这个监听专门负责UI变化后的更新,比如修改了frame、调整了UI层级(UIView/CALayer)或者手动设置了setNeedsDisplay/setNeedsLayout之后就会将这些操作提交到全局容器。而这个Observer监听了主线程RunLoop的即将进入休眠和退出状态,一旦进入这两种状态则会遍历所有的UI更新并提交进行实际绘制更新。
    通常情况下这种方式是完美的,因为除了系统的更新,还可以利用setNeedsDisplay等方法手动触发下一次RunLoop运行的更新。但是如果当前正在执行大量的逻辑运算可能UI的更新就会比较卡,因此facebook推出了AsyncDisplayKit来解决这个问题。AsyncDisplayKit其实是将UI排版和绘制运算尽可能放到后台,将UI的最终更新操作放到主线程(这一步也必须在主线程完成),同时提供一套类UIView或CALayer的相关属性,尽可能保证开发者的开发习惯。这个过程中AsyncDisplayKit在主线程RunLoop中增加了一个Observer监听即将进入休眠和退出RunLoop两种状态,收到回调时遍历队列中的待处理任务一一执行。

    5.事件响应和手势识别底层处理是一致的吗,为什么?

    iOS RunLoop详解
    事件响应:
    苹果注册了一个 Source1 (基于 mach port 的) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallback()。
    当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpringBoard 只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种 Event,随后用 mach port 转发给需要的App进程。随后苹果注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内部的分发。

    _UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture/处理屏幕旋转/发送给 UIWindow 等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancel 事件都是在这个回调中完成的。

    手势识别:
    当上面的 _UIApplicationHandleEventQueue() 识别了一个手势时,其首先会调用 Cancel 将当前的 touchesBegin/Move/End 系列回调打断。随后系统将对应的 UIGestureRecognizer 标记为待处理。
    苹果注册了一个 Observer 监测 BeforeWaiting (Loop即将进入休眠) 事件,这个Observer的回调函数是 _UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行GestureRecognizer的回调。
    当有 UIGestureRecognizer 的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

    6.runMode:NSRunLoopCommonModes beforeDate:

    这是一种错误的启动RunLoop方式,根本不能启动,因为NSRunLoopCommonModes:这是一个占位用的Mode,不是一种真正的Mode
    要使用runMode:NSDefaultRunLoopMode beforeDate:或runMode: UITrackingRunLoopMode beforeDate:启动

    引用

    does NSThread create autoreleasepool automatically now?
    NSAutoreleasePool
    OC源码 —— autoreleasepool
    黑幕背后的Autorelease
    深入理解@autoreleasepool
    Objective-C Autorelease Pool 的实现原理
    RunLoop的前世今生
    深入研究 Runloop 与线程保活
    基于runloop的线程保活、销毁与通信
    NSRunLoop的退出方式
    避免使用 GCD Global 队列创建 Runloop 常驻线程
    iOS RunLoop实例代码
    iOS进程间通信之CFMessagePort
    Runloop not being stopped by CFRunLoopStop?
    RunLoop深度探究(五)
    Cocoa深入学习:NSOperationQueue、NSRunLoop和线程安全
    深入理解RunLoop

    相关文章

      网友评论

          本文标题:RunLoop与线程

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