<iOS知识体系>RunLoop知识点及面试题

382 阅读21分钟

一.初识RunLoop

1.RunLoop的基本作用

  • 保持程序的持续运行;
    如果没有Runloop,main()函数一执行完,程序就会立刻退出。
    而我们的iOS程序能保持持续运行的原因就是在main()函数中调用了UIApplcationManin函数,这个函数内部会启动主线程的RunLoop;
  • 处理App中的各种事件(比如触摸事件,定时器事件等);
  • 节省CPU资源,提高程序性能;该做事时做事,该休息时休息。

2.RunLoop的应用范畴

  • 定时器(Timer)、PerformSelector
  • GCD:dispatch_async(dispatch_get_main_queue(),^{});
  • 事件响应、手势识别、界面刷新;
  • 网络请求;
  • AutoreleasePool;

3.RunLoop在实际开发中的应用

  • 使用端口或自定义输入源与其他线程进行通信;
  • 在子线程上使用定时器;
  • 解决NSTimer在滑动时停止工作的问题;
  • 控制线程的生命周期,实现一个常驻线程;
  • 在Cocoa应用程序中使用任何performSelector...方法;
  • 监控应用卡顿;
  • 性能优化;
  • ......

二.RunLoop的数据结构

1.CFRunLoopRef
RunLoop对象的底层就是一个CFRunLoopRef结构体,它里面存储着:

  • _pthread:RunLoop与线程是一一对应关系;
  • _commonModes:存储着NSString对象的集合(Mode的名称);
  • _commonModeItems:存储着被标记为通用模式的Source0/Source1/Timer/Observer;
  • _currentMode:RunLoop当前的运行模式;
  • _modes:存储着RunLoop所有的Mode(CFRunLoopModeRef)模式;
// CFRunLoop.h
typedef struct __CFRunLoop * CFRunLoopRef;
// CFRunLoop.c
struct __CFRunLoop {
    pthread_t _pthread;                //与线程一一对应
    CFMutableSetRef _commonModes;      //存储着NSString对象的集合(Mode的名称)
    CFMutableSetRef _commonModeItems;  //存储着被标记为通用模式的Source0/Source1/Timer/Observer
    CFRunLoopModeRef _currentMode;     //RunLoop当前的运行模式
    CFMutableSetRef _modes;            //RunLoop所有的Mode(CFRunLoopModeRef)模式
    ...
};

2.CFRunLoopModeRef

  • CFRunLoopModeRef代表RunLoop的运行模式;
  • 一个RunLoop包含若干个Mode,每个Mode又包含若干个Source0/Source1/Timer/Observer;
  • RunLoop启动时只能选择其中一个Mode,作为currentMode;
  • 如果需要切换Mode,只能退出当前Loop,再重新选择一个Mode进入,切换模式不会导致程序退出;
  • 不同Mode中的Source0/Source1/Timer/Observer能分隔开来,互不影响;
  • 如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出;
// CFRunLoop.h
typedef struct __CFRunLoopMode *CFRunLoopModeRef;
// CFRunLoop.c
struct __CFRunLoopMode {
    CFStringRef _name;             // mode 类型,如:NSDefaultRunLoopMode
    CFMutableSetRef _sources0;     // CFRunLoopSourceRef
    CFMutableSetRef _sources1;     // CFRunLoopSourceRef
    CFMutableArrayRef _observers;  // CFRunLoopObserverRef
    CFMutableArrayRef _timers;     // CFRunLoopTimerRef
    ...
};

3.RunLoop的常见模式

  • NSDefaultRunLoopMode/KCFRunLoopDefaultMode:默认模式

  • UITrackingRunLoopMode:界面追踪模式,用于UIScrollView追踪触摸滑动,保证界面滑动时不受其他Mode影响;

  • NSRunLoopCommonModes/KCFRunLoopCommonModes:该模式不是实际存在的一种模式,它只是一个特殊的标记,是同步Source0/Source1/Timer/Observer到多个Mode中的技术方案,被标记为通用的Source0/Source1/Timer/Observer都会存放到_commonModeItems集合中,会同步这些Source0/Source1/Timer/Observer到多个Mode中。

  • NSDefaultRunLoopMode和NSRunLoopCommonModes属于Foundation框架;

  • KCFRunLoopDefaultMode和KCFRunLoopCommonModes属于Core Foundation框架;

  • 前者是对后者的封装,作用相同。

    CFRunLoopModeRef这样子设计有什么好处?RunLoop为什么会有多个Mode?

    • Mode做到了屏蔽的效果,当RunLoop运行在Mode1下面的时候,是处理不了Mode2的事件的;
    • 比如NSDefaultRunLoopMode默认模式和UITrackingRunLoopMode滚动模式,滚动屏幕的时候就会切换到滚动模式,就不用去处理模式下的事件了,保证了UITableView等的滚动顺畅。

