iOS ------RunLoop

一,RunLoop简介

RunLoop实际上是一个对象,这个对象在循环中用来处理程序运行过程中出现的各种事件(比如说触摸事件、UI刷新事件、定时器事件、Selector事件),从而保持程序的持续运行,RunLoop在没有事件处理的时候,会使线程进入睡眠模式,从而节省CPU资源,提高程序性能。

简单来说,runloop可以让线程在需要做事情的时候忙起来,不需要的时候让线程休眠,使程序不会结束。

二,RunLoop基本作用

1,保持程序的持续运行

程序一开启就会开一个主线程,主线程一开起来就会跑一个主线程对应的RunLoop。RunLoop保证主线程不会被销毁,也就保证了程序的持续运行

2,处理App中的各种事件

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

三,主线程的RunLoop原理

启动一个iOS程序时,系统会调用创建项目自动生成的main.m的文件

objectivec 复制代码
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
}

其中UIApplicationMain函数内部会帮我们开启主线程的RunLoop,内部拥有一个无限循环的代码,一直在do_while循环中执行,程序也就会一直保持执行状态,不会马上退出

objectivec 复制代码
int main(int argc, char * argv[]) {        
    BOOL running = YES;
    do {
        // 执行各种任务,处理各种事件
        // ......
    } while (running);  // 判断是否需要退出

    return 0;
}

RunLoop的模型图

RunLoop就是线程中的一个循环,RunLoop会在循环中不断检测,通过Input sourse(输入源)和Timer sources(定时器)两种来源更待接受事件;然后接受到事件通知线程进行处理,并在没有事件的时候让线程进行休息。

三,RunLoop对象

RunLoop实际上是一个对象,是基于CFFoundation框架的CFRunLoopRef类型分装的对象。

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

RunLoop对象的获取

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

NSRunLoop是基于CFRunLoopRef的封装,提供了面向对象的API,但是这些API不是线程安全的。

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

CFRunLoopRef类是CoreFoundation框架中Runloop的对象,并且其提供了纯C语言函数的API,所有这些API都是线程安全

看一下CoreFoundation框架中这两个函数的具体实现:

objectivec 复制代码
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这个方法。

objectivec 复制代码
//全局的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和线程的关系

  1. 每个线程都有唯一一个与之对应的RunLoop对象
  2. RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  3. 线程刚创建的时没有RunLoop对象,在第一次获取RunLoop时创建,销毁则是在线程结束的时候
  4. 主线程的RunLoop对象系统自动创建,子线程的RunLoop对象需要我们主动创建和维护

CFRunLoopRef源码部分

objectivec 复制代码
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;
};

CFRunLoopModeRef代表RunLoop的运行模式,一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer,而RunLoop启动时只能选择其中一个Mode作为currentMode

  • Source0:触摸事件,performSelectors
  • Sourse1:基于Port的线程间的通信
  • Timers:定时器,NSTimer
  • Observer;监听器,用于监听RunLoop的状态

ConmonModes

它记录了所有被标记了common的mode

  • 一个Mode可以将自己标记为Common属性,通过将其ModelName添加到RunLoop的commonModes中
  • 每当RunLoop的内容发生变化时,RunLoop都会将_commonItems里的Source/Observer/Timer同步到具有Common标记的所有Mode里。
objectivec 复制代码
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);
}

_commonModes 中存储的是所有被标记为 common 的 mode 的集合,这些 mode 具有跨 run loop 共享的特性,能够让特定的事件源、定时器和观察者在多个 run loop 中共同作用。

四,RunLoop的相关类

  • CFRunLoopRef:RunLoop对象
  • CFRunLoopModeRef:RunLoop的运行模式
  • CFRunLoopSourceRef:RunLoop运行模式的输入源/事件源
  • CFRunLoopTimerRef:RunLoop运行模式的定时源
  • CFRunLoopObserverRef:RunLoop运行模式的观察者,监听RunLoop的状态改变

一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)

  • 每次 RunLoop 启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式(CFRunLoopModeRef)被称作当前运行模式(CurrentMode)
  • 如果需要切换运行模式(CFRunLoopModeRef),只能退出当前 Loop,再重新指定一个运行模式(CFRunLoopModeRef)进入。
  • 这样做主要是为了分隔开不同组的输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef),让其互不影响 。

