RunLoop的学习总结

Posted SSIrreplaceable

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了RunLoop的学习总结相关的知识,希望对你有一定的参考价值。

一. RunLoop相关概念

1. 什么是RunLoop

RunLoop与线程相关且是基础框架的一部分。一个RunLoop就代表一个事件处理循环,它可以不停的调度工作以及处理输入事件。使用RunLoop的目的是有效的控制线程的执行和休眠,让线程在有工作的时候忙于工作,而在没工作的时候处于休眠状态。如果不使用RunLoop类似的循环机制,线程执行完当前任务队列中的任务就结束了,程序不能持续运行。也可以把RunLoop理解成一个高级的死循环,这个死循环可以让程序持续运行,且可以时刻监听和处理各种事件。

每一个线程都有唯一对应的RunLoop,主线程的RunLoop是默认开启的;子线程的RunLoop要显示开启且至少添加一个事件源source。我们不需要显示的创建RunLoop,因为RunLoop是懒加载的,在 Cocoa 和 Core Fundation 中都提供了关于RunLoop对象的API来帮助配置和管理线程对应的RunLoop。

2. RunLoop的简单剖析

下图是RunLoop与Source的联系图,图中的左边方框代表一个线程,线程的开始Start到结束End之间有一个RunLoop,当这个RunLoop一直存在的时候,线程就不会销毁。方框中黄色的圈圈代表一个RunLoop,圈圈左边代表当有事件源时,RunLoop就会被唤醒(线程也随之被唤醒),RunLoop会先检测定时事件源,再检测关于performSelector:onThread:的事件源,再检测自定义事件源,最后检测基于端口的事件源。圈圈右边代表当没有事件源时,RunLoop和线程都处于睡眠状态。当然RunLoop也可以通过runUntilDate:方法设定过期时间来退出,当时间到的时候,RunLoop退出,线程也随之销毁。

图中的右边代表事件源的类型,它们分别是:基于端口的事件源、自定义事件源、关于performSelector:onThread:的事件源、定时事件源,前三种又统称为输入事件源。只有当RunLoop存在时,才能保证这些事件源能被处理;如果RunLoop不存在,当前线程运行到End时,线程就会被销毁了,之后如果再有事件源尝试在这个线程中处理事件,系统就会崩溃报错。

RunLoop与Source的联系示意图:

提示:

  1. 输入事件源传递异步事件,通常消息来自于其它线程或程序;定时事件源传递同步事件,事件发生在特定时间或者重复的时间间隔。

  2. RunLoop会在处理事件之前发出通知,但要监听这些通知,必须注册一个观察者observer添加到RunLoop中才可监听。

3. RunLoop的运行模式

一个RunLoop要能运行,必须要有一个运行模式。RunLoop的一个运行模式是所有要监听的输入源、定时源、观察者的集合。当要运行一个RunLoop时,必须指定(无论显示还是隐式指定)一个运行模式。在RunLoop的运行过程中,只有和模式相关的输入源和定时源才会被处理,只有和模式相关的观察者才会被激活。和其它运行模式相关的输入源、定时源、观察者,只有在其相关的模式下才能被运行,否则处于暂停状态。通过指定RunLoop的运行模式可以使得RunLoop在某一阶段过滤来源于源的事件。大多数时候,RunLoop都是运行在系统定义的默认模式上。

CFRunLoopModeRef对象代表RunLoop的一个运行模式,一个Runloop对象可以有多个运行模式,但至少有一个运行模式,每个运行模式内又包含若干个Source/Timer/Observer,运行模式内的Source/Timer/Observer可以没有,但是如果没有,RunLoop对象运行时就直接退出了。当要切换运行模式时,必须先停止当前的运行模式,才能启动新的运行模式,这样做主要是为了分隔开不同组的Source/Timer/Observer,让其互不影响。

系统默认注册的5个运行模式:

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

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

UIInitializationRunLoopMode: 刚启动App时进入的第一个Mode,启动完成后就不再使用,开发中一般不用

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

