IOS RunLoop详解以及API使用

使用RunLoop的目的:

1) 使用端口或自定义输入源来 和其他线程通信

2) 使用线程的定时器; ( 在子线程中添加定时器 )

3) cocoa中使用任何performSelector...的方法

4) 使线程长期性工作

否则,开启一个线程的RunLoop没有意义



一 获取/创建RunLoop对象

        苹果不允许直接创建RunLoop,它提供了两个自动获取的函数: CFRunLoopGetMain()和CFRunLoopGetCurrent(). 线程和RunLoop之间是一一对应的。线程创建时并没有RunLoop,且如果不去获取,那么RunLoop一直都不存在. RunLoop的创建发生在第一次获取的时候( 除了主线程的RunLoop,你只能在一个线程的内部获取自己对应的RunLoop ) ;RunLoop的销毁发生在线程结束后;

     1)  [NSRunLoop currentRunLoop];

      2)  [NSRunLoop mainRunLoop];

      3)  CFRunLoopGetCurrent();

     4)  CFRunLoopGetMain();

     5)  [NSRunLoop currentRunLoop].getCFRunLoop; //NSRunLoop转CFRunLoopRef

主线程的RunLoop是程序开始就创建的;子线程的RunLoop在第一次获取RunLoop对象时创建的;




二. 配置RunLoop ( 输入源source0,source1, timer, observer )

2.1 RunLoop的组成结构

CFRunLoopRef                 RunLoop对象

CFRunLoopModeRef        RunLoop中的模式,每次只能运行在一种模式下

CFRunLoopSourceRef     输入源source0,source1

CFRunLoopTimerRef       输入源timer

CFRunLoopObserverRef  RunLoop的观察者对象



RunLoop结构体的内部结构:

    struct __CFRunLoop {

CFStringSetRef          _commonModes;       //set

CFMutableSetRef       _commonModeItems; //set

CFMutableSetRef       _modes;    //set

CFRunLoopMideRef   _currentMode ;   //当前运行的mode

    } 

   1. commonModeItems 能增加common表示的mode; modes也能增加多个RunLoopModeRef;

   2. RunLoop支持查询当前线程所运行的Mode, currentMode;

   3. RunLoop中存在一个common集合,用来组合几种mode,让其在commonMode时能并存运行;



RunLoopMode结构体的内部结构

   struct __CFRunLoopMode {

CFStringRef                 _name;

CFMutableSetRef        _source0;   //set

CFMutableSetRef        _source1;   //set

CFMutableArrayRef     _observers; //Array

CFMutableArrayRef     _timers;       //Array

   }

   1.每个Mode都有一个名字,用来区分不同的Mode, 以及加入commonMode;

   2.管理事件输入源集合: source0, source1;

   3. 管理timer输入源: timer;

   4.该Mode运行时,RunLoop所处状态的观察者集合,用来获取RunLoop所处的不同状态;


可以打印 NSLog(@"%@",[NSRunLoop currentRunLoop]);来知道

(1) RunLoop的运行状态(stop or run), 

(2) 在哪种mode(KCFRunLoopDefaultMode)下运行

(3) 以及注册为CommondMode的源或者timer;

(4) source, timer,observer的数量


关系:

1. CFRunLoopRef代表 RunLoop的运行模式;

2. RunLoop包含若干个Mode, 而每个Mode中又注册了若干个Source, timer,  observer;  事件输入源.timer等不直接与RunLoop有关联,而是注册在Mode中,         RunLoop每次只能在一种Mode下运行,只有注册在此Mode下的source,timer,observer才能被运行和反馈;

 3. 如果需要切换Mode,只能退出runLoop,重新指定一个Mode进入。这样不同组Mode注册的source,timer,observer相互独立;

CFRunLoopModeRef 没有对外暴露,只有系统注册的5中Mode;



CFRunLoopSourceRef

是事件产生的地方. source有两个版本:source0和source1.

(1) source0, 只包含一个回调(函数指针),它并不能主动触发事件。使用时,需要先调用CFRunLoopSourceSignal(source),将这个Source标记为待处理,然后手动调用CFRunLoopWajeUp(runLoop)来唤醒RunLoop,让其处理这个事件;

  (2)  source1, 包含一个mach_port和一个回调,被用于通过内核和其他线程相互发送消息.这种source能主动唤醒RunLoop的线程;



