写在前面
本文是继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_kernreturn
,runloop跑完一圈后会调用类似如上的指令陷阱函数,此时发生软件中断,该函数不会返回直到再次发生系统调用,内核才会关闭中断,此时线程从内核栈回到用户栈,线程被激活,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的网络拥塞机制是怎样的?巴拉巴拉..
太多了,这里就不赘述了,有兴趣的,自行去看下各层协议内容~