4.CFRunLoopSourceRef

  • RunLoop中有两个很重要的概念,一个是上面提到的模式,还有一个就是事件源事件源分为输入源(Input Sources)定时器源(Timer Sources)两种;
  • 输入源(Input Sources)又分为Source0Source1两种,以下__CFRunLoopSource共用体union中的version0version1就分别对应Source0Source1
// CFRunLoop.h
typedef struct __CFRunLoopSource * CFRunLoopSourceRef;
// CFRunLoop.m
struct __CFRunLoopSource {
    CFRuntimeBase _base;
    uint32_t _bits;
    pthread_mutex_t _lock;
    CFIndex _order;                       /* immutable */
    CFMutableBagRef _runLoops;
    union {
        CFRunLoopSourceContext version0;  /* immutable, except invalidation */
        CFRunLoopSourceContext1 version1; /* immutable, except invalidation */
    } _context;
};

Source0和Source1的区别

  • Source0:需要手动唤醒线程,添加Source0RunLoop并不会主动唤醒线程,需要手动唤醒

    • 触摸事件处理
    • performSelector:onThread:
  • Source1:具备唤醒线程的能力

    • 基于Port的线程间通信
    • 系统事件捕捉:系统事件捕捉是由Source1来处理,然后再交给Source0处理

5.CFRunLoopTimerRef

  • CFRunLoopTimerNSTimer是toll-free bridged的,可以相互转换;
  • performSelector:withObject:afterDelay:方法创建timer并添加到RunLoop中。
// CFRunLoop.h
typedef struct CF_BRIDGED_MUTABLE_TYPE(NSTimer) __CFRunLoopTimer * CFRunLoopTimerRef;
// CFRunLoop.c
struct __CFRunLoopTimer {
    CFRuntimeBase _base;
    uint16_t _bits;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;           // 添加该 timer 的 RunLoop
    CFMutableSetRef _rlModes;        // 所有包含该 timer 的 modeName
    CFAbsoluteTime _nextFireDate;
    CFTimeInterval _interval;        /* immutable 理想时间间隔 */    
    CFTimeInterval _tolerance;       /* mutable 时间偏差 */  
    uint64_t _fireTSR;	             /* TSR units */
    CFIndex _order;                  /* immutable */
    CFRunLoopTimerCallBack _callout; /* immutable 回调入口 */
    CFRunLoopTimerContext _context;  /* immutable, except invalidation */
};

6.CFRunLoopObserverRef

作用

  • CFRunLoopObserverRef用来监听RunLoop的6种活动状态
/* Run Loop Observer Activities */
typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),          // 即将进入 RunLoop
    kCFRunLoopBeforeTimers = (1UL << 1),   // 即将处理 Timers
    kCFRunLoopBeforeSources = (1UL << 2),  // 即将处理 Sources
    kCFRunLoopBeforeWaiting = (1UL << 5),  // 即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),   // 刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),           // 即将退出 RunLoop
    kCFRunLoopAllActivities = 0x0FFFFFFFU  // 表示以上所有状态
};
  • UI刷新(BeforeWaiting)
  • Autorelease pool (BeforeWaiting)

定义

// CFRunLoop.h
typedef struct __CFRunLoopObserver * CFRunLoopObserverRef;
// CFRunLoop.c
struct __CFRunLoopObserver {
    CFRuntimeBase _base;
    pthread_mutex_t _lock;
    CFRunLoopRef _runLoop;              // 添加该 observer 的 RunLoop
    CFIndex _rlCount;
    CFOptionFlags _activities;	        /* immutable 监听的活动状态 */
    CFIndex _order;                     /* immutable */
    CFRunLoopObserverCallBack _callout;	/* immutable 回调入口 */
    CFRunLoopObserverContext _context;	/* immutable, except invalidation */
};

CFRunLoopObserverRef中的_activities用来保存RunLoop的活动状态。当RunLoop的状态发生改变时,通过回调_callout通知所有监听这个状态的Observer

7.CFRunLoop函数实现:事件循环的实现机制