提示:
在Core Foundation的底层有一个mutable set类型的集合Common Modes,集合中保存着NSDefaultRunLoopMode和UITrackingRunLoopMode,NSRunLoopCommonModes是用来标记它们的,只要使用了NSRunLoopCommonModes,就相当于同时使用NSDefaultRunLoopMode和UITrackingRunLoopMode

4. Source的分类

  • 按照官方文档:

    a. 基于端口的输入事件源
    b. 自定义输入事件源
    c. Cocoa关于performSelector的事件源
    d. 定时事件源

  • 按照函数调用栈:

    a. Source0:非基于Port的源
    b. Source1:基于Port的源,通过内核和其他线程通信,能接收和分发系统的事件
    c. 定时事件源

5. 输入事件源

输入源异步的发送消息给你的线程。事件来源可以分为两种:基于端口的输入源和自定义输入源。基于端口的输入源监听程序相应的端口。自定义输入源则监听自定义的事件源。RunLoop不关心输入源的是基于端口还是自定义的。两类输入源的区别在于是谁发送: 基于端口的输入源由内核自动发送,而自定义的输入源则需要人工从其他线程发送。创建好了事件源,还需要把事件源分配给RunLoop的一个或多个运行模式,当RunLoop运行在被添加到的运行模式时,事件源才会被监听到。

  • 基于端口的输入事件源
    Cocoa和Core Foundation支持与端口相关的对象和函数,用它们来创建的基于端口的源。在Cocoa中不需要直接创建输入源,只要简单地创建端口对象,并使用NSPort的方法把该端口添加到RunLoop中,端口对象会自己创建和配置输入源,基于端口的事件源处理完后不会自动从RunLoop中移除。在Core Foundation中,必须人工创建端口和它的事件源,可以使用端口相关的函数(CFMachPortRef,CFMessagePortRef,CFSocketRef) 来创建相关对象。
 void createPortSource() 

    // 创建消息端口
    CFMessagePortRef messagePort = CFMessagePortCreateLocal(kCFAllocatorDefault, CFSTR("com.someport"),myCallbackFunc, NULL, NULL);

    // 创建自定义基于端口的源
    CFRunLoopSourceRef sourcePort =  CFMessagePortCreateRunLoopSource(kCFAllocatorDefault, messagePort, 0);

    // 把自定义基于端口的源加入RunLoop
    CFRunLoopAddSource(CFRunLoopGetCurrent(), sourcePort, kCFRunLoopCommonModes);

    // 运行RunLoop
    CFRunLoopRun();

    // RunLoop退出后移除自定义基于端口的源
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

    // 撤销引用
    CFRelease(source);

  • 自定义输入事件源
    要创建自定义输入源,只能使用Core Foundation里面与CFRunLoopSourceRef类型相关的函数来创建。可以使用回调函数来配置自定义输入源,当Core Fundation配置源时,会在多处地方调用回调函数,处理输入事件,当事件源被移除的时要清理它。除了定义自定义输入源的行为,还要定义消息传递机制;事件源的消息传递在线程里面实现,并负责在数据等待处理的时候传递数据给事件源并通知它处理数据;消息传递机制的定义逻辑取是随意的,但最好不要过于复杂。
 void createCustomSource() 

    // 定义自定义输入源的上下文
    CFRunLoopSourceContext context = 0, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL;

    // 创建自定义输入源
    CFRunLoopSourceRef source = CFRunLoopSourceCreate(kCFAllocatorDefault, 0, &context);

    // 把自定义输入源加入RunLoop
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

    // 运行RunLoop
    CFRunLoopRun();

    // RunLoop退出后移除自定义输入源
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

    // 撤销引用
    CFRelease(source);

  • Cocoa关于performSelector的事件源
    在Cocoa中,可以使用关于performSelector的方法自定义事件源,一个selector执行完后会自动从RunLoop里面移除。关于performSelector:onThread:…的方法,目标线程要有一个活动的RunLoop,否则系统可能崩溃,因为目标线程可能已经被销毁。当目标线程是非主线程时,要显示开启RunLoop,才能保证程序正常执行。关于performSelector:…afterDelay:…的方法,也需要有一个活动的RunLoop,因为它会自动创建一个NSTimer对象加入当前的RunLoop,如果在子线程中使用,需要手动启动RunLoop,事件才会被处理。
