【iOS】RunLoop

目录


什么是RunLoop

字面意思来看,就是运行循环的意思,即在程序运行过程中循环做一些事情

作用:保证程序持续处于运行状态,处理App中的各种事件,节省CPU资源、提高程序性能

比如一个ViewController的View有一个Button,我现在没有点击它、也没有任何网络请求,像这种情况下,RunLoop就会让iOS程序的当前线程就进入休眠,CPU就不会花时间在这条线程(这个程序)上面,这条线程(这个程序)也就不会消耗资源(CPU不会给这个程序分配资源),达到节省CPU资源的目的;一旦点击了Button触发了事件,RunLoop就会唤醒这条线程,进入while循环里面做事情

这些技术都需要在RunLoop下才能进行:定时器(Timer)、PerformSelector、GCD Async Main Queue、事件响应、手势识别、界面刷新、网络请求、AutoreleasePool

main函数中,如果没有RunLoop,程序执行一次后就会退出;有了RunLoop,程序就会保持运行,不会退出:

objectivec 复制代码
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        // Setup code that might create autoreleased objects goes here.
        appDelegateClassName = NSStringFromClass([AppDelegate class]);
    }
    
    // iOS程序默认是有RunLoop的,因为返回了UIApplicationMain,程序会一直保持运行状态,不会退出
    return UIApplicationMain(argc, argv, nil, appDelegateClassName);
    
    // 没有RunLoop,程序一启动就会退出
//    return 0;
}

RunLoop运行逻辑(伪代码):

objectivec 复制代码
// 等效伪代码
int main(int argc, char * argv[]) {
    NSString * appDelegateClassName;
    @autoreleasepool {
        int retVal = 0;
        do {
            // 睡眠中等待消息
            int message = sleep_and_wait();
            // 处理消息
            retVal = process_message(message);
        } while (0 == retVal);
        return 0;
    }
}

获取RunLoop对象

iOS中有2套API来访问和使用RunLoop

  • Core Foundation:CFRunLoopRef

    objectivec 复制代码
    CFRunLoopRef currentRunLoopCF = CFRunLoopGetCurrent(); // 获取当前线程RunLoop对象
    CFRunLoopRef mainRunLoopCF = CFRunLoopGetMain();  // 获取主线程RunLoop对象
  • Foundation:NSRunLoop

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

    NSRunLoop是基于CFRunLoopRef的一层OC封装,真正底层的RunLoop对象还是得看CFRunLoopRef的地址

    NSArray是基于CFArrayRef的一层封装,NSString是基于CFStringRef的一层封装

RunLoop与线程

  • 每条线程都有唯一的一个与之对应的RunLoop对象
  • RunLoop保存在一个全局的Dictionary里,线程作为key,RunLoop作为value
  • 线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建
  • RunLoop会在线程结束时销毁

主线程的RunLoop会在main函数执行UIApplicationMain的时候自动创建,所以调用[NSRunLoop currentRunLoop]时已经有了RunLoop对象

子线程中,默认是没有RunLoop对象的,需要调用[NSRunLoop currentRunLoop]才会创建

源码CFRunloop.c文件中查看对应实现:

objectivec 复制代码
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    if (pthread_equal(t, kNilPthreadT)) {
		t = pthread_main_thread_np();
    }
    __CFLock(&loopsLock);
    
    // 如果没有__CFRunLoops字典,先创建一个
    if (!__CFRunLoops) {
        __CFUnlock(&loopsLock);
		CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        
    // 然后创建主线程的runloop,对应主线程的key存储到字典中
	CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
	CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
	if (!OSAtomicCompareAndSwapPtrBarrier(NULL, dict, (void * volatile *)&__CFRunLoops)) {
	    CFRelease(dict);
	}
	CFRelease(mainLoop);
        __CFLock(&loopsLock);
    }
    
    // 从字典__CFRunLoops里面获取runloop
    // key: pthreadPointer
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    __CFUnlock(&loopsLock);
    
    // 如果没有与之对应的runloop对象,就创建一个新的runloop
    if (!loop) {
		CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        __CFLock(&loopsLock);
    
    	// 再次判断是否有该runloop对象
		loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    	// 如果没有,就将新创建的runloop赋值进去
		if (!loop) {
	   	 	CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
	   	 	loop = newLoop;
		}
        // don't release run loops inside the loopsLock, because CFRunLoopDeallocate may 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);
        }
    }
    return loop;
}

RunLoop底层结构

CFRunLoopRef

上面提到NSRunLoopCFRunLoopRef的一层包装,所以探究其本质应查看CFRunLoopRef源码
CFRunLoopRef本质是一个__RunLoop结构体类型:

(删去了不必了解的代码)

RunLoop最核心的部分就是CFRunLoopModeRef,表示RunLoop的运行模式
RunLoop对象里面的所有Mode存在一个Set集合里面,看下图关系:

CFRunLoopModeRef

Mode代表RunLoop的运行模式,其作用就是将不同模式下的一些操作隔离开来

好处是:

如果应用里面有个TableView,能够滚动,RunLoop就可以办到,当TableView滚动的时候,RunLoop切换到滚动模式 ,只需要专心处理滚动模式下的事情,就不用处理默认模式下的事件,这样就能保证滚动TableView时更加流畅,当手松开停止滑动时,切回默认模式

一个RunLoop包含若干个Mode,每个Mode由包含若干个Source0/Source1/Timers/Observers,RunLoop启动时只能选择其中一个Mode作为currentMode

如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入(切换Mode程序不会退出,也是在内部做的切换),不同组的Mode是分隔开来的,互不影响

如果Mode里没有任何Source0/Source1/Timers/ObserversRunLoop会立马退出

常见Mode

  • kCFRunLoopDefaultMode(NSDefaultRunLoopMode):App的默认Mode,通常主线程是在这个Mode下运行
  • UITrackingRunLoopMode:界面跟踪Mode,用于ScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响
  • kCFRunLoopCommonModes默认包括kCFRunLoopDefaultMode、UITrackingRunLoopMode

Mode中成员的用途

  • source0:触摸事件处理、performSelector:onThread:
  • source1:基于端口Port的线程间通信、系统事件捕捉(分发到source0去处理)
  • Timers:NSTimer、performSelector:withObject:afterDelay:
  • Observers:监听RunLoop的状态(在RunLoop进入休眠之前唤醒刷新UI、在RunLoop将要进入睡眠时唤醒进行内存释放)

RunLoop运行逻辑

添加观察者Observer的方式来监听RunLoop的状态以及模式变化

通过代码监听状态

通过函数调用的方式来监听

objectivec 复制代码
// 创建Observer
// kCFRunLoopAllActivities:监听所有状态变化
CFRunLoopObserverRef observer = CFRunLoopObserverCreate(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, observeRunLoopActicities, NULL);
// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放(C语言中create创建后要对应释放观察者)
CFRelease(observer);

void observeRunLoopActicities(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info)
{
    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;
    }
}

通过block回调来监听

objectivec 复制代码
// 创建Observer
CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(kCFAllocatorDefault, kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
   switch (activity) {
       case kCFRunLoopEntry: { // 一旦有局部变量,花括号确定局部变量的作用域
           CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
           NSLog(@"kCFRunLoopEntry - %@", mode); // copy来的要release
           CFRelease(mode);
           break;
       }
           
       case kCFRunLoopBeforeTimers: {
           CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
           NSLog(@"kCFRunLoopBeforeTimers - %@", mode);
           CFRelease(mode);
           break;
       }
           
       case kCFRunLoopBeforeSources: {
           CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
           NSLog(@"kCFRunLoopBeforeSources - %@", mode);
           CFRelease(mode);
           break;
       }
           
       case kCFRunLoopBeforeWaiting: {
           CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
           NSLog(@"kCFRunLoopBeforeWaiting - %@", mode);
           CFRelease(mode);
           break;
       }
           
       case kCFRunLoopAfterWaiting: {
           CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
           NSLog(@"kCFRunLoopAfterWaiting - %@", mode);
           CFRelease(mode);
           break;
       }
           
       case kCFRunLoopExit: {
           CFRunLoopMode mode = CFRunLoopCopyCurrentMode(CFRunLoopGetCurrent());
           NSLog(@"kCFRunLoopExit - %@", mode);
           CFRelease(mode);
           break;
       }
           
       default:
           break;
   }
});

// 添加Observer到RunLoop中
CFRunLoopAddObserver(CFRunLoopGetMain(), observer, kCFRunLoopCommonModes);
// 释放
CFRelease(observer);

下面是RunLoop状态变化的枚举注释:

通过源码分析原理

核心是RunLoopRunSpecific函数,一进入此函数

  • 首先会通知Observers:进入RunLoop,并伴有当前模式currentMode

  • 接下来会执行RunLoopRun函数,也就是具体要做的事情,内部主体是一个do {...} while (!retVal) 循环

    • 首先会通知Observers:即将处理timers、即将处理Sources
    • 处理Blocks
    • 处理Source0,可能会再次处理Blocks
    • 判断有无Source1
      • 如果有,直接跳转到handle_msg(被Source1唤醒)
      • 如果无,通知Observers:即将休眠,RunLoop进入睡眠,也就是会阻塞当前进程,等待别的消息来唤醒当前线程,通知Observers:结束休眠
    • 调用handle_msg
      • 如果被timer唤醒,处理timers
      • 如果被GCD唤醒,处理GCD相关事情
      • 如果被Source1唤醒,处理Source1
    • 处理Blocks
  • 根据执行结果retVal来决定是否退出do {...} while ()循环

  • 通知Observes:退出Loop

注:RunLoop休眠时的阻塞__CFRunLoopServiceMachPort真正的阻塞,线程会进入休息,不再做任何事情,和写个死循环的阻塞是不一样的,死循环还是会执行代码,线程还是要一直工作的

让RunLoop进入睡眠的__CFRunLoopServiceMachPort函数内部会调用内核层面的api来进行真正的休眠,调用mach_msg()我们可以分为用户态内核态的两种状态切换,RunLoop进入睡眠就是从用户态切换到内核态的调用,然后有消息唤醒线程时再切换到用户态来处理事件

