RunLoop介绍

311 阅读16分钟

什么是RunLoop

一般来讲,一个线程一次只能执行一个任务,任务完成后线程就会退出。如果每执行一个任务都创建一个线程会非常浪费资源,所以我们需要一个机制,让线程能随时处理事件但并不退出,通常的代码逻辑为:

function loop() {
    initialize();
    do {
        var message = get_next_message();
        process_message(message);
    } while (message != quit);
}

这种模型通常被称作 Event LoopEvent Loop 在很多系统和框架里都有实现,比如 Node.js 的事件处理,比如 Windows 程序的消息循环,再比如 OSX/iOS 里的 RunLoop,其在没有处理消息时休眠以避免CPU资源占用、在有消息到来时立刻被唤醒。

RunLoop 实际上就是一个对象,这个对象管理了其需要处理的事件和消息,并提供了一个入口函数来执行上面 Event Loop 的逻辑。线程执行了这个函数后,就会一直处于这个函数内部 “接受消息->等待->处理” 的循环中,直到这个循环结束(比如传入 quit 的消息),函数返回。

OSX/iOS 系统中,RunLoop有两个这样的对象:NSRunLoopCFRunLoopRef

  • CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,这些 API 都是线程安全的。
  • NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

CFRunLoopRef 的代码是开源的,你可以在这里下载到整个 CoreFoundation 的源码来查看。Swift 开源后,苹果又维护了一个跨平台的 CoreFoundation 版本,这个版本的源码可能和现有 iOS 系统中的实现略不一样,但更容易编译,而且已经适配了 Linux/Windows。

RunLoop 与线程的关系

iOS开发遇到两种线程对象:pthread_tNSThread,之前苹果文档说NSThread是对pthread_t的封装,但是现在那份文档已经失效了,有可能现在都是直接包装自更底层的mach threadCFRunLoop是基于pthread来管理的

苹果不允许直接创建Runloop,只提供了两个自动获取的函数,CFRunloopGetMain()CFRunLoopGetCurrent()。这两个函数内部的逻辑大概如下:

/// 全局的Dictionary,key 是 pthread_t, value 是 CFRunLoopRef
static CFMutableDictionaryRef loopsDic;
/// 访问 loopsDic 时的锁
static CFSpinLock_t loopsLock;
 
/// 获取一个 pthread 对应的 RunLoop。
CFRunLoopRef _CFRunLoopGet(pthread_t thread) {
    OSSpinLockLock(&loopsLock);
    
    if (!loopsDic) {
        // 第一次进入时,初始化全局Dic,并先为主线程创建一个 RunLoop。
        loopsDic = CFDictionaryCreateMutable();
        CFRunLoopRef mainLoop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, pthread_main_thread_np(), mainLoop);
    }
    
    /// 直接从 Dictionary 里获取。
    CFRunLoopRef loop = CFDictionaryGetValue(loopsDic, thread));
    
    if (!loop) {
        /// 取不到时,创建一个
        loop = _CFRunLoopCreate();
        CFDictionarySetValue(loopsDic, thread, loop);
        /// 注册一个回调,当线程销毁时,顺便也销毁其对应的 RunLoop。
        _CFSetTSD(..., thread, loop, __CFFinalizeRunLoop);
    }
    
    OSSpinLockUnLock(&loopsLock);
    return loop;
}
 
CFRunLoopRef CFRunLoopGetMain() {
    return _CFRunLoopGet(pthread_main_thread_np());
}
 
CFRunLoopRef CFRunLoopGetCurrent() {
    return _CFRunLoopGet(pthread_self());
}

从上面的代码可以看出:

  • 线程和 RunLoop 之间是一一对应的,其关系是保存在一个全局的 Dictionary 里,该字典以线程对象pthread_tkey,以RunLoop对象value
  • 线程刚创建时并没有RunLoop,如果不主动获取,那它一直都不会有。
  • RunLoop 的创建是发生在第一次获取时,创建后便会添加进全局字典中,即每一个线程有且仅有一个与之关联的RunLoop对象,在线程结束时将RunLoop对象销毁。
  • 只能在一个线程的内部获取其 RunLoop(主线程除外)。

苹果官方也给出了一个 RunLoop 模型图,如下:

从上图也能看出,一个线程会关联一个RunLoop对象,RunLoop对象会一直循环,直到超时或收到退出指令。在无限循环的过程中会一直处理到来的事件,右侧将事件分为了两类,一类是Input sources这部分包括基于端口的source1事件,开发者提交的各种source0事件,调用performSelector:onThread:方法事件,还有一类Timer sources,就是常用的定时器事件,这些事件在程序运行期间会不断产生之后会由RunLoop对象检测并负责处理相关事件。