/**
 *  __CFRunLoopRun
 *
 *  @param rl              运行的 RunLoop 对象
 *  @param rlm             运行的 mode
 *  @param seconds         loop 超时时间
 *  @param stopAfterHandle true: RunLoop 处理完事件就退出  false:一直运行直到超时或者被手动终止
 *  @param previousMode    上一次运行的 mode
 *
 *  @return 返回 4 种状态
 */
static int32_t __CFRunLoopRun(CFRunLoopRef rl, CFRunLoopModeRef rlm, CFTimeInterval seconds, Boolean stopAfterHandle, CFRunLoopModeRef previousMode) 
{
    int32_t retVal = 0;
    do {
        // 通知 Observers:即将处理 Timers
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeTimers);
        // 通知 Observers:即将处理 Sources
        __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeSources);
        // 处理 Blocks
    	__CFRunLoopDoBlocks(rl, rlm);
        // 处理 Sources0
        if (__CFRunLoopDoSources0(rl, rlm, stopAfterHandle)) {
            // 处理 Blocks
            __CFRunLoopDoBlocks(rl, rlm);
	    }
        // 判断有无 Source1
        if (__CFRunLoopServiceMachPort(dispatchPort, &msg, sizeof(msg_buffer), &livePort, 0, &voucherState, NULL)) {
            // 如果有 Source1,就跳转到 handle_msg
            goto handle_msg;
        }
        // 通知 Observers:即将进入休眠
	    __CFRunLoopDoObservers(rl, rlm, kCFRunLoopBeforeWaiting);
    	__CFRunLoopSetSleeping(rl);
        // ⚠️休眠,等待消息来唤醒线程
        __CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort, poll ? 0 : TIMEOUT_INFINITY, &voucherState, &voucherCopy);
    	__CFRunLoopUnsetSleeping(rl);
        // 通知 Observers:刚从休眠中唤醒
	    __CFRunLoopDoObservers(rl, rlm, kCFRunLoopAfterWaiting);

handle_msg:
        if (被 Timer 唤醒) {
            // 处理 Timer
            __CFRunLoopDoTimers(rl, rlm, mach_absolute_time())
        } else if (被 GCD 唤醒) {
            // 处理 GCD 
            __CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(msg);
        } else {  // 被 Source1 唤醒   
            // 处理 Source1
            __CFRunLoopDoSource1(rl, rlm, rls, msg, msg->msgh_size, &reply) || sourceHandledThisLoop;  
        }

        // 处理 Blocks
        __CFRunLoopDoBlocks(rl, rlm);
        
        /* 设置返回值 */
        // 进入 loop 时参数为处理完事件就返回
        if (sourceHandledThisLoop && stopAfterHandle) {  
            retVal = kCFRunLoopRunHandledSource;
        // 超出传入参数标记的超时时间
        } else if (timeout_context->termTSR < mach_absolute_time()) {  
            retVal = kCFRunLoopRunTimedOut;
        // 被外部调用者强制停止
        } else if (__CFRunLoopIsStopped(rl)) {  
            __CFRunLoopUnsetStopped(rl);
            retVal = kCFRunLoopRunStopped;
        // 自动停止
        } else if (rlm->_stopped) {  
            rlm->_stopped = false;
            retVal = kCFRunLoopRunStopped;
        // mode 中没有任何的 Source0/Source1/Timer/Observer
        } else if (__CFRunLoopModeIsEmpty(rl, rlm, previousMode)) {  
            retVal = kCFRunLoopRunFinished;
        }
    
    } while (0 == retVal);

    return retVal;
}

从该函数实现中可以得知RunLoop主要就做以下几件事情:

  • _CFRunLoopDoObservers:通知Observers接下来要做什么
  • _CFRunLoopDoBlocks:处理Blocks
  • _CFRunLoopDoSources0:处理Source0
  • _CFRunLoopDoSources1:处理Source1
  • _CFRunLoopDoTimers:处理Timers
  • 处理GCD相关:dispatch_async(dispatch_get_main_queue(),^{});
  • _CFRunLoopSetSleeping/_CFRunLoopUnsetSleeping:休眠等待/结束休眠
  • _CFRunLoopServiceMachProt->mach-msg():转移当前线程的控制权

8.CFRunLoopServiceMarchPort函数实现:RunLoop休眠的实现原理

_CFRunLoopRun函数中,会调用_CFRunLoopServiceMachPort函数,该函数中调用了mach_msg()函数来转移当前线程的控制权给内核态/用户态。

  • 没有消息需要处理时,休眠线程以避免资源占用,调用mach_msg()从用户态切换成内核态,等待消息;
  • 有消息需要处理时,立刻唤醒线程,调用mach_msg()回到用户态处理消息。

这就是RunLoop休眠的实现原理,也是RunLoop与简单的do...while循环区别:

  • RunLoop:休眠的时候,当前线程不会做任何事,CPU不会再分配资源;
  • 简单的do...while循环:当前线程并没有休息,一直占用CPU资源;

9.RunLoop与线程的关系

  • RunLoop对象和线程是一一对应关系;
  • RunLoop保存在一个全局的Dictionary里,线程作为keyRunLoop作为value
  • 如果没有RunLoop,线程执行完任务就会退出;如果没有RunLoop,主线程执行完main()函数就退出,程序就不能处于运行状态;
  • RunLoop创建时机:线程刚创建时并没有RunLoop对象,RunLoop会在第一次获取它时创建;
  • RunLoop销毁时机:RunLoop会在线程结束时销毁;
  • 主线程的RunLoop已经自动获取(创建),子线程默认没有开启RunLoop
  • 主线程的RunLoop对象是在UIApplcationMain中通过[NSRunLoop currentRunLoop]获取,一旦发现它不存在,就会创建RunLoop对象。

实现一个常驻线程

  • 好处:经常用到子线程的时候,不用一直创建销毁,提高性能;

  • 条件:该任务需是串行的,而非并发;

  • 步骤:

    • 获取/创建当前线程的RunLoop
    • 向该RunLoop中添加一个Source/Port等来维持RunLoop的事件循环(如果Mode里没有任何Source0/Source1/Timer/Observer,RunLoop会立马退出);
    • 示例代码及测试输出如下:
   // ViewController.m
#import "ViewController.h"
#import "HTThread.h"

@interface ViewController ()
@property (nonatomic, strong) HTThread *thread;
@property (nonatomic, assign, getter=isStoped) BOOL stopped;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    __weak typeof(self) weakSelf = self;
    
    self.stopped = NO;
    self.thread = [[HTThread alloc] initWithBlock:^{
        NSLog(@"begin-----%@", [NSThread currentThread]);
        
        // ① 获取/创建当前线程的 RunLoop 
        // ② 向该 RunLoop 中添加一个 Source/Port 等来维持 RunLoop 的事件循环
        [[NSRunLoop currentRunLoop] addPort:[[NSPort alloc] init] forMode:NSDefaultRunLoopMode];
    
        while (weakSelf && !weakSelf.isStoped) {
            // ③ 启动该 RunLoop
            /* 
              [[NSRunLoop currentRunLoop] run]
              如果调用 RunLoop 的 run 方法,则会开启一个永不销毁的线程
              因为 run 方法会通过反复调用 runMode:beforeDate: 方法,以运行在 NSDefaultRunLoopMode 模式下
              换句话说,该方法有效地开启了一个无限的循环,处理来自 RunLoop 的输入源 Sources 和 Timers 的数据
            */ 
            [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate distantFuture]];
        }
        NSLog(@"end-----%@", [NSThread currentThread]);    
    }];
    [self.thread start];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event
{
    if (!self.thread) return;
    [self performSelector:@selector(test) onThread:self.thread withObject:nil waitUntilDone:NO];
}

// 子线程需要执行的任务
- (void)test
{
    NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
}

// 停止子线程的 RunLoop
- (void)stopThread
{
    // 设置标记为 YES
    self.stopped = YES;   
    // 停止 RunLoop
    CFRunLoopStop(CFRunLoopGetCurrent());    
    NSLog(@"%s-----%@", __func__, [NSThread currentThread]);
    // 清空线程
    self.thread = nil;
}

- (void)dealloc
{
    NSLog(@"%s", __func__);
    
    if (!self.thread) return;
    // 在子线程调用(waitUntilDone设置为YES,代表子线程的代码执行完毕后,当前方法才会继续往下执行)
    [self performSelector:@selector(stopThread) onThread:self.thread withObject:nil waitUntilDone:YES];
}

@end


// HTThread.h
#import <Foundation/Foundation.h>
@interface HTThread : NSThread
@end

// HTThread.m
#import "HTThread.h"
@implementation HTThread
- (void)dealloc
{
    NSLog(@"%s", __func__);
}
@end

10.RunLoop与NSTimer

  • NSTimer是由RunLoop来管理的,NSTimer其实就是CFRunLoopTimerRef,他们之间是toll-free bridge的,可以相互转换;
  • 如果我们在子线程上使用NSTimer,就必须开启子线程的NSTimer,否则定时器无法生效。

解决tableview滑动时NSTimer失效的问题

  • 问题:RunLoop同一时间只能运行在一种模式下,当我们滑动tableview/scrollview的时候RunLoop会切换到UITrackingRunLoopMode界面追踪模式下。如果我们的NSTimer是添加到RunLoopKCFRunLoopDefaultMode/NSDefaultRunLoopMode默认模式下的话,此时是会失效的。
  • 解决:我们可以将NSTimer添加到RunLoopKCFRunLoopCommonModes/NSRunLoopCommonModes通用模式下,来保证无论在默认模式还是界面追踪模式下NSTimer都可以执行。
  • NSTimer的创建方式。

如果我们是通过以下方式创建的NSTimer,是自动添加到RunLoop的默认模式下

    [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"123");
    }];

我们可以通过以下方式创建NSTimer,来自定义添加到RunLoop的某种模式下

    NSTimer *timer = [NSTimer timerWithTimeInterval:1.0 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"123");
    }];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSRunLoopCommonModes];

注意: 如果是通过timerxxx开头方法创建的NSTimer是不会自动添加到RunLoop中的,所以一定要记得手动添加,否则NSTimer不生效。

11.CFRunLoopAddTimer函数实现

CFRunLoopAddTimer()函数中会判断传入的modeName模式名称是不是kCFRunLoopCommonModes通用模式,是的话就会将timer添加到RunLoop的_commonModeItems 集合中,并同步该timer到的_commonModeItems里的所有模式中,这样无论在默认模式还是界面追踪模式下NSTimer都可以执行。

12.NSTimer和CADisplayLink存在的问题

  • 不准时:
    NSTimerCADisplayLink底层都是基于RunLopCFRunLoopTimerRef的实现,也就是说他们都依赖与RunLop。如果RunLop的任务过于繁重,会导致它们不准时。比如NSTimer每1.0秒就会执行一次任务,RunLoop每进行一次循环,就会看一下NSTimer的时间是否达到1.0秒,是的话就执行任务。但是由于RunLoop每一次循环的任务不一样,所花费的时间就不固定。假设第一次循环所花时间为 0.2s,第二次 0.3s,第三次 0.3s,则再过 0.2s 就会执行NSTimer的任务,这时候可能RunLoop的任务过于繁重,第四次花了0.5s,那加起来时间就是1.3s,导致NSTimer不准时。
  • 解决方法:
    使用GCD的定时器。GCD的定时器是直接跟系统内核挂钩的,而且它不依赖于RunLop,所以它非常的准时。示例如下:
    dispatch_queue_t queue = dispatch_queue_create("myqueue", DISPATCH_QUEUE_SERIAL);
    
    //创建定时器
    dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, queue);
    //设置时间(start:几s后开始执行; interval:时间间隔)
    uint64_t start = 2.0;    //2s后开始执行
    uint64_t interval = 1.0; //每隔1s执行
    dispatch_source_set_timer(timer, dispatch_time(DISPATCH_TIME_NOW, start * NSEC_PER_SEC), interval * NSEC_PER_SEC, 0);
    //设置回调
    dispatch_source_set_event_handler(timer, ^{
       NSLog(@"%@",[NSThread currentThread]);
    });
    //启动定时器
    dispatch_resume(timer);
    NSLog(@"%@",[NSThread currentThread]);
    
    self.timer = timer;
/*
2020-02-01 21:34:23.036474+0800 多线程[7309:1327653] <NSThread: 0x600001a5cfc0>{number = 1, name = main}
2020-02-01 21:34:25.036832+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:26.036977+0800 多线程[7309:1327705] <NSThread: 0x600001acb600>{number = 7, name = (null)}
2020-02-01 21:34:27.036609+0800 多线程[7309:1327707] <NSThread: 0x600001a1e5c0>{number = 4, name = (null)}
 */

面试题

1.为什么NSTimer有时候不好使?

因为创建的NSTimer默认是被接入到了NSDefaultMode,所以当RunLoop的Mode变化时,当前的NSTimer就不会工作了。

2.PerformSelector的实现原理?

  • 当调用NSObject的performSelecter:afterDelay:后,实际上其内部会创建一个Timer并添加到当前线程的RunLoop中,所以如果当前线程没有RunLoop,则这个方法会失效。
  • 当调用performSelector:onThread:时,实际上其会创建一个Timer加到对应的线程中去,同样的,如果对应线程没有RunLoop该方法也会失效。

3.PerformSlector:afterDelay:这个方法在子线程中是否起作用?为什么?怎么解决?

  • 不起作用,子线程默认没有RunLoop,也没有Timer;
  • 解决办法是可以使用GCD来实现:Dispatch_after

4.AFNetworking中如何运用RunLoop?

AFURLConnectionOperation这个类是基于NSURLConnection构建的,其希望能在后台线程接收Delegate回调,为此AFNetworking单独创建了一个线程,并在这个线程中启动了一个RunLoop:

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [[NSThread currentThread] setName:@"AFNetworking"];
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

+ (NSThread *)networkRequestThread {
    static NSThread *_networkRequestThread = nil;
    static dispatch_once_t oncePredicate;
    dispatch_once(&oncePredicate, ^{
        _networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
        [_networkRequestThread start];
    });
    return _networkRequestThread;
}

RunLoop启动前内部必须要有至少一个Timer/Observer/Source,所以AFNetworking[runloop run]之前先创建了一个新的NSMachPort添加进去了。通常情况下,调用者需要持有这个NSMachPort(mach_port)并在外部线程通过这个port发送消息到loop内;但此处添加port只是为了让RunLoop不至于退出,并没有用于实际的发送消息。

- (void)start {
    [self.lock lock];
    if ([self isCancelled]) {
        [self performSelector:@selector(cancelConnection) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    } else if ([self isReady]) {
        self.state = AFOperationExecutingState;
        [self performSelector:@selector(operationDidStart) onThread:[[self class] networkRequestThread] withObject:nil waitUntilDone:NO modes:[self.runLoopModes allObjects]];
    }
    [self.lock unlock];
}

当需要这个后台执行任务时,AFNetworking通过调用[NSObject performSelector:onThread:..]将这个任务扔到了后台线程的RunLoop

5.AutoReleasePool在何时被释放?

App启动后,苹果在主线程RunLoop里面注册了两个Observer,其回调都是_wrapRunLoopWithAutoreleasePoolHandler()

  • 第一个Observer监视的事件是Entry(即将进入RunLoop),其回调内会调用_objc_autoreleasePoolPush()创建自动释放池。

  • 第二个Observer监视了两个事件:

    • BeforeWaiting(准备进入休眠)时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush()释放旧的池并创建新池;
    • Exit(即将退出Loop)时调用_objc_autoreleasePoolPop()来释放自动释放池。

6.RunLoop的Mode

总共有5种CFRunLoopMode:

  • kCFRunLoopDefaultMode:默认模式,主线程是在这个运行模式下运行;
  • UITrackingRunLoopMode:跟踪用户交互事件(用于UIScrollView/UITableView追踪触摸滑动,保证界面滑动时不受其他Mode影响);
  • UIInitalizationRunLoopMode:在刚启动App时进入的第一个Mode,启动完成后就不再使用;
  • GSEventReceiveRunLoopMode:接受系统内部事件,通常用不到;
  • kCFRunLoopCommonModes:伪模式,不是一种真正的运行模式,是同步Source/Timer/Observer到多个Mode中的一种解决方案;

7.RunLoop的实现机制

对于RunLoop而言最核心的事情就是保证线程在没有消息的时候休眠,在没有消息时唤醒,以提高程序性能。RunLoop这个机制是依靠系统内核来完成的。
RunLoop通过mach_msg()函数接收、发送消息。它的本质是调用函数mach_msg_trap(),相当于是一个系统调用,会触发内核状态切换。在用户调用mach_msg_trap()时会切换到内核态;内核态中内核实现的mach_msg()函数会完成实际的工作。即基于Portsource1,监听端口,端口有消息就会触发回调;而source0,要手动标记为待处理和手动唤醒RunLoop
大致逻辑如下:

  • 1.通知观察者RunLoop即将启动;

  • 2.通知观察者即将要处理Timer事件;

  • 3.通知观察者即将要处理Source0事件;

  • 4.处理Source0事件;

  • 5.如果基于端口的源(Source1)准备好并处于等待状态,进入步骤9;

  • 6.通知观察者线程即将进入休眠状态;

  • 7.将线程置于休眠状态,由用户态切换到内核态,直到下面的任一事件发生才唤醒线程

    • 一个基于portSource1的事件;
    • 一个Timer到时间了;
    • RunLoop自身的超时时间到了;
    • 被其他调用者手动唤醒;
  • 8.通知观察者线程将被唤醒;

  • 9.处理唤醒时收到的事件

    • 如果用户定义的定时器启动,处理定时器事件并重启 RunLoop。进入步骤 2。
    • 如果输入源启动,传递相应的消息。
    • 如果 RunLoop 被显示唤醒而且时间还没超时,重启 RunLoop。进入步骤 2
  • 10.通知观察者 RunLoop 结束。

8.RunLoop 和线程

  • 线程和 RunLoop 是一一对应的,其映射关系是保存在一个全局的 Dictionary 里
  • 自己创建的线程默认是没有开启 RunLoop 的

8.1 怎么创建一个常驻线程

  • 为当前线程开启一个 RunLoop(第一次调用 [NSRunLoop currentRunLoop]方法时实际是会先去创建一个 RunLoop)
  • 向当前 中添加一个 Port/Source 等维持 RunLoop 的事件循环(如果 RunLoop 的 mode 中一个 item 都没有, 会退出)
  • 启动该RunLoop
@autoreleasepool {
        NSRunLoop * runLoop = [NSRunLoop   currentRunLoop] ;
        [[NSRunLoop  currentRunLoop] addPort:[NSMachPort  port] forMode:NSDefaultRunLoopMode];
        [runLoop  run] ;
    }

8.2怎样保证子线程数据回来更新 UI 的时候不打断用户的滑动操作?

当我们在子请求数据的同时滑动浏览当前页面,如果数据请求成功要切回主线程更新 UI,那么就会影响当 前正在滑动的体验。

我们就可以将更新 UI 事件放在主线程的 上执行即可,这样就会等用户不再滑动页 面,主线程 RunLoop 由 切换到 时再去更新 UI

9.RunLoop的数据结构

NSRunLoop(Foundation)CFRunLoop(CoreFoundation)的封装,提供了面向对象的API
RunLoop相关的主要涉及五个类:

  • CFRunLoop:RunLoop对象
  • CFRunLoopMode:运行模式
  • CFRunLoopSource:输入源/事件源
  • CFRunLoopTimer:定时源
  • CFRunLoopObserver:观察者
9.1 CFRunLoop

pthread(线程对象,说明RunLoop和线程是一一对应的)、
currentMode(当前所处的运行模式)、
modes(多个运行模式的集合)、
commonModes(模式名称字符串集合)、
commonModelItems(ObserverTimerSource集合)构成

9.2 CFRunLoopMode

namesource0source1currentModetimers构成

9.3 CFRunLoopSource

分为source0source1两种

  • source0:即非基于port的,也就是用户触发的事件,需要手动唤醒线程,将当前线程从内核态切换到用户态;
  • source1:基于port的,包含一个mach_port和一个回调,可监听系统端口和通过内核和其他线程发送的消息,能主动唤醒RunLoop,接收分发系统事件,具备唤醒线程的能力。
9.4 CFRunLoopTimer

基于时间的触发器,基本上说的就是NSTimer。在预设的时间点唤醒RunLoop执行回调。因为它是基于RunLoop的,因此它不是实时的(就是NSTimer是不准确的。因为RunLoop只负责分发源的消息。如果线程当前正在处理繁重的任务,就有可能导致Timer本次延迟,或者少执行一次)。

9.5 CFRunLoopObserver

监听以下时间点:CFRunLoopAtivity

  • kCFRunLoopEntry:RunLoop准备启动
  • kCFRunLoopBeforeTimers:RunLoop将要处理一些Timer相关事件
  • kCFRunLoopBeforeSource:RunLoop将要处理一些Source事件
  • kCFRunLoopBeforeWaiting:RunLoop将要进行休眠状态,即将由用户态转换成内核态
  • kCFRunLoopAfterWaiting:RunLoop被唤醒,即从内核态切换成用户态后
  • kCFRunLoopExit:RunLoop退出
  • kCFRunLoopAllActivities:监听所有状态
9.6 各数据结构之间的联系
  • 线程和RunLoop一一对应
  • RunLoopMode是一对多的
  • ModeObserverTimerSource也是一对多的

10.RunLoop概念

RunLoop是通过内部维护的事件循环Event Loop来对事件/消息进行管理的一个对象。

  • 没有消息处理时,休眠已避免资源占用,由用户态切换成内核态(CPU-内核态和用户态)
  • 有消息需要处理时,立刻被唤醒,由内核态切换到用户态

为什么main函数不会退出?

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,并执行了一段无限循环的代码(不是简单的for循环或while循环)
UIApplicationMain函数一直没有返回,而是不断地接收处理消息以及等待休眠,所以运行程序之后会保持持续运行状态。

11.RunLoop与NSTimer

滑动UITableView时,定时器还会生效吗?
默认情况下RunLoop运行在kCFRunLoopDefaultMode下,而当滑动tableView时,RunLoop切换到UITrackingRunLoopMode,而Timer是在kCFRunLoopDefaultMode下的,就无法接受处理Timer的事件。怎么去解决这个问题呢?把Timer添加到UITrackingRunLoopMode上并不能解决问题,因为这样在默认情况下就无法接受定时器事件了。

所以我们需要把Timer同时添加到kCFRunLoopDefaultModeUITrackingRunLoopMode上。那么如何把Timer同时添加到多个mode上呢?就要用到NSRunLoopCommonModes了,Timer就被添加到多个mode上,这样即使RunLoopkCFRunLoopDefaultMode切换到UITrackingRunLoopMode下,也不会影响接收Timer事件。

12.解释一下NSTimer

NSTimer其实就是CFRunLoopTimerRef,他们之间是toll-free bridge的。一个NSTimer注册到RunLoop后,RunLoop会为其重复的时间点注册好事件。RunLoop为了节省资源,并不会在非常准确的时间点回调这个TimerTimer有个属性叫做Tolerance(宽容度),标示了当时间点到后,容许有多少最大误差。

CADisplayLink是一个和屏幕刷新率一致的定时器,如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去,造成界面卡顿的感觉,在快速滑动TableView时,即使一帧也会让用户有所察觉。

13.解释一下事件响应的过程?

苹果注册了一个Source1(基于mach port的)用来接收系统事件,其回调函数为_IOHIDEventSystemClientQueueCallback()

当一个硬件事件(触摸/锁屏/摇晃等)发生后,首先由IOKit.framework生成一个IOHIDEvent事件并由SpringBoard接收。SpringBoard只接收按键(锁屏/静音等),触摸,加速,接近传感器等几种Event,随后用mach port转发给需要的App进程。随后苹果注册的Source1就会触发回调,并调用_UIApplicationHandleEventQueue()会把IOHIDEvent处理并包装成UIEvent进行处理或分发,其中包含识别UIGesture/处理屏幕旋转/发送给UIWindow等。通常事件比如UIButton点击、touchesBegin/Move/End/Cancel事件都是在这个回调中完成的。

14.解释一下手势识别的过程?

_UIApplicationHandleEventQueue()识别了一个手势时,其首先会调用Cancel将当前的code>touchesBegin/Move/End系列回调打断。随后系统将对应的UIGestureRecognizer标记为待处理。

苹果注册了一个Observer监测BeforeWaiting(RunLoop即将进入休眠)事件,这个Observer的回调函数是_UIGestureRecognizerUpdateObserver(),其内部会获取所有刚被标记为待处理的GestureRecognizer,并执行GestureRecognizer的回调。

当有UIGestureRecognizer的变化(创建/销毁/状态改变)时,这个回调都会进行相应处理。

15.利用RunLoop解释一下页面的渲染的过程?

当我们调用[UIView setNeedsDisplay]时,这时会调用当前view.layer[view.layer setNeedsDisplay]方法。

这等于给当前的layer打上了一个脏标记,而此时并没有直接进行绘制工作。而是会到当前的RunLoop即将休眠,也就是beforeWaiting时才会进行绘制工作。

紧接着会调用[CALayer display],进入到真正绘制的工作。CALayer层会判断自己的delegate有没有实现异步绘制的代理方法displayer:,这个带俩方法是异步绘制的入口,如果没有实现这个方法,那么会继续进行系统绘制的流程,然后绘制结束。

CALayer内部会创建一个Backing Store,用来获取图形上下文,接下来会判断这个layer是否有delegate

如果有的话,会调用[layer.delegate drawLayer:inContext:],并且会返回给我们[UIView drawRect:]的回调,让我们在系统绘制的基础之上再做一些事情。

如果没有delegate,那么会调用[CALayer drawInContext:]

以上两条分支,最终CALayer都会将位图提交到Backing Store,最后提交给GPU。至此绘制的过程结束。