【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线程保活封装类

相关推荐
endingCode1 小时前
45.坑王驾到第九期:Mac安装typescript后tsc命令无效的问题
javascript·macos·typescript
dr李四维2 小时前
iOS构建版本以及Hbuilder打iOS的ipa包全流程
前端·笔记·ios·产品运营·产品经理·xcode
️ 邪神3 小时前
【Android、IOS、Flutter、鸿蒙、ReactNative 】自定义View
flutter·ios·鸿蒙·reactnative·anroid
soulteary3 小时前
突破内存限制:Mac Mini M2 服务器化实践指南
运维·服务器·redis·macos·arm·pika
小江村儿的文杰13 小时前
XCode Build时遇到 .entitlements could not be opened 的问题
ide·macos·ue4·xcode
比格丽巴格丽抱14 小时前
flutter项目苹果编译运行打包上线
flutter·ios
网络安全-老纪15 小时前
iOS应用网络安全之HTTPS
web安全·ios·https
天涯倦客的美丽人生16 小时前
2024年11月最新 Alfred 5 Powerpack (MACOS)下载
macos
SoraLuna16 小时前
「Mac玩转仓颉内测版24」基础篇4 - 浮点类型详解
开发语言·算法·macos·cangjie
总爱写点小BUG16 小时前
VM虚拟机装MAC后无法联网,如何解决?
macos