关于 RunLoop 的类

在深入了解RunLoop的运行机制前,我们先了解下CoreFoundation里面关于RunLoop的5个类:

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

它们之间的关系如下:

可以看出,一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。每次调用 RunLoop的主函数时,只能指定其中一个 Mode,这个Mode被称作 CurrentMode。如果需要切换 Mode,只能退出 Loop,再重新指定一个 Mode 进入。这样做主要是为了分隔开不同组的 Source/Timer/Observer,让其互不影响。

CFRunLoopRef 类

CFRunLoopRefCore Foundation 框架下 RunLoop对象类。

在Core Foundation中获取RunLoop对象的方法:

CFRunLoopGetCurrent(); 	// 获得当前线程的 RunLoop 对象
CFRunLoopGetMain(); 	// 获得主线程的 RunLoop 对象

在Foundation下获取RunLoop对象的方法:

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

CFRunLoopMode和CFRunLoop的结构:

struct __CFRunLoopMode {
    CFStringRef _name;            // Mode Name, 例如 @"kCFRunLoopDefaultMode"
    CFMutableSetRef _sources0;    // Set
    CFMutableSetRef _sources1;    // Set
    CFMutableArrayRef _observers; // Array
    CFMutableArrayRef _timers;    // Array
    ...
};
 
struct __CFRunLoop {
    CFMutableSetRef _commonModes;     // Set
    CFMutableSetRef _commonModeItems; // Set<Source/Observer/Timer>
    CFRunLoopModeRef _currentMode;    // Current Runloop Mode
    CFMutableSetRef _modes;           // Set
    ...
};

  • _commonModes:是一个集合,里面保存的是具有“Common”的mode的名称
  • _commonModeItems:也是一个集合,里面保存的是那些需要同步添加到具有“Common”属性的Mode中的Source/Timer/Observer集合

CFRunLoopModeRef 类

系统默认注册了5个Mode:

kCFRunLoopDefaultMode: App的默认 Mode,通常主线程是在这个 Mode 下运行的。

UITrackingRunLoopMode: 界面跟踪 Mode,用于 ScrollView 追踪触摸滑动,保证界面滑动时不受其他 Mode 影响。

UIInitializationRunLoopMode: 在刚启动 App 时第进入的第一个 Mode,启动完成后就不再使用。

GSEventReceiveRunLoopMode: 接受系统事件的内部 Mode,通常用不到。

kCFRunLoopCommonModes: 这是一个占位的 Mode,没有实际作用。

值得注意的是kCFRunLoopCommonModes就是被标记为“Common”属性的Mode,可以看出这种模式不是一种真正的模式,仅仅是标识其他模式是否需要同步添加_commonModeItems中的Source/Timer/Observer

系统默认将kCFRunLoopDefaultModeUITrackingRunLoopMode添加到了_commonModes中,即标识为“Common”属性,所以当RunLoop运行在这两种模式中会自动同步添加_commonModeItems中的Source/Timer/Observer

这里看到更多的苹果内部的 Mode,但那些 Mode 在开发中就很难遇到了

CFRunLoopTimerRef 类

CFRunLoopTimerRef是定时源,是基于时间的触发器。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调,基本上就是NSTimer

通过对上面三个类的理解,我们可以解释一个关于NSTime的问题

ViewController.m添加下面的代码并运行,每两秒打印“run”,但是当我们滑动UITableView的时候,会发现定时器在滑动期间,计时停滞了。

- (void)viewDidLoad {
    [super viewDidLoad];
    // Do any additional setup after loading the view.
    // 添加一个tableView
    [self.view addSubview:self.myTableV];
    // 定义一个定时器,
    NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
    // 将定时器添加到当前RunLoop的NSDefaultRunLoopMode下
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
}

- (void)run{
    NSLog(@"***run***");
}

造成上面问题的原因: 程序刚开始运行的时候,RunLoop处于NSDefaultRunLoopMode下。当我们滑动的时候,RunLoop就结束NSDefaultRunLoopMode,切换到了UITrackingRunLoopMode模式下,而这个模式下没有添加NSTimer,所以NSTimer就不工作了。当停止滑动的时候,RunLoop就结束了UITrackingRunLoopMode模式,又切换回NSDefaultRunLoopMode模式,所以NSTimer就又开始正常工作了。