CFRunLoopTimerRef

是基于时间的触发器,和NSTimer可以混用.其包含一个时间长度和回调;当其加入到RunLoop时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调.



CFRunLoopObserverRef 

是观察者,每个Observer都包含一个回调, 当RunLoop的状态发生变化时,观察者就能通过回调接受这个变化;

typedef   CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {

      kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop

     kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer

     kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source

     kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠

           kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒

           kCFRunLoopExit          = (1UL << 7), // 即将退出Loop

};



上面的source,timer,obsercer被统称为modeItem, 一个item可以被同时加入多个mode,一个item被重复加入同一个mode时不会有效果.如果一个mode中一个item都没有,则RunLoop会直接退出,不进入循环;



 2.1 RunLoop是以指定的Mode运行的,指定的Mode必须存在一个输入源或者Timer,否则在进入Loop之前RunLoop就退出了;

1)  输入源 source0, source1

2) 定时器 timer

  3) 观察者observer


   2.2 生成观察者observer

  observer只能通过CFRunLoopRef对象的API去添加; 生成并向RunLoop中加入observer:

     CFRunLoopObserverContext context = {0, (__bridge void *)(self),NULL,NULL,NULL}; //不注册任何回调函数

                CFRunLoopObserverRef     observer1 = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, test, &context);

            CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer1, kCFRunLoopDefaultMode);

                 void test( CFRunLoopObserverRef observer, CFRunLoopActivity activity, void * info  ){  }



   2.3 生成自定义事件source (source0)

   2.3.1  CFRunLoopSourceContext

typedef struct {

      CFIndexversion;

      void *info;

      const void *(*retain)(const void *info);

           void(*release)(const void *info);

      CFStringRef(*copyDescription)(const void *info);

      Boolean(*equal)(const void *info1, const void *info2);

           CFHashCode(*hash)(const void *info);

           void(*schedule)(void *info, CFRunLoopRef rl, CFRunLoopMode mode);

         void(*cancel)(void *info, CFRunLoopRef rl,CFRunLoopMode mode);

        void(*perform)(void *info);

} CFRunLoopSourceContext;


//输入源的上下文对象,用来当该输入源状态发生变化时,进行回调

    CFRunLoopSourceContext context = {

        0,

        (__bridge void *)(self),

             NULL,

                 NULL,

                 NULL,

                 NULL,

                NULL,

              /* source注册到Mode时候的回调  void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){}*/

                schedule,

               /* source从Mode中删除时候的回调  void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){}*/

               cancel,

              /* source在RunLoop中执行时的回调 void perform( void * info ){}*/

              perform

    };

void schedule ( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){ }

         void cancel( void * info, CFRunLoopRef r1, CFRunLoopMode mode ){ }

void perform( void * info ){ }



      2.3.2  CFRunLoopSourceRef 

   NSTimer,CFRunLoopTimerRef 或 CFRunLoopObserverRef注册在RunLoop中的某种Mode下之后,会根据时间,或者runLoop的状态自动执行回调,但是source0却需要程序员自己在其他线程中去发送信号;, 下面介绍CFRunLoopSourceRef(source0)的注册,回调使用,以及移除;

  (1)创建source的环境,设置注册,执行,删除三个对应的回调;

    CFRunLoopSourceContext context = {

        0,

                     (__bridge void *)(self),

                 NULL,

              NULL,

                          NULL,

                          NULL,

                          NULL,

                          schedule,

                           cancel,

                           perform

               };



(2)  创建source (source0)

      self.source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);


(3)  注册到指定Mode中

        self.runLoop = CFRunLoopGetCurrent();

        CFRunLoopAddSource( self.runLoop, self.source, kCFRunLoopDefaultMode );


 (4) Source所处状态回调:( 3中状态:注册,执行,删除 )      

       因为注册,删除都会自动触发对应的回调方法;

      关于"执行"回调的调用: 因为我们注册的事件都是source0,所以没有对应的信号能够唤醒你注册的事件,而且CFRunLoopSouorceSignal没有唤醒RunLoop对应线程的能力 ,那么只有你自己去唤醒了; 使用如下方法去唤醒该source;

CFRunLoopSouorceSignal( self.source );

CFRunLoopWakeup( self.runLoop );