// 在主线程中执行selector
performSelectorOnMainThread:withObject:waitUntilDone:
performSelectorOnMainThread:withObject:waitUntilDone:modes:

// 在指定线程中执行selector
// 目标线程必须要有一个活动的RunLoop,否则可能崩溃,因为目标线程可能已经被销毁
performSelector:onThread:withObject:waitUntilDone:
performSelector:onThread:withObject:waitUntilDone:modes:

// 在当前线程执行selector,可设置延迟时间
// 会自动创建一个NSTimer对象加入当前的RunLoop,如果在子线程中使用,需要手动启动RunLoop,事件才会被处理
performSelector:withObject:afterDelay:
performSelector:withObject:afterDelay:inModes:

// 在当前线程执行selector
performSelector:
performSelector:withObject:
performSelector:withObject:withObject:

// 撤销消息
cancelPreviousPerformRequestsWithTarget:
cancelPreviousPerformRequestsWithTarget:selector:object:

6. 定时事件源

定时事件源在预设的时间点以同步方式传递消息。定时器可以通知线程在某一时间处理某件事,但定时器并不是实时机制。定时器可以配置成仅工作一次或重复工作,当它是仅工作一次时,处理完事件后定时器就会被自动移除出RunLoop;当是重复工作时,会一直存在于RunLoop中。

如果定时器所在的运行模式不是当前RunLoop的运行模式,那么定时器将不会开始,只有RunLoop的运行模式是定时器所在的运行模式定时器才会被启动。类似的,如果定时器在运行期间,RunLoop的运行模式被切换了,定时器不在被切换的运行模式,定时器就会暂停。如果定时器在切换模式时,被延迟以至于它错过了一个或多个触发时间,那么被错过的触发事件会被忽略,定时器会在下一个最近的触发时间重新启动,后面的触发事件会正常的按照时间间隔执行。

7. RunLoop的观察者

事件源是在同步或异步事件发生时触发的,而RunLoop的观察者则是在RunLoop本身运行的特定情况下触发的。RunLoop的观察者是在事件即将被触发之前收到通知的,所以可以使用RunLoop的观察者来监听某一事件即将被触发,且可以在这些事件被触发前做一些准备工作。RunLoop的观察者可以仅用一次或循环使用,若仅用一次,那么在它被触发后,会把它自己从RunLoop里面移除,而循环使用的观察者则不会。要往RunLoop中添加观察者,只能使用Core Fundation的相关函数创建和添加观察者。

  • 观察者可以监听的状态为:
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) 

    kCFRunLoopEntry = (1UL << 0), // 即将进入 RunLoop,枚举成员对应的整数为1
    kCFRunLoopBeforeTimers = (1UL << 1), // 即将处理 Timer,枚举成员对应的整数为2
    kCFRunLoopBeforeSources = (1UL << 2), // 即将处理 Source,枚举成员对应的整数为4
    kCFRunLoopBeforeWaiting = (1UL << 5), // 即将进入休眠,枚举成员对应的整数为32
    kCFRunLoopAfterWaiting = (1UL << 6), // 刚从睡眠中唤醒,枚举成员对应的整数为64
    kCFRunLoopExit = (1UL << 7), // 即将退出 RunLoop,枚举成员对应的整数为128
    kCFRunLoopAllActivities = 0x0FFFFFFFU // 监听所有状态