解决方案: 1、将NSTimer也添加到UITrackingRunLoopMode中,但是方式很不优雅 2、通过上面CFRunLoopRefCFRunLoopModeRef的介绍,我们知道只需将[[NSRunLoop currentRunLoop] addTimer:timer forMode: NSDefaultRunLoopMode];中的NSDefaultRunLoopMode修改为NSRunLoopCommonModes也可以解决。因为将NSTimer加入到NSRunLoopCommonModes中就是把其加入到_commonModeItems集合中,这样在滑动时就会自动同步添加NSTimerUITrackingRunLoopMode模式下,所以定时器也可以得到执行。

CFRunLoopSourceRef 类

CFRunLoopSourceRef是事件源,CFRunLoopSourceRef有两种分类方法。

按照官方文档来分类

  • Port-Based Sources(基于端口)
  • Custom Input Sources(自定义)
  • Cocoa Perform Selector Sources

按照函数调用栈来分类

  • Source0 :非基于Port,它并不能主动触发事件。使用时,你需要先调用CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
  • Source1:基于Port,被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其回调函数为 __IOHIDEventSystemClientQueueCallback()

source0 有公开的 API 可供开发者调用,source1 却只能供系统使用,而且 source1 的实现原理是基于 mach_msg 函数,通过读取某个 port 上内核消息队列上的消息来决定执行的任务。

其实上面的这两种分类方式没有区别,只不过第一种是通过官方理论来分类,第二种是在实际应用中通过调用函数来分类。

下面我们了解一下函数调用栈和Source。

ViewController中新建一个UIButton,然后在其触发事件中打断点,在xcode左侧可以看到

点击事件为:首先程序启动,调用17行的main函数,main函数调用16行UIApplicationMain函数,然后一直往上调用函数,最终调用到0行的clickMethod函数,即点击函数。

注意

我们在12行看到的是Sources0,是否意味着点击事件是属于Sources0函数的,点击事件就是在Sources0中处理的呢?

其实还是系统注册的那个 source1 接收IOHIDEvent,只是之后在回调 __IOHIDEventSystemClientQueueCallback()内触发的 Source0Source0再触发的 _UIApplicationHandleEventQueue(),所以UIButton事件看到是在 Source0内的。

当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去 (call out), 当你在你的代码中下断点调试时,通常能在调用栈上看到这些函数。下面是这几个函数的整理版本,如果你在调用栈中看到这些长函数名,在这里查找一下就能定位到具体的调用地点了:

{
    /// 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);
}

CFRunLoopObserverRef 类

CFRunLoopObserverRef是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry         = (1UL << 0), // 即将进入Loop 1
    kCFRunLoopBeforeTimers  = (1UL << 1), // 即将处理 Timer 2
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source 4
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠 32
    kCFRunLoopAfterWaiting  = (1UL << 6), // 刚从休眠中唤醒 64
    kCFRunLoopExit          = (1UL << 7), // 即将退出Loop 128
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听全部状态改变  

}; 

上面的 Source/Timer/Observer被统称为 mode item,一个 item可以被同时加入多个 mode。但一个 item 被重复加入同一个 mode 时是不会有效果的。如果一个mode 中一个 item 都没有,则 RunLoop 会直接退出,不进入循环。

RunLoop 的内部逻辑

根据苹果文档里的说明,RunLoop内部逻辑图:

RunLoop的具体顺序为:

1、通知观察者RunLoop已经启动

2、通知观察者即将要开始的Timer

3、通知观察者任何非基于端口的Source0即将触发

4、触发上步准备好的非基于端口的Source0

5、如果有Source1准备好并处于等待状态,立即启动;并进入步骤9

6、通知观察者RunLoop的线程即将进入休眠状态

7、调用 mach_msg 等待接受 mach_port 的消息,线程将进入休眠直到 被下面某一个事件唤醒:

* 一个基于 port 的Source 的事件
* 一个 Timer 到时间了
* RunLoop 自身的超时时间到了
* 被其他什么调用者手动唤醒

8、通知观察者RunLoop的线程刚刚被唤醒

9、处理唤醒时未处理的事件:

* 如果用户定义的Timer 到时间了,处理定时器事件并重启
* RunLoop。进入步骤2
* 如果输入源激活,传递相应的消息
* 如果RunLoop被显示唤醒而且时间还没超时,重启RunLoop。进入步骤2

10、通知观察者RunLoop即将退出。

RunLoop 的核心就是一个 mach_msg()RunLoop 调用这个函数去接收消息,如果没有别人发送port消息过来,内核会将线程置于等待状态。

其内部代码整理如下:

/// 用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里如果没有input source 与 timer就会返回。就算有observer也是会返回的。
    if (__CFRunLoopModeIsEmpty(currentMode)) return;
    
    /// 1. 通知 Observers: RunLoop 即将进入 loop。
    __CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
    
    /// 内部函数,进入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) 回调。
            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 的线程即将进入休眠(sleep)。
            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);
            
 
            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的应用

AutoreleasePool

程序运行后产生的两个CFRunLoopObserver

  • 一个监听RunLoop对象进入循环的事件,执行回调函数_wrapRunLoopWithAutoreleasePoolHandler,并且优先级order-2147483647即32位整数的最小值,保证了它的优先级最高。在回调内会调用_objc_autoreleasePoolPush函数来创建AutoreleasePool,由于它的优先级最高,所以能够保证自动释放池在其他回调执行前得到创建。

  • 另一个监听器监听RunLoop对象进入休眠和退出循环的事件,回调函数同样是_wrapRunLoopWithAutoreleasePoolHandler,而优先级为2147483647即32位整数的最大值,保证它的优先级最低。对于监听进入休眠状态时回调函数内首先会调用_objc_autoreleasePoolPop函数来释放AutoreleasePool然后使用_objc_autoreleasePoolPush函数重新创建一个自动释放池。优先级最低保证了释放操作是在其他所有回调执行之后发生。

main函数就是被@autoreleasepool包围着,所以在主线程中创建的任何对象都会及时被释放。

AutoreleasePool的一个小问题

通过上面的讲解,我们会发现需要进入到Runloop中才会释放旧的自动释放池然后创建新的自动释放池。但如果程序在处理一个比较耗时且占用内存较大的任务时,在没有任何事件产生的情况下是不会进入到Runloop中,那些本应该立即释放的局部变量就不会被释放,程序就会被占用过多的内存

- (void)btnClickedHandler
{
    NSArray *urls = ;
    for (NSURL *url in urls) {
       NSError *error;
       NSString *fileContents = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error];
}

通过监听Runloop状态的代码,可以发现,在for循环结束前都不会改变Runloop的状态,Runloop一直处于休眠的状态,所以for循环大量创建的局部变量不会得到释放,就会占用过多的内存,直到Runloop改变状态,此时就需要手动添加@autoreleasepool来手动创建和释放自动释放池:

- (void)btnClickedHandler
{
    NSArray *urls = ;
    for (NSURL *url in urls) {
        @autoreleasepool {
            NSError *error;
            NSString *fileContents = [NSString stringWithContentsOfURL:url                                    encoding:NSUTF8StringEncoding error:&error];
    }
}

RunLoop实现常驻内存的线程

在开发应用程序的过程中,如果后台操作特别频繁,经常会在子线程做一些耗时操作(下载文件、后台播放音乐等),我们最好能让这条线程永远常驻内存。

+ (void)entryPoint
{
    //设置当前线程名为MyThread
    [[NSThread currentThread] setName:@"MyThread"];
    //获取NSRunLoop对象,第一次获取不存在时系统会创建一个
    NSRunLoop *runloop = [NSRunLoop currentRunLoop];
    /*
    添加一个Source1事件的监听端口
    RunLoop对象会一直监听这个端口,由于这个端口不会有任何事件到来所以不会产生影响
    监听模式是默认模式,可以修改为Common
    */
    [runloop addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    //启动RunLoop
    [runloop run];
}

+ (NSThread *)longTermThread
{
    //静态变量保存常驻内存的线程对象
    static NSThread *longTermThread = nil;
    //使用GCD dispatch_once 在应用生命周期只执行一次常驻线程的创建工作
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        //创建一个线程对象,并执行entryPoint方法
        longTermThread = [[NSThread alloc] initWithTarget:self selector:@selector(entryPoint) object:nil];
        //启动线程,启动后就会执行entryPoint方法
        [longTermThread start];
    });
    return longTermThread;
} 

- (void)viewDidLoad
{
    //获取这个常驻内存的线程
    NSThread *thread =  [ViewController longTermThread];
    //在该线程上提交任务
    [self performSelector:@selector(test) onThread:thread withObject:nil waitUntilDone:NO];
}

上面的例子主要利用了一个source1事件的监听,由于ModeSource/Observer/Timer中Observer不为空,所以RunLoop不会退出循环,能够常驻内存。

参考资料

iOS多线程——你要知道的RunLoop都在这里

iOS 多线程:『RunLoop』详尽总结

runloop视频(sunny-孙源)

深入理解RunLoop(YY大神-郭曜源)

RunLoop官方文档