4.1 CFRunLoopModeRef类

objectivec 复制代码
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 */
};

系统默认注册的5个Mode:

  1. kCFRunLoopDefaultMode:App的默认Mode,通常主线程是在这个Mode下运行
  2. UITrackingRunLoopMode:界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响
  3. UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用,会切换到kCFRunLoopDefaultMode
  4. GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到
  5. kCFRunLoopCommonModes: 实际上是一个 Mode 的集合,默认包括 NSDefaultRunLoopMode 和 NSEventTrackingRunLoopMode(注意:并不是说Runloop会运行在kCFRunLoopCommonModes这种模式下,而是相当于分别注册了 NSDefaultRunLoopMode和UITrackingRunLoopMode

4.2 CFRunLoopSourceRef类

CFRunLoopSource是对input sources的抽象。CFRunLoopSource分source0source1两个版本,它的结构如下:

objectivec 复制代码
struct __CFRunLoopSource 
    CFRuntimeBase _base;
    uint32_t _bits; //用于标记Signaled状态,source0只有在被标记为Signaled状态,才会被处理
    pthread_mutex_t _lock;
    CFIndex _order;         /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;     /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1;    /* immutable, except invalidation */
    } _context;
};

source0

  • source0是App内部事件,由App自己管理的UIEvent、CFSocket都是source0。当一个source0事件准备执行的时候,必须要先把它标记为signal状态。
  • source0是非基于Port的。只包含了一个回调(函数指针),它并不能触发出发事件。
  • 使用时,你需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop

source1

  • Source1除了包含回调指针外包含一个mach port,Source1可以监听系统端口和通过内核和其他线程通信,接收、分发系统事件,它能够主动唤醒RunLoop。

4.3 CFRunLoopTimerRef

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

objectivec 复制代码
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

scheduledTimerWithTimeInterval和RunLoop的关系

objectivec 复制代码
[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];

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

objectivec 复制代码
NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

NSTimer在滑动时停止工作:

我们在写NSTimer时,如果我们滑动UIScrollView时,定时器的事件就不执行了,如果我们停止滑动时,定时器就会重新工作。

原因:

  • 当我们不做任何操作的时候,RunLoop处于NSDefaultRunLoopMode下。
  • 而当我们拖动ScrollView的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,这个模式下没有添加NSTimer,所以我们的NSTimer就不工作了。
  • 但当我们松开鼠标的时候,RunLoop就结束UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了

如何将NSTimer在两种模式下运行?

就是将Timer加入到顶层RunLoop的commonModeItems中,commonModeItems被RunLoop自动更新到所有具有Common属性的Mode中去,Common Modes并不是一个正真的模式,它只是一个标记,NSDefaultRunLoopModeUITrackingRunLoopMode被标记其中。我们可以将NSTimer添加到当前RUnLoop的kCFRunLoopCommonModes

objectivec 复制代码
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

其实现的效果和下面的相同

objectivec 复制代码
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:UITrackingRunLoopMode];

4.4 CFRunLoopObserverRef

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

objectivec 复制代码
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的六种状态

objectivec 复制代码
//观测的时间点有一下几个
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
};

RunLoop的底层原理

流程图

__CFRunLoopRun源码实现

objectivec 复制代码
/// RunLoop的实现, 大概在文件的2622行
SInt32 CFRunLoopRunSpecific(CFRunLoopRef rl, CFStringRef modeName, CFTimeInterval seconds, Boolean returnAfterSourceHandled) { {
    
    /// 首先根据modeName找到对应mode
    CFRunLoopModeRef currentMode = __CFRunLoopFindMode(rl, modeName, false);
    
    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopEntry);
    
    /// __CFRunLoopRun中具体要做的事情
    result = __CFRunLoopRun(rl, currentMode, seconds, returnAfterSourceHandled, previousMode);
    
    /// 11. 通知 Observers: RunLoop 即将退出。
    __CFRunLoopDoObservers(rl, currentMode, kCFRunLoopExit);
    
    return result;
}