;
  • 一般使用下面两个函数添加观察者,函数原型为:
    // allocator: 用于分配observer的内存空间
    // activities: 用以设置observer要监听的状态
    // repeats: 用于设置是否只监听一次
    // order: 用于设置observer的优先级,一般为0
    // block: 用于设置observer的回调代码
    // observer: 当前的observer对象
    // activity: 当前的状态
    CFRunLoopObserverRef CFRunLoopObserverCreateWithHandler(CFAllocatorRef allocator, CFOptionFlags activities, Boolean repeats, CFIndex order, void (^block) (CFRunLoopObserverRef observer, CFRunLoopActivity activity));

    // rl: observer要加入的RunLoop
    // observer: 要加入的观察者observer
    // mode: 要加入的运行模式
    void CFRunLoopAddObserver(CFRunLoopRef rl, CFRunLoopObserverRef observer, CFStringRef mode);
  • 添加观察者举例:
    // 创建观察者observer
    CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) 
        // 回调,即将要切换到activity时调用
         NSLog(@"----监听到RunLoop状态发生改变---%zd", activity);
     );

    // 添加观察者,监听RunLoop在kCFRunLoopDefaultMode下的状态
    CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

    // 释放Observer
    CFRelease(observer);

提示:

在使用Core Fundation中的函数时,凡是带有Create、Copy、Retain等字眼的函数,创建出来的对象,都需要在最后做一次release。

release的方法:CFRelease(对象);

8. RunLoop的事件队列处理逻辑

每次运行RunLoop时,线程的RunLoop会自动处理之前未处理的消息,并通知相关的观察者。具体的顺序如下:

  1. 判断当前的运行模式是否为空,为空直接退出RunLoop
  2. 通知观察者RunLoop已经启动
  3. 通知观察者任何即将要开始的定时器
  4. 通知观察者任何即将要启动的非基于端口的源
  5. 启动任何准备好的非基于端口的源
  6. 如果基于端口的源准备好并处于等待状态,立即启动。并进入步骤9
  7. 通知观察者线程进入休眠
  8. 将线程置于休眠直到有下面的任一事件发生:
    a. 某一事件到达基于端口的源
    b. 定时器启动
    c. RunLoop设置的退出时间已到
    d. RunLoop被显式唤醒
  9. 通知观察者线程将被唤醒
  10. 处理未处理的事件
    a. 如果用户定义的定时器启动,处理定时器事件并重启RunLoop。进入步骤 3
    b. 如果输入源启动,传递相应的消息
    c. 如果RunLoop被显式唤醒而且退出时间未到,重启RunLoop。进入步骤 3
  11. 通知观察者RunLoop结束

上面RunLoop的事件队列处理逻辑,可以通过分析Apple开源的Core Fundation代码中去理解,可以参考我的博客【关于RunLoop部分源码的注释】,也可以【在线浏览Core Fundation开源代码】和【下载Core Fundation开源代码】,还可以查看官方文档【Threading Programming Guide】里面的RunLoop部分的内容。

补充:因为输入源和定时器的观察者是在相应的事件发生之前传递消息的,所以通知的时间和实际事件发生的时间之间可能存在误差。在运行RunLoop时,定时器和其它周期性事件经常需要被传递,当撤销RunLoop时,也会终止消息传递。

二. RunLoop的使用场景

仅当在子线程中才需要显式启动RunLoop,主线程是默认开启的。对于子线程,RunLoop的启动不是必须的,仅当在需要的时候才去配置并启动它。不需要在任何情况下都去配置和启动一个线程的RunLoop,因为当此线程在处理完所以任务时,它不能被自动释放,这就会占用一定的内存空间。比如,当使用子线程来处理一个预先定义的需要长时间运行的任务时,没必要启动RunLoop,因为这条线程所处理的任务会在特定的时间内会结束,线程运行期间不会自动销毁,只有在任务结束时才会被销毁,所以不需要用到RunLoop,用RunLoop的目的是在线程还需要处理任务时保证它不被销毁。

RunLoop是在要和线程有更多的交互时才需要,比如以下情况:

  • 使用端口或自定义输入源来和其他线程通信

  • 使用线程的定时器

  • 在Cocoa中使用以performSelector开头的部分方法(上文已经解释)

  • 使线程周期性工作,即让线程常驻内存,等待其他线程发来消息,处理其他事件

  • 在特定的运行模式下执行特殊任务(如:图片延迟加载,默认模式不加载,滚动时才去加载图片)

三. RunLoop对象

