ios-Runloop简单介绍

一、简单介绍

最近也看了下很多资料,所以也想总结下,写篇文章来记录下。RunLoop指的是NSRunloop或者CFRunloopRef,CFRunloopRef是纯C的结构体,而NSRunloop仅仅只是对CFRunloopRef的OC封装。查看CFRunLoop.h文件中的源码,我们可以发现有下面的这一段代码typedef struct __CFRunLoop * CFRunLoopRef; 这就说明CFRunLoopRef 是指向结构体 struct __CFRunLoop的指针类型。下面是CFRunLoop的结构体,其实我们可以把Runloop理解成一个对象,是对事件循环封装的一个对象,这个对象管理了需要处理的事件和消息,并且提供了一个入口函数来执行事件循环里面的逻辑,线程执行到这个函数后会一直处于这个函数内部接收 、等待 、 处理 的循环中,直到这个循环结束为止。

下面是关于RunLoop的结构体

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 for runs of the run loop
    pthread_t _pthread;//RunLoop对应的那个线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;//当前运行的mode
    CFMutableSetRef _modes;
    struct _block_item *_blocks_head;
    struct _block_item *_blocks_tail;
    CFTypeRef _counterpart;
};

从源码看Runloop总是运行在某种特定的CFRunLoopModeRef下,一个Runloop可以包含多种模式。

那么我们应该如何获取这样的RunLoop对象呢,其实在CFRunLoop.h中有定义两个函数

CF_EXPORT CFRunLoopRef CFRunLoopGetCurrent(void);
CF_EXPORT CFRunLoopRef CFRunLoopGetMain(void);

之后再看下CFRunLoopMode的结构成员

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timer;
    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 */
};

在我们在手机上点击应用程序,启动应用程序的时候,操作系统会创建一个线程也就是我们应用程序的主线程去调用main函数。来启动应用程序,其中主线程之所以一直存在的原因是因为开启了Runloop,Runloop可以保持主线程一直存在也就是保证应用程序不退出以及监听事件比如说触摸事件,定时器事件等等。还有就是其实一个 RunLoop 包含若干个 Mode,每个Mode又包含若干个Source/Timer/Observer。每次RunLoop启动时,我们只能指定其中的一个Mode,这个Mode就是结构体中的currentMode,如果我们需要切换Mode,只能退出Loop,再重新去指定一个Mode再进入。主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

二、内部探析

Runloop有五种模式如下所示,

kCFRynLoopDefaultMode:App的默认模式,通常主线程是在这个Mode下运行,一般来说在默认模式下处理网络事件和timer事件
UITrackingRunLoopMode:其实也就是UI模式,也就是说我们在scrollView滚动的时候或者TextView滚动的时候便会在这个模式下,在这个模式下处理UI
kCFRunLoopCommonModes:这是一个占位Mode,不是一种真正的Mode,就是系统为了我们使用方便而设立的
UIInitializationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后不再使用
GSEventReceiveRunLoopMode:接受系统事件的内部Mode,通常用不到,只有系统的内核事件发生的时候才会去做处理

其实我们把定时器加到 NSRunLoopCommonModes模式中其实就是相当于把定时器加到了两种模式中,一种是NSDefaultRunLoopMode另外一种是UITrackingRunLoopMode

我们如果在使用定时器的时候,如果我们把定时器加到了主线程的运行循环中,我们又做了耗时的操作,会阻塞主线程,所以我们如果要做耗时操作就放在子线程中执行。

这里还要介绍下的是,我们在程序中当然也可以让主线程挂掉,只需要在主线程执行的代码中实现[NSThread exit]; 只是这样的话,无论我们点什么都没有反应了,因为主线程已经挂掉了,操作系统的线程池当中已经没有这个线程的,所以肯定也就无法再去处理事件了,我们之前一直让主线程去显示一些控件,显示在界面其实是显示在操作系统上面的。也就是说只是操作系统去问app显示什么东西而已。

在一个应用程序中如果主线程挂掉了,但是子线程还一直存在的话,子线程是不会挂掉的。因为子线程是由CPU调度的。

RunLoopMode其实也是一个结构体,在CFRunLoop.c中有定义

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

关于其内部的定义

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;	/* must have 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 */
};

简单的说下RunloopMode的一个架构

在CFRunLoop.h文件中的开头声明就声明了这三大类

typedef struct __CFRunLoopSource * CFRunLoopSourceRef;

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;

typedef struct __CFRunLoopTimer * CFRunLoopTimerRef;

Source就是输入源,在上面的__CFRunLoopMode的成员中我们可以看到输入源可以分为soucre0和source1,其中source0表示的是非系统的内核事件,source1表示的是系统的内核事件。

Source0只包含了一个回调(函数指针),它并不能主动触发事件,使用的时候,应该先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。

从下面的CFRunLoopTimerSetNextFireDate这个函数里面的一句话可以看出,Source0是不会直接唤醒RunLoop的,下面这段英文大致的意思就是说设置一个计时器的日期,而不是与运行循环的直接交互,所以我们将为调用者做一个唤醒。


以及我们去查看CFRunLoop.c文件中会发现有下面的这样一个函数

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(void (*perform)(void *), void *info) {
    if (perform) {
        perform(info);
    }
    getpid(); // thwart tail-call optimization
}

还有个函数是和Soure1有关的,下面就有设置到了mach_msg_header_t。这也是一个结构体,里面包含有目标端口。

static void __CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        void *(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info),
        mach_msg_header_t *msg, CFIndex size, mach_msg_header_t **reply,
#else
        void (*perform)(void *),