(5) Source从Mode中移除:

    CFRunLoopSourceInvalidate(self.source);  此时会有删除回调;



  2.3.3 生成基于时间的Timer

       NSTimer在RunLoop中注册:

    NSTimer * timer = [NSTimer timerWithTimeInterval:1 target:self selector:@selector(test) userInfo:nil repeats:YES];

        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];


   CFRunLoopTimerRef在RunLoop中注册

    CFRunLoopTimerContext context = { 0, NULL,NULL,NULL,NULL };

       CFRunLoopTimerRef   timer = CFRunLoopTimerCreate(kCFAllocatorDefault, 1, 0.1, 0, 0, callback, &context);

    CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);



2.3.4 生成基于端口的源:

     NSPort, NSMachPort,CFMessagePortRef

     现在ios系统不允许生成带有名字的port,否则直接报错并且crash; 只能使用匿名port,匿名port是不会被回调的;现在的唯一作用就是如AFNetworking中使用一样,注册一个匿名port,让RunLoop能进入Loop不退出;

      [[NSRunLoop currentRunLoop] addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];



  2.2  线程安全与RunLoop对象

CFRunLoopRef线程安全

NSRunLoop线程不安全,所以操作应该在runloop自身对应的线程的中完成;



三.启动RunLoop

3.1 启动的API:

   1) [[NSRunLoop currentRunLoop] run]; //无条件且以默认的NSDefaultRunLoopMode启动

    2) [[NSRunLoop currentRunLoop] runUntilDate:[NSDate new]]; //指定过期时间且以默认的NSDefaultRunLoopMode启动

    3) [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate new]];//指定过期时间,指定启动方式

   4)  CFRunLoopRun(); //子线程的runLoop需要启动

    5) CFRunLoopRunInMode(<#CFRunLoopMode mode#>, <#CFTimeInterval seconds#>, <#Boolean returnAfterSourceHandled#>)


3.2   Int32 result = CFRunLoopRunInMode( kCFRunLoopDefaultMode. 10, YES );

   if( result==kcFRunLoopRunStopped || result==kCFRunLoopFinished ) 

   {  }

    注意: RunLoop启动的Mode,必须存在至少一个输入源或者Timer,否则无法进入Loop,其中并不包括Observer;



四.退出RunLoop

     让RunLoop退出的方法:

    (1)给RunLoop设置超时时间;

      (2)   通知RunLoop停止

CFRunLoopStop([NSRunLoop currentRunLoop].getCFRunLoop);

      (3)   删除Mode中的所有输入源;  (这种方法不太好用)




五.杂谈

1. 因为Mode是以stringModeName去对应相应的CFRunLoopModeRef,且CFRunLoopModeRef并没有创建方式,所以我们能使用的只有两种, KCFRunLoopDefaultMode 和   UITrackingRunLoopMode; 一般就是用的KcFRunLoopDefaultMode;


2. 关于kCFRunLoopCommonModes,

为什么这个不算能使用中的一种,这种只是timer,source添加进Mode中的一种方式,被添加进来的timer,source会被具有common标记     的Mode所共有;即RunLoop  并不会有kCFRunLoopCommonModes这种运行状态,只是将该Mode下注册的timer,source0让其他mode共有;

 至于如何让其他Mode具有common标志,不用担心,你知道的KCFRunLoopDefaultMode 和 UITrackingRunLoopMode都已经加上了common标志;


3.关于定时器与页面滑动:

     很多人在不懂之前,写的定时器与页面滑动的事件相冲突,即页面滑动时,定时器不工作: 主要原因是页面滑动时,主线程的RunLoop会停止,并且以  UITrackingRunLoopMode形式启动,这个时候schedule方式生成的NSTimer处在KCFRunLoopDefaultMode下,所以不会被运行;解决办法时,将NSTimer注册到kCFRunLoopCommonModes下,则NSTimer在Mode切换时仍然可以运行;



4.runLoop的执行流程:

(1) 以指定Mode启动之后,根据ModeNameString生成对应Mode,检查当前Mode中是否存在Item(source,timer),(observer不算),如果没有,RunLoop直接返回;如果存在Item,则(2);

(2) 通知Observer,RunLoop即将进入Loop;  在 CFRunLoopRun()之后的程序表达式在RunLoop运行时不会指定到的,因为进入了loop;

(3) 通知Observer,即将触发timer回调;

(4) 通知observer,即将出发source0(非port)回调;

(5) 执行source0的回调;

(6) 检查是否有source1(基于port)处于ready状态,如果有就直接处理source1,然后跳转去消息处理;

         (7) 如果runLoop没有任何任务,就通知observer,RunLoop即将进入休眠(sleep),否则跳过休眠;

(8)在休眠的RunLoop被唤醒 , 比如CFRunLoopWakeup( self.runLoop );唤醒之后,处理该事件,并且开始新的一轮loop;



5. 主线程中的RunLoop状态函数:

{

     /// 1. 通知Observers,即将进入RunLoop

     /// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();

     __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);

     do {

  

         /// 2. 通知 Observers: 即将触发 Timer 回调。

         __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);

         /// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。

         __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);

         __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

  

         /// 4. 触发 Source0 (非基于port的) 回调。

         __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);

        __CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

  

         /// 6. 通知Observers,即将进入休眠

         /// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();

         __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

  

        /// 7. sleep to wait msg.

         mach_msg() -> mach_msg_trap();

        

        /// 8. 通知Observers,线程被唤醒

         __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

  

        /// 9. 如果是被Timer唤醒的,回调Timer

         __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

  

        /// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block

        __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

  

        /// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件

        __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);

  

    while (...);

  



    /// 10. 通知Observers,即将退出RunLoop

     /// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();

     __CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);