ios中有2套API来访问和使用RunLoop,分别是Foundation和Core Foundation,它们都提供了配置输入源、定时器和RunLoop的观察者以及启动RunLoop的接口,NSRunLoop是基于CFRunLoopRef的一层OC包装。RunLoop在Cocoa中被称为NSRunLoop类的一个实例,而在Carbon或BSD程序中则是一个指向CFRunLoopRef类型的指针。每个线程都有唯一的与之关联的RunLoop对象。RunLoop对象不用人工显示创建,只要是第一次使用的时候系统会自动创建一个RunLoop对象,并会把它保存在一个字典中,当再次使用的时候直接从字典中取,这种机制也称为懒加载。可以通过官方开放源码进行分析。

1. 获取RunLoop对象

获取RunLoop对象有下面两种方式:

  • 在Cocoa程序中,使用NSRunLoop的currentRunLoop类方法来获取/访问NSRunLoop对象
// 获得当前线程的RunLoop对象
NSRunLoop *runloop = [NSRunLoop currentRunLoop];
// 获得主线程的RunLoop对象
NSRunLoop *runloopMain = [NSRunLoop mainRunLoop];
  • 使用Core Foundation的函数获取/访问RunLoop对象
// 获得当前线程的RunLoop对象
CFRunLoopRef runloop = CFRunLoopGetCurrent(); 
// 获得主线程的RunLoop对象
CFRunLoopRef runloopMain = CFRunLoopGetMain();

虽然上面两种创建方式不一样,但是它们都指向同一个RunLoop,这两种获取方式可以在需要的时候混合使用。NSRunLoop类定义了一个getCFRunLoop方法,该方法可以返回一个CFRunLoopRef类型的RunLoop。

CFRunLoopRef runloop = [[NSRunLoop currentRunLoop] getCFRunLoop];

2. 配置RunLoop

在子线程中,要启动RunLoop,至少要在运行模式mode中添加一个输入源或定时器,因为RunLoop启动时先检查运行模式是否为空,如果为空就直接退出RunLoop。上文已经介绍过观察者的创建和添加,下面程序将用另外一个函数创建观察者,其中SJMThread类是继承自NSThread类,重写main方法,在RunLoop中添加定时器和观察者。

#import "SJMThread.h"

@implementation SJMThread

/** 观察者的回调函数 */
void observerCallBack(CFRunLoopObserverRef observer, CFRunLoopActivity activity, void *info) 
    NSLog(@"%ld", activity);


/** 线程的入口 */
- (void)main 

    // 1. 获取RunLoop
    NSRunLoop *myRunLoop = [NSRunLoop currentRunLoop];


    // 2. 创建观察者
    // 2.1 初始化一个观察者的上下文
    CFRunLoopObserverContext context = 0, (__bridge void *)(self), NULL, NULL, NULL;
    // 2.2 设置观察者的回调函数
    CFRunLoopObserverCallBack myRunLoopObserver = observerCallBack;
    // 2.3 创建观察者
    CFRunLoopObserverRef observer = CFRunLoopObserverCreate(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, myRunLoopObserver, &context);
    // 2.4 如果观察者创建成功就添加到RunLoop
    if (observer) 
        // 转换RunLoop的类型
        CFRunLoopRef cfLoop = [myRunLoop getCFRunLoop];
        CFRunLoopAddObserver(cfLoop, observer, kCFRunLoopDefaultMode);
    


    // 3. 创建一个定时器,scheduledTimerWithTimeInterval会把定时器自动添加到当前的RunLoop
    [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(doFireTimer:) userInfo:nil repeats:YES];


    // 4. 连续启动10次RunLoop,是为了配合定时器的打印
    // 4.1 循环次数
    NSInteger loopCount = 10;
    do 
        // 4.2 启动RunLoop,1秒后退出
        [myRunLoop runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
        loopCount --;
     while (loopCount);



/** 定时器的定时调用方法 */
- (void)doFireTimer:(NSTimer *)timer 

    NSLog(@"-----");


@end

3. 启动RunLoop

启动RunLoop只对程序的子线程才有意义,因为主线程是自动启动的不需要显示启动。一个RunLoop的运行模式mode至少要添加一个输入源或定时器,因为RunLoop启动时先检查运行模式是否为空,如果为空就直接退出RunLoop。需要注意的是,只添加观察者不添加输入源和定时器,RunLoop是启动不了的。有几种方式可以启动RunLoop,包括以下这些:

  • 无条件的启动RunLoop

虽然无条件的启动RunLoop的方式很简单,但是RunLoop就不容易被控制,可能会使线程常驻内存。

// 方式1
[[NSRunLoop currentRunLoop] run];
// 方式2
CFRunLoopRun();
  • 通过设置RunLoop的退出时间来启动RunLoop
// 默认是运行在NSDefaultRunLoopMode下,10秒后退出
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:10]];
  • 使用特定的运行模式启动RunLoop
