【iOS】—— RunLoop初学

RunLoop

RunLoop简介

简单的说 RunLoop是一种高级的循环机制,让程序持续运行,并处理程序中的各种事件,让线程在需要做事的时候忙起来,不需要的话就让线程休眠。
在App运行的过程中,主线程的Runloop保证了主线程不被销毁从而保证应用的存活,从而能实时接收到用户的响应事件,能够触发定时事件。如果没有Runloop的话,程序执行完代码就会立马return。

RunLoop基本使用

  • 保持程序的持续运行
  • 处理app中各种事件
  • 节省CPU资源,提高程序性能:该做事时做事,该休眠时休眠。并且休眠时不占用CPU

Runloop伪代码

int main(int argc, char *argv[]) {
    
    
	@atuoreleasepool {
    
    
		int retVal = 0;
		do {
    
    
			// 睡眠中等待消息
			int message = sleep_and_wait();
			// 处理消息
			retVal = process_message(message);
		} while (0 == retVal);
		return 0;
	}
}

Runloop会一直在do-while循环中执行,这也就是我们写的程序不会在执行完一次代码之后就退出的原因了。

Runloop模型图

官方给的图:
请添加图片描述

Runloop对象

runloop实际上是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行相应的处理逻辑。线程执行了这个函数后,就会处于这个函数内部的循环中,直到循环结束,函数返回。

Runloop对象的获取

Runloop对象主要有两种获取方式:

// Foundation
NSRunLoop *runloop = [NSRunLoop currentRunLoop]; // 获得当前RunLoop对象
NSRunLoop *runloop = [NSRunLoop mainRunLoop]; // 获得主线程的RunLoop对象

NSRunloop类是Fundation框架中Runloop的对象,并且NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。

// Core Foundation
CFRunLoopRef runloop = CFRunLoopGetCurrent(); // 获得当前RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetMain(); // 获得主线程的RunLoop对象

CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全。
请添加图片描述
CoreFoundation框架中这两个函数的具体实现:

CFRunLoopRef CFRunLoopGetCurrent(void) {
    
    
    CHECK_FOR_FORK();
    CFRunLoopRef rl = (CFRunLoopRef)_CFGetTSD(__CFTSDKeyRunLoop);
    if (rl) return rl;
    return _CFRunLoopGet0(pthread_self());
}

CFRunLoopRef CFRunLoopGetMain(void) {
    
    
    CHECK_FOR_FORK();
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

这两个方法里都是调用了_CFRunLoopGet0这个方法

_CFRunLoopGet0方法

//全局的Dictionary,key是pthread_t,value是CFRunLoopRef
static CFMutableDictionaryRef __CFRunLoops = NULL;
//访问__CFRunLoops的锁
static CFSpinLock_t loopsLock = CFSpinLockInit;

// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
//t==0是始终有效的“主线程”的同义词

//获取pthread对应的RunLoop
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    
    
    if (pthread_equal(t, kNilPthreadT)) {
    
    
    //pthread为空时,获取主线程
    t = pthread_main_thread_np();
    }
    __CFSpinLock(&loopsLock);
    //如果这个__CFRunLoops字典不存在,即程序刚开始运行
    if (!__CFRunLoops) {
    
    
        __CFSpinUnlock(&loopsLock);
    //第一次进入时,创建一个临时字典dict
    CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
    //根据传入的主线程,获取主线程对应的RunLoop
    CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
    //保存主线程的Runloop,将主线程-key和RunLoop-Value保存到字典中
    CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
    //此处NULL和__CFRunLoops指针都指向NULL,匹配,所以将dict写到__CFRunLoops
    if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
    
    
    	//释放dict,因为我们已经将dict的内存保存了,该临时变量也就没用了,要及时的释放掉
        CFRelease(dict);
    }
    //释放mainRunLoop,刚才用于获取主线程的Runloop,已经保存了,就可以释放了
    CFRelease(mainLoop);
        __CFSpinLock(&loopsLock);
    }
    //以上说明,第一次进来的时候,不管是getMainRunLoop还是get子线程的runLoop,主线程的runLoop总是会被创建

	//从全局字典里获取对应的RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFSpinUnlock(&loopsLock);
    //如果找不到对应的Runloop
    if (!loop) {
    
    
    //创建一个该线程的Runloop
    CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFSpinLock(&loopsLock);
    //再次在__CFRunLoops中查找该线程的Runloop
    loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    //如果在字典中还是找不到该线程的Runloop
    if (!loop) {
    
    
    	//把刚创建的该线程的newLoop存入字典__CFRunLoops,key是线程t
        CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
        //并且让loop指向刚才创建的Runloop
        loop = newLoop;
    }
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may end up taking it
        __CFSpinUnlock(&loopsLock);
    	//loop已经指向这个newLoop了,他也就可以释放了
    	CFRelease(newLoop);
    }

	//如果传入线程就是当前线程
    if (pthread_equal(t, pthread_self())) {
    
    
        _CFSetTSD(__CFTSDKeyRunLoop, (void *)loop, NULL);
        if (0 == _CFGetTSD(__CFTSDKeyRunLoopCntr)) {
    
    
        	//注册一个回调,当线程销毁时,销毁对应的RunLoop
            _CFSetTSD(__CFTSDKeyRunLoopCntr, (void *)(PTHREAD_DESTRUCTOR_ITERATIONS-1), (void (*)(void *))__CFFinalizeRunLoop);
        }
    }
    //返回该线程对应的Runloop
    return loop;
}

流程大概是这个样子:
请添加图片描述

从这部分源码我们可以得知:

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取线程的RunLoop时创建,RunLoop会在线程结束时销毁
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
  • RunLoop会在线程结束时销毁

RunLoop的相关类

与RunLoop相关的类有5个

  • CFRunLoopRef 代表了RunLoop的对象
  • CFRunLoopModeRef RunLoop的运行模式
  • CFRunLoopSourceRef 就是RunLoop模型图中提到的输入源(事件源)
  • CFRunLoopTimerRef 定时源
  • CFRunLoopObserverRef观察者,监听RunLoop状态的改变

1.一个RunLoop包含若干个Mode,每个Mode又包含若干个Source/Timer/Observer
在这里插入图片描述
2.每次调用RunLoop的主函数时,只能指定其中的一个Mode,这个Mode被称作CurrentMode
3.如果需要切换Mode,只能退出Loop,再重新指定一个Mode进入。这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响
4.如果一个mode中一个Sourcr/Timer/Observer都没有,则RunLoop会直接退出,不进入循环

RunLoop相关类的实现

CFRunLoopRef

看一下CFRunLoopRef的源码:

struct __CFRunLoop {
    
    
    CFRuntimeBase _base;
    pthread_mutex_t _lock;			/* locked for accessing mode list */
    __CFPort _wakeUpPort; // 使用 CFRunLoopWakeUp 内核向该端口发送消息可以唤醒runloop
    Boolean _unused;
    volatile _per_run_data *_perRunData;              // reset for runs of the run loop
    pthread_t _pthread; 		// runloop对应的线程
    uint32_t _winthread;
    CFMutableSetRef _commonModes;  // 存储的是字符串,记录所有标记为common的mode
    CFMutableSetRef _commonModeItems; // 存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode; // 当前运行的mode
    CFMutableSetRef _modes;           // 装着一堆CFRunLoopModeRef类型
    struct _block_item *_blocks_head; // do blocks时用到
    struct _block_item *_blocks_tail;
    CFAbsoluteTime _runTime;
    CFAbsoluteTime _sleepTime;
    CFTypeRef _counterpart;
};

Runloop中除了记录了一些属性外,重点还是一下几个:

pthread_t _pthread; 		// runloop对应的线程
CFMutableSetRef _commonModes;  // 存储的是字符串,记录所有标记为common的mode
CFMutableSetRef _commonModeItems; // 存储所有common标记的mode的item(source、timer、observer)
CFRunLoopModeRef _currentMode; // 当前运行的mode
CFMutableSetRef _modes;           // 装着一堆CFRunLoopModeRef类型,runloop中的所有模式