#endif
        void *info) {
    if (perform) {
#if DEPLOYMENT_TARGET_MACOSX || DEPLOYMENT_TARGET_EMBEDDED || DEPLOYMENT_TARGET_EMBEDDED_MINI
        *reply = perform(msg, size, kCFAllocatorSystemDefault, info);
#else
        perform(info);
#endif
    }
    getpid(); // thwart tail-call optimization
}

Source1 包含了端口和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。Source1 能主动唤醒 RunLoop 的线程

下面解释下什么是Source,就比如说touchesBegan方法做了些处理,一个点击事件,这个事件就可以说是一个soucre0。由自己触发管理,source1就是可以监听系统端口和其他线程相互发送消息,它能够主动唤醒RunLoop由Runloop和操作系统内核进行管理。Timers就是定时源。就是我们加入到运行循环中的定时器。系统会把这个时钟事件包装成定时源发送给当前线程的Runloop来处理

Observer就是观察者,可以监听Runloop的状态的变化

struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFIndex _rlCount;
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;			/* immutable */
    CFRunLoopObserverCallBack _callout;	/* immutable */
    CFRunLoopObserverContext _context;	/* immutable, except invalidation */
};

比如可以这么用


/**创建观察者  参数一: 指定给observer分配存储空间,这里采取默认的,参数二: 需要监听的状态类型,参数三: 是不是每次都要去监听,参数四: 优先级,参数五: 
监听到状态改变之后的回调*/
CFRunLoopObserverRef  observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), 
kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
            switch (activity) {
                case kCFRunLoopEntry:
                    NSLog(@"进入runloop");
                    break;
                case kCFRunLoopBeforeTimers:
                    NSLog(@"即将开始处理timer");
                    break;
                case kCFRunLoopBeforeSources:
                    NSLog(@"即将开始处理source");
                    break;
                case kCFRunLoopBeforeWaiting:
                    NSLog(@"即将进入睡眠");
                    break;
                case kCFRunLoopAfterWaiting:
                    NSLog(@"从睡眠中被唤醒");
                    break;
                case kCFRunLoopExit:
                    NSLog(@"即将退出");
                    break;
                default:
                    break;
            }
        });
        //给主运行循环添加观察者,参数一给哪个Runloop添加观察者,第二个观察者对象,第三个在哪种模式下监听
        CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopDefaultMode);

CFRunLoopGetCurrent()表示的是获取当前的运行循环,最后还有个释放对象,CFRelease(observer);

可以理解为RunLoop其内部就是一个 do-while 循环。当你调用 CFRunLoopRun() 时,线程就会一直停留在这个循环里;直到超时或我们手动停止该函数才会返回。如果说这样就可以停止CFRunLoopStop(self.downloadRunloop);

三、RunLoop核心

RunLoop 的核心是基于 mach port 的,休眠的时候调用的函数

__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }
  

在 Mach 中,所有的东西都是通过自己的对象实现的。 Mach 的一个对象不能直接调用另一个对象,只能通过消息传递的方式实现对象间的通信。消息是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,Mach是第一个以多线程方式处理任务的系统内核,因此多线程的底层实现机制是基于Mach的线程。

"消息"是 Mach 中最基础的概念,消息在两个端口 (port) 之间传递,这就是 Mach 的 IPC (进程间通信) 的核心。

Mach 的消息定义是在 <mach/message.h> 头文件的

typedef struct { 
mach_msg_header_t header; //消息头
mach_msg_body_t body; //可选的body
}mach_msg_base_t;

typedef    struct 
{
  mach_msg_bits_t    msgh_bits;//标志位,表示消息的性质
  mach_msg_size_t    msgh_size;//size则表示消息的大小
  mach_port_t        msgh_remote_port;//目标端口
  mach_port_t        msgh_local_port;//当前端口
  mach_port_name_t    msgh_voucher_port;
  mach_msg_id_t        msgh_id;//id标识了该消息的唯一性
} mach_msg_header_t;

一条 Mach 消息就是一个二进制数据包。它发送和接受消息是通过同一个API 进行的,option 标记了是消息传递的方向:

extern mach_msg_return_t    mach_msg(
	mach_msg_header_t *msg,
	mach_msg_option_t option,
	mach_msg_size_t send_size,
        mach_msg_size_t rcv_size,
	mach_port_name_t rcv_name,
        mach_msg_timeout_t timeout,
	mach_port_name_t notify);

为了实现消息的发送和接收,mach_msg() 函数实际上是调用了一个 Mach 陷阱 (trap),即函数mach_msg_trap(),当我们在用户态调用 mach_msg_trap() 时会触发陷阱机制,切换到内核态;内核态中内核实现的 mach_msg() 函数会完成实际的工作。

这里再介绍下陷阱机制,现代的CPU都是有优先级概念的,用户程序运行在低优先级,操作系统运行在高优先级。高优先级的一些指令低优先级无法执行。有一些操作只能由操作系统来执行,用户想要执行这些操作的时候就要通知操作系统,让操作系统来执行。用户态的程序就是用这种方法来通知操作系统的。

四、事件处理

之前一直没用太注意的com.apple.uikit.eventfetch-thread 这个线程,今天稍微看了下,这个地方子左边栏有提到,经过查阅一些资料知道了苹果其实会在一个单独的线程 com.apple.uikit.eventfetch-thread 中创建一个Runloop并且去添加mach_port去接收系统的事件,在接收到事件后由通过触发主线程的事件将事件传递过去。

下面发生在程序崩溃的时候。

下面的mach_msg_trap函数,我觉得可以理解成是调用objc_exception_throw抛出异常,然后这个mach_msg_trap函数去切换到内核态,通知系统有异常产生,然后向CPU提出中断请求。然后CPU进行响应。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值