• iOS-RunLoop


    简单的说run loop是事件驱动的一个大循环,如下代码所示
    int main(int argc, char * argv[]) {
         //程序一直运行状态
         while (AppIsRunning) {
              //睡眠状态,等待唤醒事件
              id whoWakesMe = SleepForWakingUp();
              //得到唤醒事件
              id event = GetEvent(whoWakesMe);
              //开始处理事件
              HandleEvent(event);
         }
         return 0;
    }

    Cocoa会涉及到Run Loops的

    系统级:GCD,mach kernel,block,pthread
    应用层:NSTimer,UIEvent,Autorelease,NSObject(NSDelayedPerforming),
    NSObject(NSThreadPerformAddition),CADisplayLink,CATransition,CAAnimation,dispatch_get_main_queue()(GCD中dispatch到main queue的block会被dispatch到main RunLoop执行),NSPort,NSURLConnection,AFNetworking(这个第三方网络请求框架使用在开启新线程中添加自己的run loop监听事件)

    RunLoop原理(执行顺序的伪代码)

    SetupThisRunLoopRunTimeoutTimer(); // by GCD timer
    do {
         __CFRunLoopDoObservers(kCFRunLoopBeforeTimers);
         __CFRunLoopDoObservers(kCFRunLoopBeforeSources);

         __CFRunLoopDoBlocks();
         __CFRunLoopDoSource0();

         CheckIfExistMessagesInMainDispatchQueue(); // GCD

         __CFRunLoopDoObservers(kCFRunLoopBeforeWaiting);
         var wakeUpPort = SleepAndWaitForWakingUpPorts();
         // mach_msg_trap
         // Zzz...
         // Received mach_msg, wake up
         __CFRunLoopDoObservers(kCFRunLoopAfterWaiting);
         // Handle msgs
         if (wakeUpPort == timerPort) {
              __CFRunLoopDoTimers();
         } else if (wakeUpPort == mainDispatchQueuePort) {
              // GCD
              __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__()
         } else {
              __CFRunLoopDoSource1();
         }
         __CFRunLoopDoBlocks();
    } while (!stop && !timeout);

    构成

    Thread包含一个CFRunLoop,一个CFRunLoop包含一种CFRunLoopMode,mode包含CFRunLoopSource,CFRunLoopTimer和CFRunLoopObserver。

    CFRunLoopMode

    RunLoop只能运行在一种mode下,如果要换mode当前的loop也需要停下重启成新的。利用这个机制,ScrollView过程中 NSDefaultRunLoopMode的mode会切换UITrackingRunLoopMode来保证ScrollView的流畅滑动不受只能在 NSDefaultRunLoopMode时处理的事件影响滑动。同时mode还是可定制的。

    • NSDefaultRunLoopMode:默认,空闲状态
    • UITrackingRunLoopMode:ScrollView滑动时
    • UIInitializationRunLoopMode:启动时
    • NSRunLoopCommonModes:Mode集合 Timer计时会被scrollView的滑动影响的问题可以通过将timer添加到NSRunLoopCommonModes来解决

    //将timer添加到NSDefaultRunLoopMode中
    [NSTimer scheduledTimerWithTimeInterval:1.0
         target:self
         selector:@selector(timerTick:)
         userInfo:nil
         repeats:YES];
    //然后再添加到NSRunLoopCommonModes里
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0
         target:self
         selector:@selector(timerTick:)
         userInfo:nil
         repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    CFRunLoopTimer

    NSTimer是对RunLoopTimer的封装

    + (NSTimer *)timerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;
    + (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)ti invocation:(NSInvocation *)invocation repeats:(BOOL)yesOrNo;

    - (void)performSelector:(SEL)aSelector withObject:(id)anArgument afterDelay:(NSTimeInterval)delay inModes:(NSArray *)modes;

    + (CADisplayLink *)displayLinkWithTarget:(id)target selector:(SEL)sel;
    - (void)addToRunLoop:(NSRunLoop *)runloop forMode:(NSString *)mode;

    CFRunLoopSource

    • source0:处理如UIEvent,CFSocket这样的事件
    • source1:Mach port驱动,CFMachport,CFMessagePort

    CFRunLoopObserver

    Cocoa框架中很多机制比如CAAnimation等都是由RunLoopObserver触发的。observer到当前状态的变化进行通知。

    typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
         kCFRunLoopEntry = (1UL << 0),
         kCFRunLoopBeforeTimers = (1UL << 1),
         kCFRunLoopBeforeSources = (1UL << 2),
         kCFRunLoopBeforeWaiting = (1UL << 5),
         kCFRunLoopAfterWaiting = (1UL << 6),
         kCFRunLoopExit = (1UL << 7),
         kCFRunLoopAllActivities = 0x0FFFFFFFU
    };

    使用RunLoop的案例

    AFNetworking

    使用NSOperation+NSURLConnection并发模型都会面临NSURLConnection下载完成前线程退出导致NSOperation对象接收不到回调的问题。AFNetWorking解决这个问题的方法是按照官方的guidhttps://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Classes/NSURLConnection_Class/Reference/Reference.html#//apple_ref/occ/instm/NSURLConnection/initWithRequest:delegate:startImmediately:上写的NSURLConnection的delegate方法需要在connection发起的线程runloop中调用,于是AFNetWorking直接借鉴了Apple自己的一个Demohttps://developer.apple.com/LIBRARY/IOS/samplecode/MVCNetworking/Introduction/Intro.html的实现方法单独起一个global thread,内置一个runloop,所有的connection都由这个runloop发起,回调也是它接收,不占用主线程,也不耗CPU资源。

    + (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;
    }

    类似的可以用这个方法创建一个常驻服务的线程。

    TableView中实现平滑滚动延迟加载图片

    利用CFRunLoopMode的特性,可以将图片的加载放到NSDefaultRunLoopMode的mode里,这样在滚动UITrackingRunLoopMode这个mode时不会被加载而影响到。

    UIImage *downloadedImage = ...;
    [self.avatarImageView performSelector:@selector(setImage:)
         withObject:downloadedImage
         afterDelay:0
         inModes:@[NSDefaultRunLoopMode]];

    接到程序崩溃时的信号进行自主处理例如弹出提示等

    CFRunLoopRef runLoop = CFRunLoopGetCurrent();
    NSArray *allModes = CFBridgingRelease(CFRunLoopCopyAllModes(runLoop));
    while (1) {
         for (NSString *mode in allModes) {
              CFRunLoopRunInMode((CFStringRef)mode, 0.001, false);
         }
    }


    异步测试

    - (BOOL)runUntilBlock:(BOOL(^)())block timeout:(NSTimeInterval)timeout
    {
         __block Boolean fulfilled = NO;
         void (^beforeWaiting) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) =
         ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
              fulfilled = block();
              if (fulfilled) {
                   CFRunLoopStop(CFRunLoopGetCurrent());
              }
         };

         CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, beforeWaiting);
         CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

         // Run!
         CFRunLoopRunInMode(kCFRunLoopDefaultMode, timeout, false);

         CFRunLoopRemoveObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);
         CFRelease(observer);

         return fulfilled;
    }

    //NSRunLoop与NSTimer的联系

    在Windows时代,大家肯定对SendMessage,PostMessage,GetMessage有所了解,这些都是windows中的消息处理函数,那对应在ios中是什么呢,其实就是NSRunloop这个东西。在ios中,所有消息都会被添加到NSRunloop中,分为‘input source’跟'timer source'种,并在循环中检查是不是有事件需要发生,如果需要那么就调用相应的函数处理。

    我们在使用NSTimer的时候,可能会接触到runloop的概念,下面是一个简单的例子:

    复制代码
     1 - (void)viewDidLoad
     2 {
     3     [super viewDidLoad];
     4     // Do any additional setup after loading the view, typically from a nib.
     5     NSTimer * timer = [NSTimer scheduledTimerWithTimeInterval:1
     6                                               target:self
     7                                             selector:@selector(printMessage:)
     8                                             userInfo:nil
     9                                              repeats:YES];
    10 }
    复制代码

    这个时候如果我们在界面上滚动一个scrollview,那么我们会发现在停止滚动前,控制台不会有任何输出,就好像scrollView在滚动的时候将timer暂停了一样,在查看相应文档后发现,这其实就是runloop的mode在做怪。
    runloop可以理解为cocoa下的一种消息循环机制,用来处理各种消息事件,我们在开发的时候并不需要手动去创建一个runloop,因为框架为我们创建了一个默认的runloop,通过[NSRunloop currentRunloop]我们可以得到一个当前线程下面对应的runloop对象,不过我们需要注意的是不同的runloop之间消息的通知方式。

    接着上面的话题,在开启一个NSTimer实质上是在当前的runloop中注册了一个新的事件源,而当scrollView滚动的时候,当前的MainRunLoop是处于UITrackingRunLoopMode的模式下,在这个模式下,是不会处理NSDefaultRunLoopMode的消息(因为RunLoop Mode不一样),要想在scrollView滚动的同时也接受其它runloop的消息,我们需要改变两者之间的runloopmode.

    1 [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    简单的说就是NSTimer不会开启新的进程,只是在Runloop里注册了一下,Runloop每次loop时都会检测这个timer,看是否可以触发。当Runloop在A mode,而timer注册在B mode时就无法去检测这个timer,所以需要把NSTimer也注册到A mode,这样就可以被检测到。

    说到这里,在http异步通信的模块中也有可能碰到这样的问题,就是在向服务器异步获取图片数据通知主线程刷新tableView中的图片时,在tableView滚动没有停止或用户手指停留在屏幕上的时候,图片一直不会出来,可能背后也是这个runloop的mode在做怪,嘿嘿。

  • 相关阅读:
    DateTimeHelper
    Check F5 refresh submit post back repeatly
    GZip Compress SoapExtension
    WebBrowser 高级扩展 js扩展 js订阅C#事件
    DataSet GZip/Deflate Serializer
    Socket/Stream Data Helper
    历年评书出版一览表(1955~1994)
    c#检测字节流编码
    Execute SQL Server Store Procedure C# ADO.Net Wrapper Code Generator
    Directory File Disk Searcher
  • 原文地址:https://www.cnblogs.com/jx66/p/6237094.html
Copyright © 2020-2023  润新知