// __CFRunLoopRun的实现, 进入loop, 大概在文件的2304行
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) {

    int32_t retVal = 0;
    do {

        // 2. 通知 Observers: RunLoop 即将触发 Timer 回调。
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        
        // 3. 通知 Observers: RunLoop 即将触发 Source0 (非port) 回调。
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);

        // 4. 处理block
        __CFRunLoopDoBlocks(rl, rlm);

        // 5. 处理Source0
        Boolean sourceHandledThisLoop = __CFRunLoopDoSources0(rl, rlm, stopAfterHandle);
        // 如果处理Source0的结果是rrue
        if (sourceHandledThisLoop) {
            // 再次处理block
            __CFRunLoopDoBlocks(rl, rlm);
        }

        Boolean poll = sourceHandledThisLoop || (0ULL == timeout_context->termTSR);

        // 6. 如果有Source1 (基于port) 处于ready状态,直接处理这个Source1然后跳转去处理消息。
        if (__CFRunLoopWaitForMultipleObjects(NULL, &dispatchPort, 0, 0, &livePort, NULL)) {
            // 如果有Source1, 就跳转到handle_msg
            goto handle_msg;
        }

        // 7. 通知 Observers: RunLoop 的线程即将进入休眠(sleep)
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
        __CFRunLoopSetSleeping(rl);
	
        // 7. 调用mach_msg等待接受mach_port的消息。线程将进入休眠, 等待别的消息来唤醒当前线程
        // 一个基于 port 的Source 的事件。
        // 一个 Timer 到时间了
        // RunLoop 自身的超时时间到了
        // 被其他什么调用者手动唤醒
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
        
        
        __CFRunLoopUnsetSleeping(rl);
        // 8. 通知Observers: 结束休眠, RunLoop的线程刚刚被唤醒了
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

        
    // 收到消息,处理消息。
    handle_msg:;
        if (/* 被timer唤醒 */) {
            // 01. 处理Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        } else if (/* 被gcd唤醒 */) {
            // 02. 处理gcd
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {  // 被Source1唤醒
            // 处理Source1
            sourceHandledThisLoop = __CFRunLoopDoSource1(rl, rlm, rls) || sourceHandledThisLoop;
	    }
        
        // 9. 处理Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        // 10. 设置返回值, 根据不同的结果, 处理不同操作
        if (sourceHandledThisLoop && stopAfterHandle) {
            // 进入loop时参数说处理完事件就返回。
            retVal = kCFRunLoopRunHandledSource;
        } else if (timeout_context->termTSR < mach_absolute_time()) {
            // 超出传入参数标记的超时时间了
            retVal = kCFRunLoopRunTimedOut;
        } else if (__CFRunLoopIsStopped(rl)) {
             // 被外部调用者强制停止了
            retVal = kCFRunLoopRunStopped;
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {
            // source/timer/observer一个都没有了
            retVal = kCFRunLoopRunFinished;
        }

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

    return retVal;
}

RunLoop其内部是一个do-while循环; 当你调用CFRunLoopRun()时,线程就会一直停留在这个循环里;直到超时或被手动停止,该函数才会返回

RunLoop的退出

  1. 主线程销毁时
  2. Mode中的TimerSourceObserver为空
  3. RunLoop到了设置的停止时间

Run Loop的实现原理

从用户态切换到内核态,在内核态让线程进行休眠,有消息的时候就还行线程,回到用户态处理消息

RunLoop的核心就是调用mach_msg().

RunLoop的实际应用

图片下载

由于图片渲染到屏幕需要消耗较多的资源,为了提高1用户的体验,当用户滚动TableView的时候,只在后台下载图片,但是不显示图片,当用户停下来的时候才显示出来。

objectivec 复制代码
[self.imageView performSelector:@selector(setImage:) withObject:image afterDelay:3.0 inModes:@[NSDefaultRunLoopMode]];

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

常驻线程

我们在开发应用程序的时候,如果后台操作特别频繁,经常会做一些耗时的操作(下载文件,后台播放音乐),如果频繁的创建销毁线程,非常消耗性能。我们最好能让这条线程永驻内存。添加一条长驻于内存的强引用的子线程,在该线程添加一个Sources,开启RunLoop.

objectivec 复制代码
#import "ViewController.h"

@interface ViewController ()
@property (strong, nonatomic) NSThread* thread;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    self.thread = [[NSThread alloc] initWithTarget:self selector:@selector(run1) object:nil];
    [self.thread start];
}
- (void) run1 {
    NSLog(@"----run1----");
    NSLog(@"%@", [NSThread currentThread]);
    // 添加下边两句代码,就可以开启RunLoop,之后self.thread就变成了常驻线程,可随时添加任务,并交于RunLoop处理
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    [[NSRunLoop currentRunLoop] run];
    // 测试是否开启了RunLoop,如果开启RunLoop,则来不了这里,因为RunLoop开启了循环。
    NSLog(@"为开启RunLoop");
}
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    // 利用performSelector,在self.thread的线程中调用run2方法执行任务
    [self performSelector:@selector(run2) onThread:self.thread withObject:nil waitUntilDone:NO];
}