RunLoop中主要的变量就是_pthread_currentMode_modes_currentMode主要就是为了在_modes中找当前对应的mode的item,然后发送消息。而_commonModes_commonModeItems完全就是为了common标记mode准备的,如果我们选择的mode是commonMode,那么就不用在_modes中找每个mode对应的item了,因为被标记的mode的item都在_commonModeItems中,直接给他里边的所有item发消息就完了。

CFRunLoopModeRef

typedef struct __CFRunLoopMode *CFRunLoopModeRef;

struct __CFRunLoopMode {
    
    
    CFRuntimeBase _base;
    pthread_mutex_t _lock;    /* must have the run loop locked before locking this */
    CFStringRef _name; //mode名称,运行模式是通过名称来识别的
    Boolean _stopped; //mode是否被终止
    char _padding[3];
    //整个结构体最核心的部分
------------------------------------------
    CFMutableSetRef _sources0; // Sources0
    CFMutableSetRef _sources1; // Sources1
    CFMutableArrayRef _observers; // 观察者
    CFMutableArrayRef _timers; // 定时器
------------------------------------------
    CFMutableDictionaryRef _portToV1SourceMap;//字典    key是mach_port_t,value是CFRunLoopSourceRef
    __CFPortSet _portSet;//保存所有需要监听的port,比如_wakeUpPort,_timerPort都保存在这个数组中
    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 */
};

  • 一个CFRunLoopModeRef对象有一个name,若干source0source1timerobserverport,可以看出来事件都是由mode在管理,而RunLoop管理着Mode。
  • Mode实际上是Source,Timer 和 Observer 的集合,不同的Mode把不同组的Source、timer、Observer隔绝开来。runloop在某一时刻只能运行在一个mode下,处理这一个mode中的source、timer、observer。
    在这里插入图片描述
五种运行模式

系统默认注册的五个mode:

  • kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  • UIInitializationRunLoopMode:在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
  • GSEventReceiveRunLoopMode:接受系统事件的内部 Mode,通常用不到
  • kCFRunLoopCommonModes:并不是一种模式
    只是一个标记,当mode标记为common时,将mode添加到runloop中的_commonModes中。runloop中的_commonModes实际上是一个Mode的集合,可使用CFRunLoopAddCommonMode()将Mode放到_commonModes中。每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems里的同步到具有Common标记的所有的Mode里
CommonModes

在RunLoop对象中,前面有一个有一个叫CommonModes的概念,它记录了所有标记为common的mode:

//简化版本
struct __CFRunLoop {
    
    
    pthread_t _pthread;
    CFMutableSetRef _commonModes;//存储的是字符串,记录所有标记为common的mode
    CFMutableSetRef _commonModeItems;//存储所有commonMode的item(source、timer、observer)
    CFRunLoopModeRef _currentMode;//当前运行的mode
    CFMutableSetRef _modes;//存储的是CFRunLoopModeRef对象,不同mode类型,它的mode名字不同
};
  • 一个Mode可以将自己标记为Common属性,通过将其ModeName添加到RunLoop的commonModes中。
  • 每当RunLoop的内容发生变化时,RunLoop都会将_commonModeItems里的Source/Observer/Timer同步到具有Common标记的所有Mode里。其底层原理如下:
void CFRunLoopAddCommonMode(CFRunLoopRef rl, CFStringRef modeName) {
    
    
    CHECK_FOR_FORK();
    if (__CFRunLoopIsDeallocating(rl)) return;
    __CFRunLoopLock(rl);
    if (!CFSetContainsValue(rl->_commonModes, modeName)) {
    
    
    //获取所有的_commonModeItems
    CFSetRef set = rl->_commonModeItems ? CFSetCreateCopy(kCFAllocatorSystemDefault, rl->_commonModeItems) : NULL;
    //获取所有的_commonModes
    CFSetAddValue(rl->_commonModes, modeName);
    if (NULL != set) {
    
    
        CFTypeRef context[2] = {
    
    rl, modeName};
        //将所有的_commonModeItems逐一添加到_commonModes里的每一个Mode
        CFSetApplyFunction(set, (__CFRunLoopAddItemsToCommonMode), (void *)context);
        CFRelease(set);
    }
    } else {
    
    
    }
    __CFRunLoopUnlock(rl);
}

