iOS --- Runloop

RunLoop基本作用:

1. 保持程序持续运行

2.  处理App中的各种事件

3.  节省CPU资源,提高程序性能

Runloop, 直译过来就是运行循环。

那到底Runloop在哪?

在不做任何手动开启Runloop操作,其实你也能感受到Runloop的存在。

可能有小伙伴立马跳出来说,你扯蛋,没看见过!!!!!!!

质疑是对的,那请问如果没有Runloop,当你开启App后,怎样保证主线程不会被销毁,使程序持续运行?

或者,我们来做个小测试。

1. 保持程序持续运行

main.m原文件

iOS --- Runloop_第1张图片

先做小小改动:


iOS --- Runloop_第2张图片

猜猜控制台打印出什么?


只有第一个NSLog是执行了,另外一个没响应,这能说明UIApplicationMain函数中,开启了一个和主线程相关的RunLoop,导致UIApplicationMain不会返回,一直在运行中,也就保证了程序的持续运行

2.  处理App中的各种事件

例如: 触摸事件,定时器事件,Selector事件等。

为什么这么说? App启动完成后,应该就完事了!!!

开玩笑,不可能完,还有很多要执行呢,即使没新开线程,在主线程完成,主线程还有任务,那么Runloop就会继续执行其他事件,直到任务完成。

3. 节省CPU资源,提高程序性能

程序运行起来时,当什么操作都没有做的时候,RunLoop就告诉CUP,现在没有事情做,我要去休息,这时CUP就会将其资源释放出来去做其他的事情,当有事情做的时候RunLoop就会立马起来去做事情

iOS --- Runloop_第3张图片

从上图看,如果把黑色箭头去掉,那么Runloop不提供Source, Timer, 那么Runloop就休息,线程无需要执行的任务,线程不工作。

RunLoop对象

Fundation框架 (基于CFRunLoopRef的封装)

NSRunLoop对象

CoreFoundation

CFRunLoopRef对象

因为Fundation框架是基于CFRunLoopRef的一层OC封装,这里我们主要研究CFRunLoopRef源码

如何获得RunLoop对象, 以下前2个使用频率相对较高。

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

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

Core FoundationCFRunLoopGetCurrent();// 获得当前线程的RunLoop对象

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

RunLoop和线程间的关系

// 拿到当前Runloop 调用_CFRunLoopGet0

CFRunLoopRef CFRunLoopGetCurrent(void) {

   CHECK_FOR_FORK();

    CFRunLoopRefrl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);

    if(rl) return rl;

    return_CFRunLoopGet0(pthread_self());

}

// 查看_CFRunLoopGet0方法内部

CF_EXPORT CFRunLoopRef_CFRunLoopGet0(pthread_t t) {

    if(pthread_equal(t, kNilPthreadT)) {

    t= pthread_main_thread_np();

    }

   __CFLock(&loopsLock);

    if(!__CFRunLoops) {

       __CFUnlock(&loopsLock);

    CFMutableDictionaryRefdict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL,&kCFTypeDictionaryValueCallBacks);

    //根据传入的主线程获取主线程对应的RunLoop

    CFRunLoopRefmainLoop = __CFRunLoopCreate(pthread_main_thread_np());

    //保存主线程 将主线程-key和RunLoop-Value保存到字典中

    CFDictionarySetValue(dict,pthreadPointer(pthread_main_thread_np()), mainLoop);

    if(!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)){

       CFRelease(dict);

    }

    CFRelease(mainLoop);

       __CFLock(&loopsLock);

    }

    //从字典里面拿,将线程作为key从字典里获取一个loop

    CFRunLoopRefloop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));

   __CFUnlock(&loopsLock);

    //如果loop为空,则创建一个新的loop,所以runloop会在第一次获取的时候创建

    if(!loop) { 

    CFRunLoopRefnewLoop = __CFRunLoopCreate(t);

       __CFLock(&loopsLock);

   loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops,pthreadPointer(t));

    //创建好之后,以线程为key runloop为value,一对一存储在字典中,下次获取的时候,则直接返回字典内的runloop

    if(!loop) {

       CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);

       loop = newLoop;

    }

       // don't release run loops inside the loopsLock, because CFRunLoopDeallocatemay end up taking it

       __CFUnlock(&loopsLock);

    CFRelease(newLoop);

    }

    if(pthread_equal(t, pthread_self())) {

       _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);

       if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {

            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void*)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);

       }

    }

    returnloop;

}

