RunLoop

325 阅读12分钟

本文内容大部分是从这篇文章来的,有兴趣可以去看原文

  • 参考文章# 深入理解RunLoop RunLoop是iOS和OSX中的一个基本概念,是一种时间循环机制(Event Loop),在iOS中许多功能的实现背后都有RunLoop的支撑,例如自动释放池、触摸事件、屏幕刷新、延迟回调、进程/线程间通讯、网络请求等等其本质是RunLoop在处理各种信号。基于RunLoop我们也可以实现屏幕卡顿监控这种类似功能,总之就是很强大啦,如果我们从应用层面去分析某一个功能或许很容易陷入细节的纠缠中去,但如果我们直接掘其祖坟,从RunLoop来分析,那么就很容易掌握其本质,达到事半功倍的效果。

OSX/iOS 系统中,提供了两个这样的对象:NSRunLoop 和 CFRunLoopRef。

CFRunLoopRef 是在 CoreFoundation 框架内的,它提供了纯 C 函数的 API,所有这些 API 都是线程安全的。

NSRunLoop 是基于 CFRunLoopRef 的封装,提供了面向对象的 API,但是这些 API 不是线程安全的。

CFRunLoopRef 的代码是开源

本文源码基于CF-1153.18

RunLoop介绍

如果我们执行一个简单脚本文件,那么他很可能是线性的,从上到下一跐溜就执行完了,但是我们的App却不能这样一跐溜就执行完,我们得让他一直活着等着接收交互。那么很容易想到使用while进行实现

while(1){
    //搬砖。。。
}

如果一直全负荷运转这当然也可以满足我们的需求,只不过有些浪费罢了,RunLoop其实就是在while的基础上做了一些优化让他有事就干,没事睡觉,再有事儿了就再去叫醒它干活,劳逸结合节省处理器性能。

官方文档中对RunLoop的介绍

Run loops are part of the fundamental infrastructure associated with threads. A run loop is an event processing loop that you use to schedule work and coordinate the receipt of incoming events. The purpose of a run loop is to keep your thread busy when there is work to do and put your thread to sleep when there is none.

RunLoop是与线程息息相关的基本基础结构的一部分。RunLoop是一个调度任务和处理任务的事件循环。RunLoop的目的是为了在有工作的时让线程忙起来,而在没工作时让线程进入睡眠状态。

看一下RunLoop是怎么运行的

void CFRunLoopRun(void) { /* DOES CALLOUT */
    int32_t result;
    do {
        result = CFRunLoopRunSpecific(CFRunLoopGetCurrent(), kCFRunLoopDefaultMode, 1.0e10, false);
        ...
    } while (kCFRunLoopRunStopped != result && kCFRunLoopRunFinished != result);
}

可以看到RunLoop确实是以do-while方式运行的,根据一些状态进行控制。

RunLoop和线程的关系

// 获取主线程RunLoop
CFRunLoopRef CFRunLoopGetMain(void) {
    ...
    static CFRunLoopRef __main = NULL; // no retain needed
    if (!__main) __main = _CFRunLoopGet0(pthread_main_thread_np()); // no CAS needed
    return __main;
}

// 获取当前线程RunLoop
CFRunLoopRef CFRunLoopGetCurrent(void) {
    ...
    return _CFRunLoopGet0(pthread_self());
}
  • pthread_main_thread_np()获取主线程
  • pthread_self()获取当前线程
// should only be called by Foundation
// t==0 is a synonym for "main thread" that always works
CF_EXPORT CFRunLoopRef _CFRunLoopGet0(pthread_t t) {
    //线程为空时默认获取主线程
    if (pthread_equal(t, kNilPthreadT)) {
        t = pthread_main_thread_np();
    }
    
    // 如果没有创建过线程,那么创建一个可变字典,
    // 字典中存储以线程为key以RunLoop为value的值
    // 默认创建一个主线程的RunLoop和主线程进行绑定
    if (!__CFRunLoops) {
        CFMutableDictionaryRef dict = CFDictionaryCreateMutable(kCFAllocatorSystemDefault, 0, NULL, &kCFTypeDictionaryValueCallBacks);
        CFRunLoopRef mainLoop = __CFRunLoopCreate(pthread_main_thread_np());
        CFDictionarySetValue(dict, pthreadPointer(pthread_main_thread_np()), mainLoop);
        ...
    }
        
    // 先从字典获取一下RunLoop
    CFRunLoopRef loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
    // 如果获取不到说明是第一次获取,创建一个RunLoop存储起来
    if (!loop) {
        CFRunLoopRef newLoop = __CFRunLoopCreate(t);
        loop = (CFRunLoopRef)CFDictionaryGetValue(__CFRunLoops, pthreadPointer(t));
        if (!loop) {
            CFDictionarySetValue(__CFRunLoops, pthreadPointer(t), newLoop);
            loop = newLoop;
        }
        ...
    }
    return loop;
}
  • 从这里我们知道有一个全局可变字典,里面以线程为key,以RunLoop为value进行关联。
  • 子线程的RunLoop默认不存在,需要主动获取并主动让其运行起来

子线程的RunLoop需要手动启动这一点很容易引起问题,我们通过几个小问题来了解一下

问题一

请问代码输出结果是什么??

- (void)performSelector{
    NSLog(@"performSelector");
}

- (void)performSelectorAfterDelay{
    NSLog(@"threadFunAfterDelay");
}

-(void)thread{
    NSLog(@"1");

    [self performSelector:@selector(performSelector) onThread:[NSThread currentThread] withObject:nil waitUntilDone:false];

    [self performSelector:@selector(performSelectorAfterDelay) withObject:nil afterDelay:0];

    [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"scheduledTimerWithTimeInterval");
    }];
    
    [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timerWithTimeInterval");
    }];
    
    NSLog(@"2");    
}

//创建线程并执行
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(thread) object:nil];
[thread start];

分别分析这几个方法

This method queues the message on the run loop of the target thread using the default run loop modes

这个方法是向RunLoop的消息队列发送消息,在default mode运行 也就是需要RunLoop启动之后才能运行

This method sets up a timer to perform the aSelector message on the current thread’s run loop. The timer is configured to run in the default mode (NSDefaultRunLoopMode). When the timer fires, the thread attempts to dequeue the message from the run loop and perform the selector. It succeeds if the run loop is running and in the default mode; otherwise, the timer waits until the run loop is in the default mode.

这个方法会创建一个计时器,时间到了以后向runloop的default mode任务队列中发送一条消息,如果RunLoop没有运行那么就不会成功

这里还是需要RunLoop跑起来才可以

  • scheduledTimerWithTimeInterval:repeats:block:
// Creates and returns a new NSTimer object initialized with 
// the specified block object and schedules it on the 
// current run loop in the default mode.
// 在runloop的default mode下执行
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

备注写了,需要在RunLoop的default mode下执行

  • timerWithTimeInterval:repeats:block:
// Creates and returns a new NSTimer object initialized with the 
// specified block object. This timer needs to be scheduled on
// a run loop (via -[NSRunLoop addTimer:]) before it will fire.
// 这里就没有发送消息到Runloop中去,需要我们手动添加
+ (NSTimer *)timerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

这个方法就没有发送消息到Runloop中去,需要我们手动添加到RunLoop中,然后启动RunLoop

所以执行结果为:

1
2

通过前文分析我们知道现在子线程的RunLoop还没有启动,所以基于RunLoop的功能都不会执行,题目做如下修改

- (void)performSelector{
    NSLog(@"performSelector");
}

- (void)performSelectorAfterDelay{
    NSLog(@"threadFunAfterDelay");
}

-(void)thread{
    // 如果我们先启动RunLoop是不是就可以了呢
    [[NSRunLoop currentRunLoop] run];
    NSLog(@"1");

    [self performSelector:@selector(performSelector) onThread:[NSThread currentThread] withObject:nil waitUntilDone:false];

    [self performSelector:@selector(performSelectorAfterDelay) withObject:nil afterDelay:0];

    [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"scheduledTimerWithTimeInterval");
    }];

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timerWithTimeInterval");
    }];

    NSLog(@"2");
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
}

//创建线程并执行
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(thread) object:nil];
[thread start];

我们在子线程中先启动RunLoop,然后执行方法是不是就可以了呢??? 执行结果

1
2

仍然没有执行,这是为什么呢????

我们上面介绍RunLoop的时候说了,RunLoop有休眠的机制,没事干的时候就歇着去了,所以我们先启动RunLoop它发现没事可干又去歇着了,所以下边的事儿还是没有干,我们将RunLoop的启动放在下面就可以了

- (void)performSelector{
    NSLog(@"performSelector");
}

- (void)performSelectorAfterDelay{
    NSLog(@"threadFunAfterDelay");
}

-(void)thread{
    NSLog(@"1");

    [self performSelector:@selector(performSelector) onThread:[NSThread currentThread] withObject:nil waitUntilDone:false];

    [self performSelector:@selector(performSelectorAfterDelay) withObject:nil afterDelay:0];

    [NSTimer scheduledTimerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"scheduledTimerWithTimeInterval");
    }];

    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:NO block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timerWithTimeInterval");
    }];

    NSLog(@"2");
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    
    [[NSRunLoop currentRunLoop] run];
    //在下面添加方法仍然是不执行了,因为RunLoop又去睡觉了
}

//创建线程并执行
NSThread *thread = [[NSThread alloc] initWithTarget:self selector:@selector(thread) object:nil];
[thread start];

执行结果

1
2
performSelector
threadFunAfterDelay
scheduledTimerWithTimeInterval
timerWithTimeInterval

都执行了,这里注意,执行完以后RunLoop又去歇着了,所以如果在下面再添加方法,仍然是不会执行的。

这里需要注意两个点:

  • 1、子线程的RunLoop需要手动启动
  • 2、RunLoop没事干就休眠了,所以启动RunLoop要在添加事件之后

RunLoop结构

RunLoop实际上是一个对象,这个对象管理了要处理的事情和监听事件等。

/* All CF "instances" start with this structure.  Never refer to
 * these fields directly -- they are for CF's use and may be added
 * to or removed or change format without warning.  Binary
 * compatibility for uses of this struct is not guaranteed from
 * release to release.
 * 所有CF对象都是已这玩意开头的
 */
typedef struct __CFRuntimeBase {
    uintptr_t _cfisa;
    ...
} CFRuntimeBase;


// 这是一个对象,这里主要是对modes的处理
struct __CFRunLoop {
    CFRuntimeBase _base;
    ...
    pthread_t _pthread;
    uint32_t _winthread;
    CFMutableSetRef _commonModes;
    CFMutableSetRef _commonModeItems;
    CFRunLoopModeRef _currentMode;
    CFMutableSetRef _modes;
    ...
};

我们看到CFRunLoop结构体中主要是对Modes的处理,看一下Mode是个啥

struct __CFRunLoopMode {
    CFRuntimeBase _base;
    pthread_mutex_t _lock; /* must have the run loop locked before locking this */
    CFStringRef _name;
    Boolean _stopped;
    char _padding[3];
    CFMutableSetRef _sources0;
    CFMutableSetRef _sources1;
    CFMutableArrayRef _observers;
    CFMutableArrayRef _timers;
    CFMutableDictionaryRef _portToV1SourceMap;
    __CFPortSet _portSet;
    CFIndex _observerMask;
    ...
#if USE_MK_TIMER_TOO
    mach_port_t _timerPort;
    Boolean _mkTimerArmed;
#endif
};
  • _sources0存储的是CFRunLoopSourceRef对象
  • _sources1存储的也是CFRunLoopSourceRef对象
  • _timers存储的是CFRunLoopTimerRef对象
  • _observers存储的是CFRunLoopObserverRef对象

RunLoop_0.png

一个RunLoop中包含若干个Mode,每一个Mode中包含若干个Source/Timer/Observer,RunLoop每次启动都需要指定一个Mode,每次切换RunLoop的Mode都只能退出RunLoop再重新进入。

  • CFRunLoopSourceRef 是事件产生的地方。Source有两个版本:Source0 和 Source1。
    • Source0只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runloop) 来唤醒 RunLoop,让其处理这个事件。
    • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其他线程相互发送消息。这种 Source 能主动唤醒 RunLoop 的线程,其原理在下面会讲到。
  • CFRunLoopTimerRef是基于时间的触发器,它和 NSTimer 是toll-free bridged 的,可以混用。其包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 时,RunLoop会注册对应的时间点,当时间点到时,RunLoop会被唤醒以执行那个回调。
  • CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop 的状态发生变化时,观察者就能通过回调接受到这个变化。可以观测的时间点有以下几个:
/* Run Loop Observer Activities */

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),         //即将进入Loop
    kCFRunLoopBeforeTimers = (1UL << 1),  //即将处理 Timer
    kCFRunLoopBeforeSources = (1UL << 2), //即将处理 Source
    kCFRunLoopBeforeWaiting = (1UL << 5), //即将进入休眠
    kCFRunLoopAfterWaiting = (1UL << 6),  //刚从休眠中唤醒
    kCFRunLoopExit = (1UL << 7),          //即将退出Loop
    kCFRunLoopAllActivities = 0x0FFFFFFFU //
};

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

RunLoop_1.png

CommonModes

这里有个概念叫 CommonModes:一个 Mode 可以将自己标记为Common属性(通过将其 ModeName 添加到 RunLoop 的 commonModes 中)。每当 RunLoop 的内容发生变化时,RunLoop 都会自动将 _commonModeItems 里的 Source/Observer/Timer 同步到具有 “Common” 标记的所有Mode里。

应用场景举例:主线程的 RunLoop 里有两个预置的 Mode:kCFRunLoopDefaultModeUITrackingRunLoopMode。这两个 Mode 都已经被标记为”Common”属性。DefaultMode 是 App 平时所处的状态,TrackingRunLoopMode 是追踪 ScrollView 滑动时的状态。当你创建一个 Timer 并加到 DefaultMode 时,Timer 会得到重复回调,但此时滑动一个TableView时,RunLoop 会将 mode 切换为 TrackingRunLoopMode,这时 Timer 就不会被回调,并且也不会影响到滑动操作。

有时你需要一个 Timer,在两个 Mode 中都能得到回调,一种办法就是将这个 Timer 分别加入这两个 Mode。还有一种方式,就是将 Timer 加入到顶层的 RunLoop 的 commonModeItems 中。commonModeItems` 被 RunLoop 自动更新到所有具有Common属性的 Mode 里去。

问题二

- (void)viewDidLoad {
    [super viewDidLoad];
    
    [NSTimer scheduledTimerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer running");
    }];

    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    scrollView.backgroundColor = [UIColor redColor];
    scrollView.contentSize = CGSizeMake(100, 10000);
    scrollView.showsVerticalScrollIndicator = YES;
    [self.view addSubview:scrollView];
}

在scrollView滚动的时候为什么timer就停止计时了呢????这是跟RunLoop的Mode有关系的一个问题,通过问题一我们知道

// Creates and returns a new NSTimer object initialized with the specified block object and schedules it on the current run loop in the default mode.
+ (NSTimer *)scheduledTimerWithTimeInterval:(NSTimeInterval)interval repeats:(BOOL)repeats block:(void (^)(NSTimer *timer))block API_AVAILABLE(macosx(10.12), ios(10.0), watchos(3.0), tvos(10.0));

这个方法是将timer事件添加到kCFRunLoopDefaultMode中去,当scrollView滚动的时候RunLoop的Mode就变成了UITrackingRunLoopMode,所以滚动的时候timer就不能计时了,我们需要将timer添加到commonItems中去才可以

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSTimer *timer = [NSTimer timerWithTimeInterval:1 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@"timer running");
    }];

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

    UIScrollView *scrollView = [[UIScrollView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
    scrollView.backgroundColor = [UIColor redColor];
    scrollView.contentSize = CGSizeMake(100, 10000);
    scrollView.showsVerticalScrollIndicator = YES;
    [self.view addSubview:scrollView];
}

此时无论scrollView是否滚动timer都能正常计时。

基于RunLoop实现的功能

问题三:自动释放池是什么时候将对象释放的??

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

第一个 Observer 监视的事件是 Entry(即将进入Loop),其回调内会调用 _objc_autoreleasePoolPush() 创建自动释放池。其 order 是-2147483647,优先级最高,保证创建释放池发生在其他所有回调之前。

第二个 Observer 监视了两个事件: BeforeWaiting(准备进入休眠) 时调用_objc_autoreleasePoolPop()_objc_autoreleasePoolPush() 释放旧的池并创建新池;Exit(即将退出Loop) 时调用 _objc_autoreleasePoolPop() 来释放自动释放池。这个 Observer 的 order 是 2147483647,优先级最低,保证其释放池子发生在其他所有回调之后。

在主线程执行的代码,通常是写在诸如事件回调、Timer回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏,除非在一个任务中会瞬间生成非常多的临时变量,否则开发者也不必显式创建 Pool 了。

所以问题答案是:自动释放池中的对象是在RunLoop即将退出或者即将休眠的时候释放。

事件响应

在之前的文章# 响应者链中介绍过。触摸事件首先是由IOKit封装为IOHIDEvent对象传递给SpringBoard进程,SpringBoard进程将对象传递给活跃App的主线程的RunLoop,此时就是给主线程RunLoop发送一个Source1事件,主动激活主线程的RunLoop,然后就是复杂的响应者链处理逻辑。

界面更新

当UI元素需要更新时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,在回调函数中触发UI更新

问题四:定时器有几种请分别介绍?

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

如果某个时间点被错过了,例如执行了一个很长的任务,则那个时间点的回调也会跳过去,不会延后执行。就比如等公交,如果 10:10 时我忙着玩手机错过了那个点的公交,那我只能等 10:20 这一趟了。

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

  • 第二种CADisplayLink

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。它的回调由事件触发,而非时间,因此它更像是一个观察者,而非计时器。触发事件是刷新的帧。因此,CADisplayLink的回调方法在另一帧渲染后立即被调用。如果设备帧率是60FPS,回调方法不是每16.6ms调用一次(六十分之一秒约等于16.6ms),而是帧更新后调用。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。

这里有两种情况需要说明,第一种是屏幕静止不动的时候,第二种是由于CPU繁忙而没有在一个周期内完成计算造成掉帧。

屏幕刷新的流程简单理解为

  1. CPU进行布局计算、图片解码、图像绘制等计算。
  2. 更新App图层树,将图层树编码打包然后将数据提交给Render Server
  3. Render ServerGPU等进行一系列操作显示出图像

流畅动画

3151492-ca818720fc676f1d.webp

掉帧动画

3151492-b6df29f8e1a58c1e.webp

严重掉帧动画

3151492-9925d8e5c31a8f19.webp

我个人理解:CPU计算完成之后就会向RunLoop中添加一个source,这个source的具体实现就是将图层树发送给Render Server,在这一步完成之后就会触发CADisplayLink的回调函数。第一种情况屏幕静止的时候每一帧都显示一样的,这个时候App将图层树发送给Render Server的这个操作还是一直在进行的,所以可以一直触发CADisplayLink的回调函数。第二种情况是CPU没有在一个周期内完成计算,造成这个周期没有向Render Server发送图层树,那么在这一周期内CADisplayLink就没有触发回调,至于说卡顿的时候页面上也在显示上一帧的内容,那是从GPU的缓存中读取出来的。这也就是CADisplayLink作为计时器不准的原因。这只是我的个人理解

  • dispatch_source_t dispatch_source_t作为计时器是比较准确的,因为其实现原理是向内核注册回调函数,到达某个时间点以后内核触发回调函数的执行,此时不依赖RunLoop,所以RunLoop的卡顿不会影响计时器事件执行的准确度。

问题五:如何实现一个保活线程

频繁开辟线程会消耗性能,如果我们有大量的后台任务需要处理,那么可以创建一个常驻线程,有什么异步任务都扔给他来处理,那么怎么创建常驻线程呢?这里有两点需要注意,第一线程本身是一个对象,我们不能让这个对象释放,可以创建一个单例来实现,第二点线程的RunLoop没事干就去歇着了,要让它一直监听事件的处理。

重点就是在子线程RunLoop保活

NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
// Port相关的是Source1事件
//添加了一个Source1,但是这个Source1也没啥事,所以线程在这里就休眠了,不会往下走,----end----一直不会打印
[runLoop addPort:[NSMachPort port] forMode:NSRunLoopCommonModes];
[runLoop run];

问题五:AFNetworking3.0为什么不需要常驻线程了?

参考文章

CFSocket

CFNetwork       ->ASIHttpRequest

NSURLConnection ->AFNetworking

NSURLSession    ->AFNetworking2, Alamofire

  • CFSocket 是最底层的接口,只负责 socket 通信。
  • CFNetwork 是基于 CFSocket 等接口的上层封装,ASIHttpRequest 工作于这一层。
  • NSURLConnection 是基于 CFNetwork 的更高层的封装,提供面向对象的接口,AFNetworking 工作于这一层。
  • NSURLSession 是 iOS7 中新增的接口,表面上是和 NSURLConnection 并列的,但底层仍然用到了 NSURLConnection 的部分功能 (比如 com.apple.NSURLConnectionLoader 线程),AFNetworking2 和 Alamofire 工作于这一层。 通常使用 NSURLConnection 时,你会传入一个 Delegate,当调用了 [connection start] 后,这个 Delegate 就会不停收到事件回调。实际上,start 这个函数的内部会会获取 CurrentRunLoop,然后在其中的 DefaultMode 添加了4个 Source0 (即需要手动触发的Source)。CFMultiplexerSource 是负责各种 Delegate 回调的,CFHTTPCookieStorage 是处理各种 Cookie 的。

当开始网络传输时,我们可以看到 NSURLConnection 创建了两个新线程:com.apple.NSURLConnectionLoader 和 com.apple.CFSocket.private。其中 CFSocket 线程是处理底层 socket 连接的。NSURLConnectionLoader 这个线程内部会使用 RunLoop 来接收底层 socket 的事件,并通过之前添加的 Source0 通知到上层的 Delegate。

RunLoop_network.png

NSURLConnectionLoader 中的 RunLoop 通过一些基于 mach port 的 Source 接收来自底层 CFSocket 的通知。当收到通知后,其会在合适的时机向 CFMultiplexerSource 等 Source0 发送通知,同时唤醒 Delegate 线程的 RunLoop 来让其处理这些通知。CFMultiplexerSource 会在 Delegate 线程的 RunLoop 对 Delegate 执行实际的回调。

通过以上的介绍我们得知当NSURLConnection发送一个网络请求之后需要作为delegate的那个线程保活以响应NSURLConnection的回调

如果我们将主线程作为delegate线程
[[NSURLConnection alloc]initWithRequest:request delegate:[NSRunLoop mainRunLoop] startImmediately:YES];

此时默认向主线成的defaultMode中添加回调任务,当页面滚动时就不会处理响应,但这并不是一个大问题,将回调任务添加到commonModes下就好了

NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:[NSRunLoop mainRunLoop] startImmediately:NO];
[connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
[connection start];

但是将主线程作为回调的代理线程会影响性能,不可取

每一个网络请求都开辟一条线程并保活

这种方式对性能的消耗也是非常大的,不可取

创建一条保活线程

所有的请求在这个线程上发起、同时也在这个线程上回调。

每一个请求对应一个AFHTTPRequestOperation实例对象(以下简称operation),每一个operation在初始化完成后都会被添加到一个NSOperationQueue中。
由这个NSOperationQueue来控制并发,系统会根据当前可用的核心数以及负载情况动态地调整最大的并发 operation 数量,我们也可以通过setMaxConcurrentoperationCount:方法来设置最大并发数。注意:并发数并不等于所开辟的线程数。具体开辟几条线程由系统决定。

也就是说此处执行operation是并发的、多线程的。

1458949-a520eb8d35620fd7.webp

到这里我们回答了为什么AFNetworking 2.x需要常驻线程,那么下面我们来分析为什么AFNetWorking为什么不需要常驻线程了呢?

NSURLConnection的一大痛点就是:发起请求后,这条线程并不能随风而去,而需要一直处于等待回调的状态。

苹果也是明白了这一痛点,从iOS9.0开始 deprecated 了NSURLConnection。 替代方案就是NSURLSession。

self.operationQueue = [[NSOperationQueue alloc] init];
self.operationQueue.maxConcurrentOperationCount = 1;
self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];

NSURLSession发起的请求,不再需要在当前线程进行代理方法的回调!可以指定回调的delegateQueue,这样我们就不用为了等待代理回调方法而苦苦保活线程了。

这一行代码的作用是什么呢??

self.operationQueue.maxConcurrentOperationCount = 1;

NSOperationQueue是用来接收回调的,这里是为了防止出现临界值访问冲突问题。 而AFN2.x中maxConcurrentOperationCount的设置是发起网络请求的,所以不需要设置。

参考文章