CFRunLoop对外暴露的管理Mode接口只有下面两个:

CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName, ...);
什么是Mode Item?Mode到底包含哪些类型的元素?
  • RunLoop需要处理的消息,包括time以及source消息,他们都属于Mode item
  • RunLoop也可以被监听,被监听的对象是observer对象,也属于Mode item
  • 所有的mode item都可以被添加到Mode中,Mode中可以包含多个mode item,一个item也可以被加入多个mode。但一个item被重复加入同一个mode时是不会有效果的。如果一个mode中一个item都没有,则RunLoop会退出,不进入循环
  • mode暴露的mode item的接口有下面几个:
CFRunLoopAddSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
CFRunLoopRemoveSource(CFRunLoopRef rl, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef rl, CFRunLoopTimerRef timer, CFStringRef mode);
  • 我们仅能通过操作mode name来操作内部的mode,当你传入一个新的mode name但RunLoop内部没有对应的mode时,RunLoop会自动帮你创建对应的CFRunLoopModeRef
  • 对于一个RunLoop来说,其内部的mode只能增加不能删除

CFRunLoopSourceRef

根据官方描述,CFRunLoopSourceRefinput sources的抽象。
CFRunLoopSource分为Source0Source1两个版本。
它的结构如下:

struct __CFRunLoopSource {
    
    
    CFRuntimeBase _base;
    uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态才会被处理
    pthread_mutex_t _lock;
    CFIndex _order;   //执行顺序
    CFMutableBagRef _runLoops;//包含多个RunLoop
    //版本
    union {
    
    
    CFRunLoopSourceContext version0;    /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

可一通过共用体union看出,它有两个版本,Source0Source1

Source0

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

Source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行时,必须要先把它标为signal状态,以下是source0结构体:

typedef struct {
    
    
    CFIndex	version;
    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;

Source0 :非基于Port的 处理事件,什么叫非基于Port的呢?就是说你这个消息不是其他进程或者内核直接发送给你的。一般是APP内部的事件, 比如hitTest:withEvent的处理, performSelectors的事件。

Source1

Source1包含了mach_port和一个回调(函数指针),Source1可以监听系统端口,通过内核和其他线程通信,接收、分发系统事件,他能主动唤醒RunLoop(由操作系统内核进行管理)

Source1在处理的时候会分发一些操作给Source0去处理。

source1结构体:

typedef struct {
    
    
    CFIndex	version;
    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);
#if TARGET_OS_OSX || TARGET_OS_IPHONE
    mach_port_t	(*getPort)(void *info);
    void *	(*perform)(void *msg, CFIndex size, CFAllocatorRef allocator, void *info);
#else
    void *	(*getPort)(void *info);
    void	(*perform)(void *info);
#endif
} CFRunLoopSourceContext1;

Source1 :基于mach_Port的,来自系统内核或者其他进程或线程的事件,可以主动唤醒休眠中的RunLoop(iOS里进程间通信开发过程中我们一般不主动使用)。mach_port大家就理解成进程间相互发送消息的一种机制就好, 比如屏幕点击, 网络数据的传输都会触发sourse1。

例:
一个APP在前台静止着,此时,用户用手指点击了一下APP界面,那么过程就是下面这样的:
我们触摸屏幕,先摸到硬件(屏幕),屏幕表面的事件会先包装成Event,Event先告诉source1(mach_port),source1唤醒RunLoop,然后将事件Event分发给source0,然后由source0来处理。

CFRunLoopTimerRef

CFRunLoopTimer是基于时间的触发器,其包含一个时间长度、一个回调(函数指针)。当其加入runloop时,runloop会注册对应的时间点,当时间点到时,runloop会被唤醒以执行那个回调。
并且CFRunLoopTimerNSTimertoll-free bridged(对象桥接),可以相互转换。其结构如下:

struct __CFRunLoopTimer {
    
    
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;
    CFMutableSetRef _rlModes;//包含timer的mode集合
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;        /* immutable */
    CFTimeInterval _tolerance;          /* mutable */
    uint64_t _fireTSR;            /* TSR units */
    CFIndex _order;            /* immutable */
    CFRunLoopTimerCallBack _callout; //timer的回调   
    CFRunLoopTimerContext _context;   //上下文对象
};
对于NSTimer:

scheduledTimerWithTimeIntervalRunLoop的关系:

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

系统会将NSTimer自动加入NSDefaultRunLoopMode模式中,所以它就等同于下面代码:

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
NSTimer在滑动时停止工作的问题

在之前写仿写知乎日报的时候,首页一个tableView里嵌套了一个scrollView,scrollView是一个自动轮播图,当我们拖动tableView的时候,scrollView也不动了,这个问题当时学长帮我解决的,一直不知道原因:

    self.timer = [NSTimer scheduledTimerWithTimeInterval:5.0 target:self selector:@selector(timeOut:) userInfo:nil repeats:YES];
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    [runloop addTimer:self.timer forMode:NSRunLoopCommonModes];

我们来写个例子:

    self.scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(0, 0, 300, 200)];
    self.scrollView.scrollEnabled = YES;
    self.scrollView.pagingEnabled = YES;
    self.scrollView.bounces = YES;
    self.scrollView.alwaysBounceHorizontal = NO;
    self.scrollView.alwaysBounceVertical = NO;
    self.scrollView.showsHorizontalScrollIndicator = NO;
    self.scrollView.contentSize = CGSizeMake(900, 200);
    self.scrollView.backgroundColor = [UIColor yellowColor];
    [self.view addSubview:self.scrollView];
    
    static int count = 0;
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
    
    
        NSLog(@"%d", ++count);
    }];

输出结果:
请添加图片描述
在5和6秒之间我们拖动,timer就停止了。

造成这种问题的原因就是:

  • 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode
  • 当我们进行拖拽时,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加该NSTimer以及其事件,所以我们的NSTimer就不工作了
  • 当我们松开鼠标时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了

想要解决这个问题也很简单,我们直接让NSTimer在两种mode下都能工作就完了,这就用到我们之前不太清楚其用法的NSRunLoopCommonModes了:

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

当然你也可以把NSTimer分别加入到NSDefaultRunLoopModeUITrackingRunLoopMode,这两种写法是相同的,因为系统的mode是默认在_commonModes中的:

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

修改之后就会一直正常运行了。

CFRunLoopObserverRef

CFRunLoopObserverRef是观察者可以观察Runloop的各种状态,每个Observer都包含了一个回调(函数指针),当runloop的状态发生变化时,观察者就能通过回调接收到这个变化。

typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
struct __CFRunLoopObserver {
    
    
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;//监听的RunLoop
    CFIndex _rlCount;//添加该Observer的RunLoop对象个数
    CFOptionFlags _activities;		/* immutable */
    CFIndex _order;//同时间最多只能监听一个
    CFRunLoopObserverCallBack _callout;//监听的回调
    CFRunLoopObserverContext _context;//上下文用于内存管理
};
RunLoop的六种状态
//观测的时间点有一下几个
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    
    
    kCFRunLoopEntry = (1UL << 0),   //   即将进入RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理Timer
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理Source
    kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),// 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),// 即将退出RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU
};

这六种状态都可以被observer观察到,我们也可以利用这一方法写一些特殊事件,创建监听,监听RunLoop的状态变化:

// 创建observer
CFRunLoopObserverRef ob = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
    
    
    switch (activity) {
    
    
        case kCFRunLoopEntry:
            NSLog(@"kCFRunLoopEntry");
            break;
        case kCFRunLoopBeforeTimers:
            NSLog(@"kCFRunLoopBeforeTimers");
            break;
        case kCFRunLoopBeforeSources:
            NSLog(@"kCFRunLoopBeforeSources");
            break;
        case kCFRunLoopBeforeWaiting:
            NSLog(@"kCFRunLoopBeforeWaiting");
            break;
        case kCFRunLoopAfterWaiting:
            NSLog(@"kCFRunLoopAfterWaiting");
            break;
        case kCFRunLoopExit:
            NSLog(@"kCFRunLoopExit");
            break;
        default:
            break;
    }
});
// 添加observer到runloop中
CFRunLoopAddObserver(CFRunLoopGetMain(), ob, kCFRunLoopCommonModes);
CFRelease(ob);

输出结果:
在这里插入图片描述
RunLoop是在不停的监听状态并做出反应

RunLoop实现逻辑

RunLoop的内部逻辑如下:
在这里插入图片描述

__CFRunLoopRun源码实现

精简后的__CFRunLoopRun函数,保留了主要代码,看一下具体实现:

//用DefaultMode启动
void CFRunLoopRun(void) {
    
    
    CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
}

//用指定的Mode启动,允许设置RunLoop的超时时间
int CFRunLoopRunInMode(CFStringRef modeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
    
    
    return CFRunLoopRunSpecific(CFRunLoopGetCurrent(), modeName, seconds, returnAfterSourceHandled);
}

//RunLoop的实现
int CFRunLoopRunSpecific(runloop, modeName, seconds, stopAfterHandle) {
    
    
	//首先根据modeName找到对应的mode
	CFRunLoopModeRef currentMode = __CFRunLoopFindMode(runloop, modeName, false);
	//如果该mode中没有source/timer/observer,直接返回
	if (__CFRunLoopModeIsEmpty(currentMode)) return;
	
	//1.通知Observers:RunLoop即将进入loop
	__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);

	//调用函数__CFRunLoopRun 进入loop
	__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandled) {
    
    
        
        Boolean sourceHandledThisLoop = NO;
        int retVal = 0;
        do {
    
    
			//2.通知Observers:RunLoop即将触发Timer回调
			__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
			//3.通知Observers:RunLoop即将触发Source0(非port)回调
			__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
			///执行被加入的block
			__CFRunLoopDoBlocks(runloop, currentMode);

			//4.RunLoop触发Source0(非port)回调,处理Source0
			sourceHandledThisLoop = __CFRunLoopDoSources0(runloop, currentMode, stopAfterHandle);
			//执行被加入的Block
			__CFRunLoopDoBlocks(runloop, currentMode);

			//5.如果有Source1(基于port)处于ready状态,直接处理这个Source1然后跳转去处理消息
			if (__Source0DidDispatchPortLastTime) {
    
    
                Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg)
                if (hasMsg) goto handle_msg;
            }

			//6.通知Observers:RunLoop的线程即将进入休眠
			if (!sourceHandledThisLoop) {
    
    
                __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
            }

			//7.调用mach_msg等待接收mach_port的消息。线程将进入休眠,直到被下面某个事件唤醒:
			// 一个基于port的Source的事件
			// 一个Timer时间到了
			// RunLoop自身的超时时间到了
			// 被其他什么调用者手动唤醒
			__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
    
    
                mach_msg(msg, MACH_RCV_MSG, port); // thread wait for receive msg
            }

			//8.通知Observers:RunLoop的线程刚刚被唤醒
			__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

			//收到消息,处理消息
			handle_msg:
			//9.1 如果一个Timer时间到了,触发这个timer的回调
			if (msg_is_timer) {
    
    
                __CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time())
            } 
			//9.2 如果有dispatch到main_queue的block,执行block
			else if (msg_is_dispatch) {
    
    
                __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
            } 
            //9.3 如果一个Source1(基于port)发出事件了,处理这个事件
            else {
    
    
                CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
                sourceHandledThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
                if (sourceHandledThisLoop) {
    
    
                    mach_msg(reply, MACH_SEND_MSG, reply);
                }
            }
            //执行加入到loop的block
            __CFRunLoopDoBlocks(runloop, currentMode);

			//设置do-while之后的返回值
			if (sourceHandledThisLoop && stopAfterHandle) {
    
    
                // 进入loop时参数说处理完事件就返回
                retVal = kCFRunLoopRunHandledSource;
            } else if (timeout) {
    
    
                // 超出传入参数标记的超时时间了
                retVal = kCFRunLoopRunTimedOut;
            } else if (__CFRunLoopIsStopped(runloop)) {
    
    
                // 被外部调用者强制停止了
                retVal = kCFRunLoopRunStopped;
            } else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
    
    
                // source/timer/observer一个都没有了
                retVal = kCFRunLoopRunFinished;
            }

			// 如果没超时,mode里没空,loop也没被停止,那继续loop。
	 	} while (retVal == 0);
    }

	//10. 通知Observers:RunLoop即将退出
	__CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
}