尤其注意 mach_msg() -> mach_msg_trap() 睡眠状态;



6  关于定时器NSTimer, CFRunLoopTimerRef

 NSTimer有个属性叫做tolerance(宽容度),标记了当时间到点之后容许的最大误差;如果这个时间点错过了,那么就需要等待下一个时间点到来了;



重点: 

(1)关于UI界面刷新:

当在操作UI时,比如改变frame,更新了UIView/CALayer的层次时,或手动调用了UIView/CALayer的setNeedsLayout/setNeedsDisplay方法后,这个UIView/CALayer就被标    记为待处理,并被提交到一个全局的容器中. 苹果注册了一个Observer监听BeforeWaiting(即将进入休眠)和Exit(即将退出Loop)事件,回调去执行一个很长的函数。

   ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。

      这个函数里会便利所有待处理的UIView/CALayer以执行实际的绘制和调整,并且更新UI界面。( UIView/CALayer的实际绘制和调整必须在主线程中,但是待处理标记可以在其他线程中操作 );




(2)网络中的运行 NSRULConnection

          source1    RunLoop                                                    CFMultiplexer  CFHTTPCooklie storage

CFSocket —-—--------------------------------———-> NSRULconnectionLoader  ——————————------------------------——>  delegate


在使用NSURLConnection时,你会传入一个Delegate,当调用[connection start]后,这个Delegate就会不停地收到事件回调。 实际上,start这个函数的内部会获取CurrentRunLoop,然后在其中的KCFDefaultMode中添加4个source0(需要手动触发的source). CFMultiplexerSource是负责各种Delegate回调的,CFHTTPCookieStorage是处理各种Cookie的.

当开始网络传输时,我们可以看到NSURLConnection创建了两个新的线程Lcom.apple.NSURLConnectionLoader和com,apple,CFSocked.private.CFSocket负责最底层的socket连接,NSURLConnectionLoader这个线程内部会使用RunLoop来接受底层socket的事件,并通过之前添加的Source0通知上层的Delegate; NSURLConnectionLoader中的RunLoop通过一些基于mach port的source接受来自底层CFSocket的通知,当收到通知后,其会在适合的时机向CFMutliplexSource等source0发送通知,同时唤醒Delegate线程的RunLoop来让其处理这些通知.CFMultiplexerSource会在Delegate线程的RunLoop对Delegate执行实际回调;

 


7.  当子线程进入runLoop,如何注册事件或抛给该线程执行某些任务:

由于IOS禁止了基于端口port的通信,所以只能用系统自带的方法:

    [self performSelectorOnMainThread:<#(nonnull SEL)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>];

       [self performSelector:<#(nonnull SEL)#> onThread:<#(nonnull NSThread *)#> withObject:<#(nullable id)#> waitUntilDone:<#(BOOL)#>];

将任务抛给指定的线程,且会注册到对应的RunLoop,这种方法会被立刻执行;

你可能感兴趣的:(IOS)