- (void) run2
{
    NSLog(@"----run2------");
    NSLog(@"%@", [NSThread currentThread]);
    
}
@end

输出结果

objectivec 复制代码
2024-08-03 15:49:07.960805+0800 线程常驻[43968:1479175] ----run1----
2024-08-03 15:49:08.730213+0800 线程常驻[43968:1479175] ----run2------
2024-08-03 15:49:08.730499+0800 线程常驻[43968:1479175] <NSThread: 0x60000210d500>{number = 6, name = (null)}
2024-08-03 15:49:15.483094+0800 线程常驻[43968:1479175] ----run2------
2024-08-03 15:49:15.483280+0800 线程常驻[43968:1479175] <NSThread: 0x60000210d500>{number = 6, name = (null)}

我们发现RunLoop启动成功进入循环,并没有打印为开启RunLoop,点击屏幕时也是在子线程添加方法,这样就达成了线程常驻的效果。

定时器NSTimer

在实际开发中,一般不把timer放到主线程的RunLoop中,因为主线程在执行阻塞的任务时,timer计时会不准。

如何让计时准确?如果timer在主线程中阻塞了怎么办?

放入子线程中(即要开辟一个新的线程,但是成本是需要开辟一个新的线程)

写一种跟RunLoop没有关系的计时,即GCD。(不会阻塞,推荐使用这种)

objectivec 复制代码
// GCD定时器(常用)
// 创建队列
dispatch_queue_t queue = dispatch_get_global_queue(0, 0);
// 1.创建一个GCD定时器
/*
 第一个参数:表明创建的是一个定时器
 第四个参数:队列
 */
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
// 需要对timer进行强引用,保证其不会被释放掉,才会按时调用block块
// 局部变量,让指针强引用
self.timer = timer;
// 2.设置定时器的开始时间,间隔时间,精准度
/*
 第1个参数:要给哪个定时器设置
 第2个参数:开始时间
 第3个参数:间隔时间
 第4个参数:精准度 一般为0 在允许范围内增加误差可提高程序的性能
 GCD的单位是纳秒 所以要*NSEC_PER_SEC
 */
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 2.0 * NSEC_PER_SEC, 0 * NSEC_PER_SEC);

// 3.设置定时器要执行的事情
dispatch_source_set_event_handler(timer, ^{
    NSLog(@"---%@--",[NSThread currentThread]);
    // 取消定时
      if (判断条件) {
      dispatch_source_cancel(timer);
          self.timer = nil;
      }
});
// 4.启动
dispatch_resume(timer);
相关推荐
SoraLuna6 小时前
「Mac玩转仓颉内测版26」基础篇6 - 字符类型详解
开发语言·算法·macos·cangjie
符小易10 小时前
PD虚拟机启动后 Mac主机无法上网解决教程
macos
SoraLuna10 小时前
「Mac玩转仓颉内测版32」基础篇12 - Cangjie中的变量操作与类型管理
开发语言·算法·macos·cangjie
@糊糊涂涂10 小时前
MAC借助终端上传jar包到云服务器
java·服务器·macos·jar
sensen_kiss10 小时前
Quest串流Mac教程
macos·vr
TomAndersen10 小时前
Mac 环境变量配置基础教程
macos
键盘敲没电14 小时前
【iOS】知乎日报总结
学习·ios·objective-c·xcode
安和昂18 小时前
【iOS】UICollectionView的学习
学习·ios·cocoa
二流小码农21 小时前
鸿蒙开发:自定义一个任意位置弹出的Dialog
android·ios·harmonyos
木子欢儿1 天前
AirScreen 安卓平板作为MacOS副屏
macos·电脑