// 在NSDefaultRunLoopMode下运行,10秒后退出
[[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:10]];
// 在kCFRunLoopDefaultMode下运行,10秒后退出,这个函数可以返回结束的原因result
CFRunLoopRunResult result = CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, YES);

// RunLoop的结束类型 
typedef CF_ENUM(SInt32, CFRunLoopRunResult) 
    kCFRunLoopRunFinished = 1, // 所有的Sources都被移除时退出
    kCFRunLoopRunStopped = 2, // 手动调用CFRunLoopStop()退出 
    kCFRunLoopRunTimedOut = 3, // 时间超时退出 
    kCFRunLoopRunHandledSource = 4 // 与returnAfterSourceHandled配合使用,事件被分发处理后退出
;

4. 退出RunLoop

RunLoop退出后,不意味着RunLoop马上被销毁,默认情况是在子线程被销毁的时候同时被销毁。退出RunLoop有两种方式:

  • 给RunLoop设置超时时间
- (void)runUntilDate:(NSDate *)limitDate;
- (BOOL)runMode:(NSString *)mode beforeDate:(NSDate *)limitDate;
CFRunLoopRunResult CFRunLoopRunInMode(CFStringRef mode, CFTimeInterval seconds, Boolean returnAfterSourceHandled);
  • 通知RunLoop停止
void CFRunLoopStop(CFRunLoopRef rl);

通过移除RunLoop里面的所有输入源和定时器的方式退出RunLoop这种退出方式不靠谱,因为程序在运行过程中系统会添加一些输入源到RunLoop中,导致输入源和定时器的数量不确定,在移除输入源和定时器的时候,可能达不到预期效果。

5. 线程安全和RunLoop对象

线程是否安全取决于使用那些API来操作RunLoop。Core Foundation中的函数通常是线程安全的,可以被任意线程调用。但是如果修改了RunLoop的配置,然后需要执行某些操作,任何时候你最好还是在RunLoop所属的线程执行这些操 作。Cocoa的NSRunLoop类则不像Core Foundation具有与生俱来的线程安全性。如果想使用NSRunLoop类来操作RunLoop,也应该在RunLoop所属的线程里面完成这些操作。给属于不同线程的RunLoop添加输入源和定时器有可能导致你的代码崩溃或产生不可预知的行为。

6. RunLoop对象和自动释放池

在RunLoop中,系统通过Observer监听RunLoop的状态,一旦监听到RunLoop即将进入睡眠等待状态(kCFRunLoopBeforeWaiting),就释放旧的自动释放池,并在RunLoop被唤醒之前创建新的自动释放池。

当我们在创建一个工程的时候,在main函数中会看到一个自动释放池,如

int main(int argc, char * argv[]) 
    @autoreleasepool 
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    

在上面代码中,在主线程结束时,会给所有对象release一次。因为子线程是异步的,不能够使用主线程的自动释放池,所有当我们创建一个子线程时,应该在子线程的任务中创建一个自动释放池,本博文为了简单起见,很多地方都没有创建自动释放池,创建自动释放池的方式如下:

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];

    [self performSelectorInBackground:@selector(test) withObject:nil];