1.  每条线程都有唯一的一个与之对应的RunLoop对象

2.  RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value

3.  主线程的RunLoop已经自动创建好了,子线程的RunLoop需要主动创建

4. RunLoop在第一次获取时创建,在线程结束时销毁

 RunLoop结构体

__CFRunLoop结构体

struct __CFRunLoop {

    CFRuntimeBase _base;

    pthread_mutex_t _lock;          /* locked for accessing mode list */

    __CFPort _wakeUpPort;           // used for CFRunLoopWakeUp

    Boolean _unused;

    volatile _per_run_data*_perRunData;              // reset forruns of the run loop

    pthread_t _pthread;

    uint32_t _winthread;

    CFMutableSetRef _commonModes;

    CFMutableSetRef_commonModeItems;

    CFRunLoopModeRef _currentMode;

    CFMutableSetRef _modes;

    struct _block_item*_blocks_head;

    struct _block_item*_blocks_tail;

    CFAbsoluteTime _runTime;

    CFAbsoluteTime _sleepTime;

    CFTypeRef _counterpart;

};

两个成员变量,可能是大家见得比较多的。

CFRunLoopModeRef_currentMode;

CFMutableSetRef_modes;

CFRunLoopModeRef 指向__CFRunLoopMode结构体的指针,

__CFRunLoopMode结构体源码如下:

typedef struct __CFRunLoopMode*CFRunLoopModeRef;

struct __CFRunLoopMode {

   CFRuntimeBase _base;

   pthread_mutex_t _lock;  /* musthave the run loop locked before locking this */

   CFStringRef _name;

   Boolean _stopped;

   char _padding[3];

   CFMutableSetRef _sources0;

   CFMutableSetRef _sources1;

   CFMutableArrayRef _observers;

   CFMutableArrayRef _timers;

   CFMutableDictionaryRef _portToV1SourceMap;

   __CFPortSet _portSet;

   CFIndex _observerMask;

#if USE_DISPATCH_SOURCE_FOR_TIMERS

   dispatch_source_t _timerSource;

   dispatch_queue_t _queue;

   Boolean _timerFired; // set to true by the source when a timer has fired

   Boolean _dispatchTimerArmed;

#endif

#if USE_MK_TIMER_TOO

   mach_port_t _timerPort;

   Boolean _mkTimerArmed;

#endif

#if DEPLOYMENT_TARGET_WINDOWS

   DWORD _msgQMask;

   void (*_msgPump)(void);

#endif

   uint64_t _timerSoftDeadline; /* TSR */

   uint64_t _timerHardDeadline; /* TSR */

};

主要查看以下成员变量

CFMutableSetRef_sources0;

CFMutableSetRef_sources1;

CFMutableArrayRef_observers;

CFMutableArrayRef_timers;

通过上面分析我们知道,CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer,而RunLoop启动时只能选择其中一个Mode作为currentMode。

Source1/Source0/Timers/Observer

1. Source1 : 基于Port的线程间通信

2. Source0 : 触摸事件,PerformSelectors

iOS --- Runloop_第4张图片


iOS --- Runloop_第5张图片


iOS --- Runloop_第6张图片

frame #29: 0x000000010d085721 CoreFoundation`__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__ + 17,

SOURCE0:控制台通过“bt”指令打印完整的堆栈信息,由堆栈信息中可以发现,触摸事件,确实是会触发Source0事件。

NSTimer

iOS --- Runloop_第7张图片

Observer :监听器,用于监听RunLoop的状态

RunLoop相关类及作用

CFRunLoopRef - 获得当前RunLoop和主RunLoop

CFRunLoopModeRef - RunLoop 运行模式,只能选择一种,在不同模式中做不同的操作

CFRunLoopSourceRef - 事件源,输入源

CFRunLoopTimerRef - 定时器时间

CFRunLoopObserverRef - 观察者

1. CFRunLoopModeRef

CFRunLoopModeRef代表RunLoop的运行模式

iOS --- Runloop_第8张图片

Source 在OC里, Set是无序,唯一的多数据集合。

Observer 数组

Timer 数组

RunLoop 与 Mode的关系是?

答:RunLoop一对Mode多;

Mode里有什么?

答:Mode包含若干个Source、Timer、Observer

每次RunLoop启动时,会做什么操作?

答:指定其中一个 Mode;