实际上RunLoop就是这样的一个函数,其内部是一个do-while循环。当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里,知道超时或者被手动调用,该函数才会返回。

RunLoop回调(流程)

  • 当App启动时,系统会默认注册五个上面说过的5个mode
  • 当RunLoop进行回调时,一般都是通过一个很长的函数调出去(call out),当在代码中加断点调试时,通常能在调用栈上看到这些函数。这就是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);
}

RunLoop启动方法

  • 1.run,无条件
    • 无条件地进入运行循环是最简单的选项,但也是最不理想的选择。无条件地运行runloop将线程放入永久循环,这使您无法控制运行循环本身。停止runloop的唯一方法是杀死它。也没有办法在自定义模式下运行循环。
  • 2.runUntilDate, 设置时间限制
    • 设置了超时时间,超过这个时间runloop结束,优于第一种
  • 3.runMode:beforeDate:,在特定模式下
    • 相对比较好的方式,可以指定runloop以哪种模式运行,但是它是单次调用的,超时时间到达或者一个输入源被处理,则runLoop就会自动退出,上述两种方式都是循环调用的
    • 实际上run方法的实现就是无限调用runMode:beforeDate:方法
    • runUntilDate:也会重复调用runMode:beforeDate:方法,区别在于它超时就不会再调用

RunLoop关闭方法

  • 1.将运行循环配置为使用超时值运行。
  • 2.手动停止。

这里需要注意,虽然删除runloop的输入源和定时器可能会导致运行循环的退出,但这并不是个可靠的方法,系统可能会添加输入源到runloop中,但在我们的代码中可能并不知道这些输入源,因此无法删除它们,导致无法退出runloop。

我们可以通过上述2、3方法来启动runloop,设置超时时间。但是如果需要对这个线程和它的RunLoop有最精确的控制,而并不是依赖超时机制,这时我们可以通过 CFRunLoopStop()方法来手动结束一个 RunLoop。但是 CFRunLoopStop()方法只会结束当前正在执行的这次runMode:beforeDate:调用,而不会结束后续runloop的调用。

ImageView延迟显示

当界面中含有UITableView,而且每个UITableViewCell里边都有图片。这是当我们滚动UITableView的时候,如果有一堆的图片需要显示,那么可能出现卡顿的情况。
我们应该推迟图片的实现,也就是ImageView推迟显示图片。当我们滑动时不要加载图片, 拖动结束在显示:

[self.imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"1.png"] afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

用户点击屏幕,在主线程中,三秒之后显示图片,但是当用户点击屏幕之后,如果此时用户又开始滚动tableview,那么就算过了三秒,图片也不会显示出来,当用户停止了滚动,才会显示图片。

这是因为限定了方法setImage只能在NSDefaultRunLoopMode模式下使用。而滚动tableview的时候,程序运行在tracking模式下面,所以方法setImage不会执行。

猜你喜欢

转载自blog.csdn.net/m0_62386635/article/details/130490705