- (void)test 
    // 自动释放池,加在最外面!!!
    @autoreleasepool 

        NSTimer *timer = [NSTimer timerWithTimeInterval:3.0 target:self selector:@selector(timerRun:) userInfo:nil repeats:NO];

        [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];

        [[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:4.0]];
      


- (void)timerRun:(NSTimer *)timer 
    NSLog(@"%@", timer);


@end

四. RunLoop在开发中的使用

1. 线程常驻内存

#import "ViewController.h"

@interface ViewController ()

@property (nonatomic, strong) NSThread *thread;

@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];

    _thread = [[NSThread alloc] initWithTarget:self selector:@selector(threadRun) object:nil];
    [_thread start];


- (void)threadRun 

    NSLog(@"%s",__func__);

    // 让线程常驻内存,只要有一个RunLoop在活动即可
    // 像RunLoop添加输入源
    [[NSRunLoop currentRunLoop] addPort:[NSPort port] forMode:NSDefaultRunLoopMode];
    // 启动RunLoop
    [[NSRunLoop currentRunLoop] run];

    /** 补充
     * 与[[NSRunLoop currentRunLoop] run];等价的代码有以下两种:
     * [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
     * [[NSRunLoop currentRunLoop] runUntilDate:[NSDate distantFuture]];
     */


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event 
    [self performSelector:@selector(performRun) onThread:_thread withObject:nil waitUntilDone:YES];


- (void)performRun 
    NSLog(@"---");


@end

有些开发者会使用死循环while (1) 驱动RunLoop,让线程常驻内存,使用那种方法也可实现常驻内存。但是线程在收到输入源之前会一直处于活动状态,因为只有RunLoop对象才能让线程进入睡眠状态。在下列代码中,在收到输入源之前,RunLoop会一直在启动和退出中循环交替进行,只有RunLoop收到输入源后,且处理完输入源后才让线程处于睡眠状态。RunLoop在交替执行启动和退出过程中,RunLoop不会被销毁,也不会多次创建,因为runloop是懒加载的。反观上面的代码,随便添加一个输入源,当RunLoop运行后,输入源快速处理后,就会让线程进入睡眠状态。

- (void)threadRun1 

    NSLog(@"%s",__func__);

    // 让线程常驻内存,只要有一个RunLoop在活动即可
    while (1) 
        // 启动RunLoop
        [[NSRunLoop currentRunLoop] run];
    

2. 图片延迟加载

#import "ViewController.h"

@interface ViewController ()

@property (weak, nonatomic) IBOutlet UIImageView *imageView;

@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];

    // 注意执行者不要写self,必须是_imageView,否则会提示找不到setImage:方法
    [_imageView performSelector:@selector(setImage:) withObject:[UIImage imageNamed:@"runloop"] afterDelay:0.0 inModes:@[UITrackingRunLoopMode]];


@end

3. 定时器使用

#import "ViewController.h"

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad 
    [super viewDidLoad];

    [self performSelectorInBackground:@selector(test2) withObject:nil];


// 添加定时器方式1
- (void)test1 

    // scheduledTimerWithTimeInterval方法中创建的timer会制动加入到当前的RunLoop的NSDefaultRunLoopMode中,如果是在子线程,需要手动启动RuunLoop
//    [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
//    NSDefaultRunLoopMode

    // 使用scheduledTimerWithTimeInterval方法时,修改运行模式要重新添加到RunLoop中,并设置要修改的运行模式
    NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    // 启动RuunLoop
    [[NSRunLoop currentRunLoop] run];



// 添加定时器方式2
- (void)test2 

    // 创建定时器
    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(runTimer) userInfo:nil repeats:YES];

    // 添加到RuunLoop
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

    // 启动RuunLoop
    [[NSRunLoop currentRunLoop] run];



- (void)runTimer 
    NSLog(@"---");


@end

以上是关于RunLoop的学习总结的主要内容,如果未能解决你的问题,请参考以下文章

RunLoop的学习总结

RunLoop的学习总结

RunLoop 总结:RunLoop的应用场景

RunLoop总结

RunLoop总结:RunLoop基础知识

RunLoop总结:RunLoop的应用场景