如果需要切换Mode, 能直接切换吗?

答:不能。只能退出RunLoop,再重新指定一个Mode进入,这样做主要是为了分隔开不同组的Source、Timer、Observer,让其互不影响。如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出。

RunLoop能否没有Source,Timer?

答:不能。一种Mode中可以有多个Source(事件源,输入源,基于端口事件源例键盘触摸等) Observer(观察者,观察当前RunLoop运行状态) 和Timer(定时器事件源)。但是必须至少有一个Source或者Timer,因为如果Mode为空,RunLoop运行到空模式不会进行空转,就会立刻退出。

系统默认注册的5个Mode:

RunLoop 有五种运行模式,第一,第二种比较常用

1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行

2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响

3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode

4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到

5. kCFRunLoopCommonModes: 这是一个占位用的Mode,作为标记kCFRunLoopDefaultMode和UITrackingRunLoopMode用,并不是一种真正的Mode

Mode间的切换

开发中,经常需要做轮播图,都会加上定时器,当滑动轮播图的时候,NSTimer就会暂停,当我们停止滑动以后,NSTimer又会重新恢复的情况, 这是为什么呢?

答:NSTimer不管用是因为Mode的切换,因为如果我们在主线程使用定时器,此时RunLoop的Mode为kCFRunLoopDefaultMode,即定时器属于kCFRunLoopDefaultMode,那么此时我们滑动ScrollView时,RunLoop的Mode会切换到UITrackingRunLoopMode,因此在主线程的定时器就不在管用了,调用的方法也就不再执行了,当我们停止滑动时,RunLoop的Mode切换回kCFRunLoopDefaultMode,所以NSTimer就又管用了。

CFRunLoopSourceRef事件源(输入源)

Source分为两种

Source0:非基于Port的 用于用户主动触发的事件(点击button 或点击屏幕)

Source1:基于Port的 通过内核和其他线程相互发送消息(与内核相关)

CFRunLoopObserverRef

能够监听RunLoop的状态改变

iOS --- Runloop_第9张图片
iOS --- Runloop_第10张图片
iOS --- Runloop_第11张图片

至此,可以看出Observer确实用来监听RunLoop的状态,包括唤醒,休息,以及处理各种事件。

RunLoop处理逻辑


iOS --- Runloop_第12张图片

RunLoop退出

1. 主线程销毁RunLoop退出

2. Mode中有一些Timer 、Source、 Observer,这些保证Mode不为空时,保证RunLoop没有空转并且是在运行的,当Mode中为空的时候,RunLoop会立刻退出

3. 启动RunLoop的时设置停止时间

[NSRunLoopcurrentRunLoop]runUntilDate:<#(nonnull NSDate *)#>

[NSRunLoopcurrentRunLoop]runMode:<#(nonnull NSString *)#> beforeDate:<#(nonnull NSDate *)#>

RunLoop应用

1.常驻线程 ---> 给子线程开启一个RunLoop

注意:

1) 子线程执行完操作之后就会立即释放,即使我们使用强引用引用子线程使子线程不被释放,也不能给子线程再次添加操作,或者再次开启。

2)创建子线程相关的RunLoop,在子线程中创建即可,并且RunLoop中要至少有一个Timer 或 一个Source 保证RunLoop不会因为空转而退出,因此在创建的时候直接加入,如果没有加入Timer或者Source,或者只加入一个监听者,运行程序会崩溃

自动释放池

Timer和Source是什么?

答:Timer和Source也是一些变量,需要占用一部分存储空间。

Timer和Source能否一直保留?

答:要释放掉,如果不释放掉,就会一直积累,占用的内存也就越来越大,这显然不是我们想要的。

什么时候释放,怎么释放呢?

答:RunLoop内部有一个自动释放池,当RunLoop开启时,就会自动创建一个自动释放池,当RunLoop在休息之前会释放掉自动释放池的东西,然后重新创建一个新的空的自动释放池,当RunLoop被唤醒重新开始时,Timer,Source等新的事件就会放到新的自动释放池中,当RunLoop退出的时候也会被释放

注意:只有主线程的RunLoop会默认启动。也就意味着会自动创建自动释放池,子线程需要在线程调度方法中手动添加自动释放池。

@autorelease{// 执行代码

 }

此文部分引用链接: https://www.jianshu.com/p/de752066d0ad

你可能感兴趣的:(iOS --- Runloop)