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);
相关推荐
叽哥11 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户091 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan1 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
小溪彼岸2 天前
macOS自带截图命令ScreenCapture
macos
法的空间2 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918412 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
TESmart碲视2 天前
Mac 真正多显示器支持:TESmart USB-C KVM(搭载 DisplayLink 技术)如何实现
macos·计算机外设·电脑
00后程序员张2 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview