iOS线程与runloop运转过程

写在前面

本文是继iOS编译过程iOS启动过程iOS渲染过程系列的最后一篇,通过讨论app中各种事件的派发过程来讲述app是如何运转的,同时假设读者对以下概念有一定的了解

操作系统、内核态、用户态、系统调用、进程、线程、runloop

好~上面的三篇文章依次讲述了:源码被编译为可执行程序、点击icon启动app、首页渲染出来,本文会从这之后的阶段说起

程序的最小执行流

从线程说起,线程是程序的最小执行流,我们写的所有代码都需要在线程中执行,初学者对线程的敏感度不高,是因为平时的开发任务大多都由主线程承载,而且主线程在程序启动之初就创建,而且直到进程退出主线程才会退出,我们写个简单的demo来重新认识下线程

@interface ViewController ()

@property (nonatomic, strong) NSTimer *nstimer;

@end

@implementation ViewController

+ (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;
}

+ (void)networkRequestThreadEntryPoint:(id)__unused object {
    @autoreleasepool {
        [NSThread currentThread].name = @"我是网络线程";
        NSRunLoop *runLoop = [NSRunLoop currentRunLoop];
        [runLoop addPort:[NSMachPort port] forMode:NSDefaultRunLoopMode];
        [runLoop run];
    }
}

- (void)nstimerAction {
    NSLog(@"%s:%@", __func__, [NSThread currentThread]);
    [self performSelector:@selector(doNetWork:) onThread:[ViewController networkRequestThread] withObject:@"hi" waitUntilDone:YES modes:@[NSRunLoopCommonModes]];
}

- (void)doNetWork:(NSString *)str {
    NSLog(@"%@,%s:%@", str, __func__, [NSThread currentThread]);
}

- (void)viewDidLoad {
    [super viewDidLoad];
    [NSThread currentThread].name = @"我是主线程";
    dispatch_async(dispatch_get_global_queue(0, 0), ^{
        [NSThread currentThread].name = @"我是nstimer线程";
        [self nstimer];
    });
}

- (NSTimer *)nstimer {
    if (!_nstimer) {
        _nstimer = [NSTimer scheduledTimerWithTimeInterval:20.0 target:self selector:@selector(nstimerAction) userInfo:nil repeats:YES];
        [[NSRunLoop currentRunLoop] addTimer:_nstimer forMode:NSDefaultRunLoopMode];
        [[NSRunLoop currentRunLoop] run];
    }
    return _nstimer;
}
@end

如上我们重写了主线程的名字,创建了一个nstimer线程和一个网络线程,启动app后在timer的回调函数打个断点,线程调用栈如下

可见应用程序目前为止创建了10个线程

  • 主线程第一个被创建,其序号是1,序号后面是队列名称
  • 序号2~5和9线程已经被操作系统回收
  • 序号6线程是系统线程,我们暂时不得而知它的工作内容
  • 序号7线程是我们通过全局并发队列创建的nstimer线程
  • 序号8线程是苹果创建用于接收UIKit事件的线程
  • 序号10是我们创建的常驻网络线程

我们将其分别展开

可以发现所有线程顶部栈帧都是mach_msg_trap,只有一个不一样的线程6停留在__workq_kernreturn,而且我们自己创建的两个线程一个nstimer线程、一个网络线程栈帧里面都发现了RunLoop字样,但是其他几个就没有,OK~带着如上事实,我们来思考如下问题

消失的序号2~5和9线程为什么被操作系统回收?
其他线程何以一直存活?

熟悉runloop的同学可能心里已经有了答案,运行循环已经启动并且添加了持续源的线程一直存活,没有启动的执行完任务后10s左右会被操作系统回收,然而,这种理解实在鲁莽!!!

重新认识下如下两个术语:

❌runloop是用来管理线程的
❌runloop跟线程是一对一关系

倒不是说这两种说法错误,只是有些喧宾夺主了,看个demo,我们自己写一个循环

BOOL quit = NO;
do {
    NSLog(@"我是runloop");
} while (!quit);

如上代码,与系统的runloop最核心的区别只有一个,苹果实现的runloop跑完一圈后会通过系统调用使当前线程由用户栈进入内核栈,线程还是那个线程,它没有休眠,只是被系统内核阻塞了,就是上面看到的两个函数mach_msg_trap__workq_kernreturnrunloop跑完一圈后会调用类似如上的指令陷阱函数,此时发生软件中断,该函数不会返回直到再次发生系统调用,内核才会关闭中断,此时线程从内核栈回到用户栈,线程被激活,runloop执行一次,进程在创建之初会被分配一个进程id称为PID,接着会绑定一个端口号,因此进程间的通讯依赖的是端口号

因此我们来修改下上面两个术语:

有能力管理线程的只有程序员和操作系统,线程的保活手段有很多,启用系统的runloop是其中一种,只是一个线程最多只需要一个runloop,runloop是执行在线程中的一个循环,通过系统调用和进程间通讯使线程在用户栈与内核栈间切换,从而实现线程的阻塞与激活,空闲时既不执行也退出,激活后执行一次循环后再次阻塞


用户交互事件

主线程被创建的同时苹果创建了一个名为com.apple.uikit.eventfetch-thread的线程,该线程用于接收用户与硬件的交互事件:触屏、运动、远程控制事件,他们都被定义为UIEvent

一个交互事件产生后会在操作系统内核中由IOKit生成一个IOHIDEvent事件,由桌面进程接收,然后通过IPC分发给活跃的app进程,mach_port消息生成source1,eventfetch-thread的runloop注册了对应的CFRunLoopSource,进程随之进行系统调用以关闭中断,随之该线程从内核栈切换为用户栈,线程被激活,执行以下逻辑:获取主runloop,将__handleEventQueue所对应的source0标志为true,接着该线程进行系统调用,以关闭主线程的中断,随之主线程从内核栈切换为用户栈,主线程被激活,然后调用__handleEventQueue进行应用内部的分发

除此之外,手势也是在这个过程中处理的

当上面的__handleEventQueue识别了一个手势时,会打断触屏事件的回调,随后系统将对应的 UIGestureRecognizer标记为待处理,runloop默认注册了一个观察者,用于监听kCFRunLoopBeforeWaiting(即将进入休眠)事件,其内部会获取所有被标记为待处理的手势对象,并执行其回调

这里可以思考个问题,苹果为什么要单独启一个线程来处理uikit交互事件,在main runloop内注册一个对应的CFRunLoopSource不就解决了吗?

用户使用app,交互是大概率事件,但是对于主线程承载的任务量来讲,交互就是个小概率事件,而且我们平时说的架构、模式,说到底就是将一个模块拆分到最小粒度,然后再组装起来,嗯~为了拓展性和易维护性

有关UI界面更新事件定时器事件的运行原理,作者写过两篇文章,本文不再赘述~

网络事件

借用网络上的两张图

如上是计算机内部网络链条的参与者:

应用程序进程->操作系统->网卡驱动->网卡

简单起见我们用四层网络模型描述iOS网络事件的流程,iOS中的基础网络api如下:

CFSocket、CFNetwork、NSURLConnection、NSURLSession

CFSocket是一组套接字接口定义在CoreFoundation中,CFNetwork是封装了套接字的网络库工作在传输层与应用层之间,NSURLConnection和NSURLSession是更上层的封装工作在应用层,如果你用过这些原生的api进行开发,你或许会发现使用NSURLConnection、NSURLSession进行网络请求的时候是无需手动创建线程的,原因是苹果在Foundation框架中帮我们做了这件事情,我们从应用程序实际场景出发,以http协议为例,假如我们要在app首页的头图位置显示天气,我们发送一条网络请求,等待天气数据回来之后刷新UI,整个过程是怎样的呢

如下是计算机网络工作流程图,展示了客户端发送请求到服务端的完成路径,响应的路径刚好相反


  • 网络线程首先通过DNS服务将其解析成目标机器的ip地址
  • 接着进行应用层的封装,就是封装成http规定的报文格式
  • 接着通过套接字将报文发送到传输层,至此进入内核协议栈,每一层协议都会包装上一层报文,再添加本层的协议头向下传,最终由网卡发送出去
  • 中途的每个节点都叫做一个网关,其内部会有路由表,指示下一步应该把数据发送到那个节点,最终找到目标网卡
  • 每一层核对本层的信息然后向上一层发送,到传输层tcp模块将数据按照报文段的序号依次放入接收缓冲区中,tcp头部包含了目标进程绑定的端口号,通过IPC向目标端口发送消息通知应用程序读取数据
  • 服务器进程监听了80端口,如果是https则监听443端口,服务端开始读取数据,响应过程的链条刚好相反

行文至此,是否想起了tcp三次握手(被聊烂了不得不想起)有理解过握手阶段是在干什么吗

以下信息来自百度百科,为了建立连接TCP连接,通信双方必须从对方了解如下信息

  • 对方报文发送的开始序号
  • 对方发送数据的缓冲区大小
  • 能被接收的最大报文段长度
  • 被支持的TCP选项

第一次握手:建立连接时,客户端发送syn包(seq=j)到服务器,并进入SYN_SENT状态,等待服务器确认;SYN:同步序列编号(Synchronize Sequence Numbers)

第二次握手:服务器收到syn包,必须确认客户端的SYN(ack=j+1),同时自己也发送一个SYN包(seq=k),即SYN+ACK包,此时服务器进入SYN_RECV状态

第三次握手:客户端收到服务器的SYN+ACK包,向服务器发送确认包(ACK=k+1),此包发送完毕,客户端和服务器进入ESTABLISHED(TCP连接成功)状态,完成三次握手

咳咳,又来了

为何tcp需要建立链接呢?

连接的主要功能在于记录端到端通信状态,为了丢包重传,为了实现数据包按序接收等可靠性功能,总之是为了可靠性传输

是不是又要问了,不用tcp就不能实现可靠传输了吗?

自然是能的,QQ的通讯就是可靠的UDP,就是自己实现了一套基于UDP的具备丢包重试、按需接收等功能的可靠传输

为什么需要四次挥手呢?中间人攻击是什么?tcp的网络拥塞机制是怎样的?巴拉巴拉..

太多了,这里就不赘述了,有兴趣的,自行去看下各层协议内容~

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345

推荐阅读更多精彩内容