RunLoop会处理GCD回到主线程的调用情况(GCD Async To Main),其他时候GCD都是有自己的逻辑去处理的,所以这是一个特殊情况

RunLoop的实际应用

解决NSTimer在滑动时停止工作的问题

定时器和滚动页面同时存在的情况下,可能会遇到因为页面滚动,而定时器无法响应的问题:

objectivec 复制代码
[NSTimer scheduledTimerWithTimeInterval: 1.0 repeats: YES block:^(NSTimer * _Nonnull timer) {
   NSLog(@"%d", ++count);
}];

该方法创建的NSTimer,一旦开始滑动scrollView(或其子类),RunLoop就会只处理UITrackingRunLoopMode下的事件,NSTimer就会失效,停止打印count

据此,可得出解决方案为,让两种模式下都嫩好处理NSTimer事件:

objectivec 复制代码
    NSTimer* timer = [NSTimer timerWithTimeInterval: 1.0 repeats: YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"%d", ++count);
    }]; // 仅仅返回了一个定时器
//    CFRunLoopAddTimer(<#CFRunLoopRef rl#>, <#CFRunLoopTimerRef timer#>, <#CFRunLoopMode mode#>);
//    [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSDefaultRunLoopMode];
//    [[NSRunLoop currentRunLoop] addTimer: timer forMode: UITrackingRunLoopMode];
    [[NSRunLoop currentRunLoop] addTimer: timer forMode: NSRunLoopCommonModes];

NSRunLoopCommonModes并不是真的模式,而是一种标记,会让定时器同时能在这两种模式下都进行工作

选择通用模式会将这两种模式都添加到RunLoop底层__CFRunLoop结构体里的_commonModes成员变量里,_commonModes意味着在通用模式下可以执行的模式列表

定时器timer也会被加入到_commonModeItems列表中;没有设置为通用模式的定时器timer只存在于__CFRunLoopMode里面的成员变量timers这个列表中

能同时在commonModes里工作的东西,都会放到commonModeItems里去

RunLoop&NSTimer Demo

控制线程生命周期(线程保活)

著名的网络请求框架AFNetworking就是使用RunLoop来控制线程生命周期的,创建一个子线程,让其一直停留在内存中,需要其做事情就通知它,因为不断创建销毁线程是非常消耗性能的

细节分析

RunLoop线程保活细节分析

  1. 控制器增加一个Thread *thread的属性来在多个方法能操作同一条线程
  2. 通过block的方式来创建线程是为了避免@selector里面会对Controller进行引用,Controller又持有thread,会造成循环引用,无法释放
  3. 在子线程里创建一个RunLoop对象,如果没有任何成员变量那么创建完就会马上退出,所以添加一个空白的任务Source1,也就是[[NSPort alloc] init],让RunLoop不会退出
    添加的任务是个空的任务,RunLoop其实没有任何事情要执行,所以会进入到睡眠状态,所以会卡住线程不再往下执行代码
  4. 采用循环创建RunLoop而不使用[[NSRunLoop currentRunLoop] run]是因为run方法的底层就是死循环进行runMode: beforeDate:的调用,那么就无法停止所有的RunLoop,所以我们自己实现while循环,并通过状态值来控制什么时候可以停止创建RunLoop
  5. 添加任务和结束RunLoop都要在同一个子线程来完成
  6. waitUntilDone:表示是否执行完selector的函数调用再继续往下执行代码,如果为NO,那么执行完performSelector,控制器就真的销毁了,这时再去调用到self就会报野指针的错误;设置为YES,代码会等子线程的stopThread执行完毕才会去真正销毁控制器
  7. while循环里进行self的判断是为了防止执行了dealloc方法会将弱指针weakSelf已经置为nil了,那么循环的判断就会为true,再次进入创建RunLoop;所以要确保self有值的情况下判断条件才成立
    [NSDate distantFuture]表示的是未来无限长的时间
封装优化

RunLoop线程保活封装类

相关推荐
奶油话梅糖5 小时前
深入解析交换机端口安全:Sticky MAC的工作原理与应用实践
网络·安全·macos
我现在不喜欢coding6 小时前
为什么runloop中先处理 blocks source0 再处理timer source1?
ios·面试
骑着猪狂飙6 小时前
iOS技术之通过Charles抓包http、https数据
网络协议·http·ios·https
1024小神8 小时前
macos使用brew报错解决办法
macos
爱转呼啦圈的小兔子9 小时前
Mac中修改Word的Normal.dotm文件
macos·word
wanghao66645510 小时前
Mac测试端口连接的几种方式
macos
Digitally1 天前
如何将视频从安卓设备传输到Mac?
android·macos
心灵宝贝1 天前
Mac用户安装JDK 22完整流程(Intel版dmg文件安装指南附安装包下载)
java·开发语言·macos
Dolphin_海豚1 天前
charles proxying iphone
前端·ios
Magnetic_h1 天前
【iOS】内存管理及部分Runtime复习
笔记·学习·macos·ios·objective-c·cocoa·xcode