TCP/IP 协议族

TCP/IP 协议族

TCP/IP 协议不是 TCPIP 这两个协议的合称,而是指因特网整个 TCP/IP 协议族。

参考模型

TCP/IP 参考模型

TCP/IP 参考模型是首先由 ARPANET 所使用的网络体系结构。这个体系结构在它的两个主要协议出现以后被称为 TCP/IP 参考模型。这一网络协议一般分为四层:

  • 网络访问层/链路层,用来处理连接网络的硬件部分。

  • 互联网层/网络层用来处理在网络上流动的数据包,是整个体系结构的关键部分,其功能是使主机可以把分组发往任何网络,并使分组独立地传向目标。这些分组可能经过不同的网络,到达的顺序和发送的顺序也可能不同。高层如果需要顺序收发,那就必须自行处理对分组的排序。互联网层使用因特网协议(IP)。

  • 传输层为处于网络连接中的计算机之间的通信提供数据传输服务。在这一层定义了两个端到端的协议:传输控制协议(TCP)和用户数据报协议(UDP)。

    • TCP 是面向连接的协议,它提供可靠的报文传输和对上层应用的连接服务。为此,除了基本的数据传输外,它还有可靠性保证、流量控制、多路复用、优先权和安全性控制等功能。

    • UDP 是面向无连接的不可靠传输协议,主要用于不需要 TCP 的排序和流量控制等功能的应用程序。

  • 应用层包含所有的高层协议,包括:虚拟终端协议(TELNET)、文件传输协议(FPT)、电子邮件传输协议(SMTP)、域名服务(DNS,)、网上新闻传输协议(NNTP)和超文本传输协议(HTTP),这些协议为应用提供对应的通信服务。

    • TELNET 是远程登录服务的标准协议和主要方式,为用户提供了在本地计算机上完成远程主机工作的能力。
      • FTP 是用于在网络上进行文件传输的一套标准协议。
    • SMTP 是一个提供可靠且有效的电子邮件传输的协议,它建立在 FTP 服务之上,主要用于完成系统之间的邮件信息传递,并提供有关来信的通知。
      • DNS 主要用于域名和 IP 之间的相互转换,是一种分布式网络目录服务。
    • NNTP 用于新闻的发布、检索和获取。
      • HTTP 是一个基于 TCP 协议的用于客户端和服务端通信的 请求-响应 协议。

image-20200714091919812

OSI 参考模型

OSI 参考模型是国际标准化组织指定的一个用于计算机或通信系统之间互联的标准体系。

  • 物理层 - 通过物理媒体传输原始字节流

  • 链路层 - 定义网络上数据的格式

  • 网络层 - 决定数据通过哪条物理路径进行传输

  • 传输层 - 通过传输协议传输数据

  • 会话层 - 维护链接并负责控制端口和会话

  • 展示层 - 保证数据的格式时可用并加密的

  • 应用层 - 计算机交互层,在这里应用可以访问网络服务

TCP/IP 特点

  • TCP/IP 协议不依赖于任何特定的计算机硬件或操作系统,提供开放的协议标准。
  • TCP/IP 并不依赖于特定的网络传输硬件,所以 TCP/IP 协议能够集成各种各样的网络。
  • 统一的网络地址分配方案,使整个 TCP/IP 设备在网中都有唯一的地址。
  • 标准化的高层协议,可以提供多种可靠的服务。

TCP/IP 通信传输流

利用 TCP/IP 协议族进行网络通信时, 会通过分层顺序与对方进行通信。发送端从应用层往下走,接收端则往应用层往上走。

发送端在层与层之间传输数据时,每经过一层时必定会被打上一个 该层所属的首部信息。反之,接收端在层与层传输数据时,每经过一层 时会把对应的首部消去。

这种把数据信息包装起来的做法称为封装。

image-20200714103627565

IP 协议

IP 协议位于 TCP/IP 参考模型中的网络层,是整个 TCP/IP 协议族的核心,也是构成互联网的基础。它的主要内容包括:

  • IP 编址方案
  • 分组封装格式
  • 分组转发规则

TCP 协议

TCP 协议位于参考模型中的传输层,是一种面向连接的、可靠的、基于字节流的传输层通信协议。当应用层向传输层发送用于网间传输的、用8位字节表示的数据流,TCP 则把数据流分割成适当长度的报文段( segment ),最大传输段大小通常受该计算机连接的网络的数据链路层的最大传送单元( MTU )限制。之后TCP把数据包传给IP层,由它来通过网络将包传送给接收端实体的TCP层。

为了准确无误地将数据送达目标处,TCP 协议采用了三次握手策略来建立连接:

  1. 发送端首先向接收端发送一个带有 SYN 标志的数据包

  2. 接收端接收到带有 SYN 标志的数据包后,返回带有 ACK/SYN 标志的数据包

  3. 发送端在接收到接收端回传的带有 ACK/SYN 标志的数据包后,发送带有 ACK 标志的数据包表示握手结束

三次握手

连接建立后,就可以开始传输数据了。

TCP 连接其实是接收端和客户端保存的一份关于对方的信息,如果 IP 地址、端口号等。

同样的,在断开连接时,TCP 会采用四次挥手策略来保证接数据发送安全和完整:

  1. 当发送端的数据都传输完成后,发送端会向接收端发送连接释放报文 FIN。需要注意的是发送端发送 FIN 报文后,只是不能发送数据了,但是能正常接收数据的。

  2. 接收端收到发送端的 FIN 报文后,回复包含 ACK 标志的确认报文,此时接收端处于等待关闭状态,而不是马上给发送端发送 FIN 报文,因为可能还有数据没有发送完成。

  3. 接收端的数据发送完成后,发送带有 ACKFIN 标志的连接释放报文给发送端。

  4. 发送端收到接收端的 FIN 报文后,向接收端发送 ACK 报文。此时接收端并不是立刻释放 TCP 连接,而是等待 2MSL( 最大报文段寿命的两倍时长 )后才释放连接。但是接收端一旦收到发送端的 ACK 报文后就会立马释放连接。

四次挥手

TCP 和 UDP

TCP 是面向连接的传输控制协议,而 UDP 提供了无连接的数据报服务;

TCP 具有高可靠性,确保传输数据的正确性,不出现丢失或乱序;UDP 在传输数据前不建立连接,不对数据报进行检查与修改,无须等待对方的应答,所以会出现分组丢失、重复、乱序,应用程序需要负责传输可靠性方面的所有工作;

UDP 具有较好的实时性,工作效率较 TCP 协议高;

UDP 段结构比 TCP 的段结构简单,因此网络开销也小。

TCP 协议可以保证接收端毫无差错地接收到发送端发出的字节流,为应用程序提供可靠的通信服务。对可靠性要求高的通信系统往往使用 TCP 传输数据。比如 HTTP 运用 TCP 进行数据的传输。

HTTP 概览

HTTP 协议

开发中,我们经常需要向服务器端发送数据或从服务器端请求特定数据,为了完成数据在客户端和服务器端的传输,我们在传输数据时必须用到 HTTP 协议。

什么是 HTTP 协议?

HTTP 协议,即 HyperText Transmission Protocol,超文本传输协议,定义了客户端与服务器端的数据传输规则,让客户端和服务器能够有效地进行数据沟通。

HTTP 的基本性质

  • HTTP 是简单的
  • HTTP 是可扩展的 - 通过 HTTP headers 可以轻松对协议进行扩展
  • HTTP 是无状态,有会话的 - 在同一个连接中,两个执行成功的请求之间是没有关系的

HTTP 请求与响应

HTTP 请求

HTTP 协议规定,一个完整的 HTTP 请求应包含如下内容

  • 请求行 :包含请求方法、请求统一资源标示符和 HTTP 版本号。

  • 请求头 :请求头包含客户端传送给服务器端的附加信息。

    Name Description
    Host 目标服务器的网络地址
    Accept 告知服务器端客户端能够接收的数据类型,如 ‘text/html’等
    Content-Type 请求体中的数据类型,如 ‘Application/Json; charset=UTF-8’等
    Accept-Language 客户端的语言环境,如 ‘zh-cn’ 等
    Accept_Encoding 客户端支持的数据压缩格式,如 ‘gzip’ 等
    User-Agent 客户端的软件环境
    Connection : keep-alive 告知服务器这是一个持久连接
    Content-Length 请求体的长度
    Cookie 记录着用户保存在本地的用户数据
  • 请求体 :发送给服务器端的数据

    在使用 POST-Multipart 上传请求中请求体就是上传文件的二进制数据。

    在使用 GET 请求时,请求体为空。

    在普通的 POST 请求中,请求体就是表单数据。

  • 响应状态行 : 服务器返回给客户端的状态信息,一般包含 HTTP 版本号、状态码和状态码对应的英文名称。

    一个典型的状态行如下:

    1
    HTTP/1.1 200 OK	

HTTP 响应

基本与 HTTP 请求相同。

HTTP 的版本

HTTP 的主要版本如下

Version Feature
< HTTP 1.1 不支持持久连接;无请求头和响应头;客户端的前后请求是同步的。
HTTP 1.1 增加请求头和响应头;支持持久连接;客户端的不同请求之间是异步的。
HTTP 2.0 向下兼容 HTTP 1.1,但只用于 https 网址。

HTTP 缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。这样带来的好处有:缓解服务器端压力,提升性能(获取资源的耗时更短了)。对于网站来说,缓存是达到高性能的重要组成部分。缓存需要合理配置,因为并不是所有资源都是永久不变的:重要的是对一个资源的缓存应截止到其下一次发生改变(即不能缓存过期的资源)。

缓存操作的目标

虽然 HTTP 缓存不是必须的,但重用缓存的资源通常是必要的。然而常见的 HTTP 缓存智能存储 GET 响应。

缓存控制

Cache-control

HTTP/1.1 定义的 Cache-Control 头用来区分对缓存机制的支持情况,请求头和响应头都支持这个属性,你可以通过它提供的不同的值来定义缓存策略。

  • 禁止进行缓存

    1
    2
    Canche-Control: no-store,
    Canche-Control: no-cache, no-store
  • 强制确认缓存

    每次有请求发出时,缓存会将次请求发送到服务器,服务器端会验证请求中所描述的缓存是否过期,若未过期,则缓存才使用本地缓存副本

    1
    Cache-Control: must-revalidate
  • 私有缓存和公共缓存

    public 指令表示该响应可以被任何中间人缓存。若指定了 public ,则一些通常不被中间人缓存的页面,将被缓存。

    private 则表示该响应是专用于某单个用户的,中间人不能缓存此响应。

  • 缓存过期机制

    max-age=<seconds> 指令表示资源能够被缓存的最大时间,这个时间是距离请求发起的时间的秒数。一般用来缓存应用中不会改变的文件,通过手动设置一定的时长以保证缓存有效。

    1
    Cache-Control: max-age=10000
  • 缓存验证确认

    当使用了 must-revalidate 指令,那就意味着缓存在考虑使用一个资源时,必须先验证它的状态,已过期的缓存将不被使用

Pargma

Pargma 是 HTTP/1.1 标准中定义的一个 header 属性,请求中包含 Pargma 的效果跟在头信息中定义 Cache-Control: no-cache 相同,但是 HTTP 的响应头不支持这个属性,所以它不能完全替代 Cache-Control 头。

新鲜度

在过期时间之前,缓存资源是新鲜的,否则是陈旧的。一个陈旧的资源是不会被直接清除的,当客户端发起一个请求时,检索到已经有一个对应的缓存副本,则会在此次请求上附加一个 If-None-Match 头,然后再发送给服务器,以此来检查此资源是否依然是新鲜的,若返回 304 (Not Modified) ,则表示该副本是新鲜的,否则返回新的资源。

缓存验证

用户点击刷新按钮时会开始缓存验证。如果缓存的响应头信息里含有 Cache-control: must-revalidate 的定义,在浏览的过程中也会触发缓存验证。另外,在浏览器偏好设置里设置 Advanced->Cache 为强制验证缓存也能达到相同的效果。

当缓存的文档过期后,需要进行缓存验证或者重新获取资源。只有在服务器返回强校验器或者弱校验器时才会进行验证。

ETag

作为缓存的一种强校验器,ETag 响应头是一个对用户代理不透明的值。对于像浏览器这样的 HTTP UA,不知道 ETag 代表什么,不能预测它的值是多少。如果资源请求的响应头里含有 ETag, 客户端可以在后续的请求的头中带上 If-None-Match 头来验证缓存。

Last-Modified 响应头可以作为一种弱校验器。说它弱是因为它只能精确到一秒。如果响应头里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。

当向服务端发起缓存校验的请求时,服务端会返回 200 ok 表示返回正常的结果或者 304 Not Modified表示浏览器可以使用本地缓存文件。304 的响应头也可以同时更新缓存文档的过期时间。

需要注意的是 If-None-Match 的优先级高于 If-Modified-Since,两者同时存在的话,按照前者进行校验。

Vary 头的响应

Vary HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还会使用缓存的文件。

当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary都匹配,才能使用缓存的响应。

HTTP Cookies

HTTP Cookie(也叫Web Cookie或浏览器Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

服务器通过在响应头里面添加一个 Set-Cookie 选项,来使浏览器保存下 Cookie,之后对该服务器的每一次请求中都通过 Cookie 请求头部将 Cookie 信息发送给服务器。

  • Set-Cookie 响应头部和 Cookie 请求头部

    服务器使用 Set-Cookie 响应头部向用户代理发送 Cookie 信息

    1
    Set-Cookie: <name>=<value>

    保存 Cookie 信息后,对该服务器发起的每一次新请求,浏览器都会将保存的 Cookie 信息通过 Cookeie 请求头再发送给服务器。

  • 会话期 Cookie

    会话期 Cookie 是最简单的 Cookie:浏览器关闭后它会被自动删除,即它仅在会话期内有效。

    会话期 Cookie 不需要指定过期时间或者有效期

  • 持久性 Cookie

    持久性 Cookie 指定了特定的过期时间或有效期,不会随着浏览器的关闭而被删除。

    1
    2
    Set-Cookie: id=asfwa; 
    Expires=Wed, 21 Oct 2015 07:28:00 GMT;

    设定的过期时间只和客户端有关,而不是服务端。

  • Cookie 的 SecureHttpOnly 标记

    标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务器,但由于 Cookie 固有的不安全性,敏感信息不应该通过 Cookie 传输。

    为避免跨域脚本攻击,通过 JavaScript 的 Document.cookie API 无法访问带有 HttpOnly 标记的 Cookie,它们只应发送给服务器。

  • Cookie 的作用域

    通过 DomainPath 标识可以定义 Cookie 的作用域:即 Cookie 应该发送给哪些 URL。

    Domain 指定哪些主机可以接受 Cookie,如果不指定,则默认为当前文档的主机,且不包含子域名。如果指定了,则会包含子域名。

    Path 指定主机下的哪些路径可以接受 Cookie,以字符 %x2F (即 /) 作为路径分隔符,子路径也会被匹配。

  • SameSite Cookies

    SameSite Cookie 允许服务器要求某个 Cookie 在跨站请求时不会被发送,从而可以组织跨站请求伪造攻击。

  • JavaScript 通过 document.cookies 访问 Cookie

    通过 Document.cookie 属性可创建新的 Cookie,也可以通过该属性访问非 HttpOnly 标记的 Cookie。

当机器处于不安全环境时,切记不能通过 Cookie 存储传输敏感信息。

  • 会话劫持和 XSS

    在 Web 应用中, Cookie 常用来标记用户或授权会话。因此,如果 Web 应用的 Cookie 被窃取,可能导致授权用户的会话受到攻击。

    HttpOnly 类型的 Cookie 由于阻止了 JavaScript 对其的访问性能而在一定程度上缓解了此类攻击。

  • 跨站请求伪造

    通过以下方式可以一定程度上阻止宽展请求伪造:

    • 对用户输入进行过滤来阻止 XSS
    • 任何敏感操作都需要确认
    • 用于敏感信息的 Cookie 只能拥有较短的生命周期

追踪和隐私

  • 第三方 Cookie

    每个 Cookie 都会有与之关联的域(Domain),如果 Cookie 的域和页面的域相同,那么我们称这个 Cookie 为第一方 Cookie ,如果 Cookie 的域和页面的域不同,则称之为第三方Cookie。一个页面包含图片或存放在其他域上的资源(如图片广告)时,第一方的 Cookie 也只会发送给设置它们的服务器。通过第三方组件发送的第三方 Cookie 主要用于广告和网络追踪。

  • 禁止追踪 Do-Not-Track

    虽然并没有法律或者技术手段强制要求使用DNT,但是通过DNT可以告诉Web程序不要对用户行为进行追踪或者跨站追踪。

  • 欧盟 Cookie 指令

  • 僵尸 Cookie 和删不掉的 Cookie

HTTP 访问控制

跨域资源共享(CORS)是一种使用额外的 HTTP 头来使运行在一个 origin 上的 web 应用被准许访问来自不同源服务器上的指定的资源的机制,即当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,就会发起一个跨域 HTTP请求。

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。对那些可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(Preflight Request),从而获知服务端是否允许该跨域请求。服务端确认允许后,才发起实际的 HTTP 请求。

HTTP 消息

HTTP 消息是客户端和服务器之间交换数据的方式。它们分为两种类型:由客户端发送的用来在服务器上触发动作的消息和从服务器得到的回应。

HTTP 消息由跨越多行的用 ASCII 编码的文本信息组成。在 HTTP/1.1 和更早期的版本的协议中,消息通过连接明文发送。在 HTTP/2 中,为了优化和性能提升,人类可读的消息被分割成 HTTP 帧。

HTTP Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 请求头的名称是大小写不敏感的

// Start line
// [HTTP Method] [Request Target] [HTTP Version]
GET /img/me.png HTTP/1.1
GET http://developer.mozilla.org/en-US/docs/Web/HTTP/Messages HTTP/1.1

// Headers
// Request Headers : 对请求的设置
User-Agent
Accept
Accept-Language
Accept-Encoding

// General Headers : 作用于消息整体
Connection

// Entity Headers : 作用与请求的 body 部分,如果 body 部分没有数据,则没有这部分头
Content-Type
Content-Length

// Body
// 大体上分为两类 : 单一资源 body 和多资源 body

HTTP Response

1
2
3
4
5
6
7
8
// Status line
// [HTTP Version] [Status Code] [Status Text]
HTTP/1.1 404 Not Found

// Headers
// 与 HTTP Request 结构相似

// Body

HTTP/2 帧

HTTP/1.1 的消息在性能上有着一系列的缺点:

  • Header 不像 body,它不会被压缩
  • 两个报文之间的 header 通常非常相似,但它们仍然在连接中重复传输
  • 无法复用。当在同一个服务器打开几个连接时:TCP 热连接比冷连接更加有效

所以在 HTTP/2 中,消息被分割成嵌入到流中的帧。Headers 和 Body 的帧是分开的,这使得 Headers 帧也可以被压缩。多个流可以被组合在一起,这是一种称为多路复用的技术,它使得 TCP 连接下的传输更有效率。

HTTP 会话

在类似 HTTP 的客户端-服务器协议中,会话由三个部分组成:

  • 客户端建立一个 TCP 连接
  • 客户端发送请求,等待回应
  • 服务器处理请求,做出回应

从 HTTP/1.1 起,连接在完成第三部后不再被关闭,客户端被允许发起新的请求,这意味着第二和第三部可以重复进行多次。

客户端-服务器协议中,在 HTTP 中打开一个连接,意味着在底层传输层初始化连接。使用 TCP 时,HTTP 服务器默认的端口号是 80。

HTTP 、Scoket 和 TCP 的区别

HTTP 是应用层的协议,TCP 是传输层的协议,而 Socket 是从传输层抽象的一个抽象层,本质是接口。

  1. TCP 连接与 HTTP 连接的区别

    HTTP 是基于 TCP的,客户端向服务器端发送一个 HTTP 请求时,第一步就是要建立与服务端的 TCP 连接。

  2. TCP 连接与 Socket 连接的区别

    Socket 层只是在 TCP/UDP 传输层上做的一个抽象接口层。

    基于 TCP 协议的 Socket 连接同样需要通过三次握手建立连接,是可靠的。

    基于 UDP 协议的 Socket 连接不需要建立连接的过程,不管对方能不能收到都会发送过去,是不可靠的。

  3. HTTP 连接与 Socket 连接的区别

    • HTTP 是短连接,基于 TCP 协议的 Socket 连接是长连接。尽管 HTTP 1.1 开始支持持久连接,但仍无法保证始终连接。

      而基于 TCP 协议的 Socket 连接一旦建立成功,除非一方主动断开,否则连接状态一直保持。

    • HTTP 连接,服务器无法主动发送消息,而 Socket 连接,双发请求的发送没有先后限制。

      HTTP 采用 ‘请求-响应’ 机制,在客户端没有发送请求给服务端时,服务端无法推送消息给客服端。

      Socket 连接双方类似于 P2P 的关系,可以随时互相发送消息。

网络编程基础

网络编程

TCP/IP 协议

TCP/IP 协议的基本概念

TCP/IP 协议不是 TCP 和 IP 这两个协议的合称,而是指因特网整个 TCP/IP 协议族。

TCP/IP 是 Transmission Control Protocol / Internet Protocol 的简写,即 ‘传输控制协议/因特网互联协议’,又名网络通讯协议,是 Internet 最基本的协议、Internet 国际互联网络的基础。

TCP/IP 由网络层的 IP 协议和传输层的 TCP 协议组成。

TCP/IP 定义了电子设备如何连如因特网,以及数据如何在它们之间传输的标准。协议采用了4层的层级架构,每一层都呼叫它的下一层所提供的协议来完成自己的需求。通俗而言,TCP 负责发现传输的问题,一旦发现问题就发出信号,要求重新传输,直到所有数据正确地传输到目的地。而 IP 是给因特网每一台联网设备规定一个地址。

参考模型

TCP/IP 参考模型

TCP/IP 参考模型是首先由 ARPANET 所使用的网络体系结构。这个体系结构在它的两个主要协议出现以后被称为 TCP/IP 参考模型。这一网络协议共分为四层:

  • 网络访问层,即 Network Access Layer,在 TCP/IP 参考模型中并没有信息描述,只是指出主机必须使用某种协议与网络相连。

  • 互联网层,即 Internet Layer,是整个体系结构的关键部分,其功能是使主机可以把分组发往任何网络,并使分组独立地传向目标。这些分组可能是经过不同的网络,到达的顺序和发送的顺序也可能不同。高层如果需要顺序收发,那就必须自行处理对分组的排序。互联网层使用因特网协议(IP, Internet Protocol)。

  • 传输层,即 Transport Layer,使源端和目的端机器上的对等实体可以进行会话。在这一层定义了两个端到端的协议:传输控制协议(TCP,Transmission Control Protocol)和用户数据报协议(UDP,User Datagram Protocol)。

    TCP 是面向连接的协议,它提供可靠的报文传输和对上层应用的连接服务。为此,除了基本的数据传输外,它还有可靠性保证、流量控制、多路复用、优先权和安全性控制等功能。

    UDP 是面向无连接 的不可靠传输协议,主要用于不需要 TCP 的排序和流量控制等功能的应用程序。

  • 应用层,即 Application Layer,包含所有的高层协议,包括:虚拟终端协议(TELNET, TELecommunications NETwork)、文件传输协议(FPT, File Transfer Protocol)、电子邮件传输协议(SMTP, Simple Mail Transfer Protocol)、域名服务(DNS, Domain Name Service)、网上新闻传输协议(NNTP, Net News Transfer Protocol)和超文本传输协议(HTTP, HyperText Transfer Protocol)。

    TELNET 允许一台机器上的用户登录到远程机器上,并进行工作。

    FTP 提供了有效地将文件从一台机器上转移到另一台机器上的方法。

    SMTP 用于电子邮件的收发。

    DNS 用于把主机名映射到网络地址。

    NNTP 用于新闻的发布、检索和获取。

    HTTP 用于在 WWW 上获取网页。

OSI 参考模型

OSI 参考模型是国际标准化组织指定的一个用于计算机或通信系统之间互联的标准体系。

  • 物理层 - 通过物理媒体传输原始字节流

  • 链路层 - 定义网络上数据的格式

  • 网络层 - 决定数据通过哪条物理路径进行传输

  • 传输层 - 通过传输协议传输数据

  • 会话层 - 维护链接并负责控制端口和会话

  • 展示层 - 保证数据的格式时可用并加密的

  • 应用层 - 计算机交互层,在这里应用可以访问网络服务

TCP/IP 特点

  • TCP/IP 协议不依赖于任何特定的计算机硬件或操作系统,提供开放的协议标准。
  • TCP/IP 并不依赖于特定的网络传输硬件,所以 TCP/IP 协议能够集成各种各样的网络。
  • 统一的网络地址分配方案,使整个 TCP/IP 设备在网中都有唯一的地址。
  • 标准化的高层协议,可以提供多种可靠的服务。

HTTP 协议

开发中,我们经常需要向服务器端发送数据或从服务器端请求特定数据,为了完成数据在客户端和服务器端的传输,我们在传输数据时必须用到 HTTP 协议。

什么是 HTTP 协议?

HTTP 协议,即 HyperText Transmission Protocol,超文本传输协议,定义了客户端与服务器端的数据传输规则,让客户端和服务器能够有效地进行数据沟通。

HTTP 的基本性质

  • HTTP 是简单的
  • HTTP 是可扩展的 - 通过 HTTP headers 可以轻松对协议进行扩展
  • HTTP 是无状态,有会话的 - 在同一个连接中,两个执行成功的请求之间是没有关系的

HTTP 请求与响应

HTTP 请求

HTTP 协议规定,一个完整的 HTTP 请求应包含如下内容

  • 请求行 :包含请求方法、请求统一资源标示符和 HTTP 版本号。

  • 请求头 :请求头包含客户端传送给服务器端的附加信息。

    Name Description
    Host 目标服务器的网络地址
    Accept 告知服务器端客户端能够接收的数据类型,如 ‘text/html’等
    Content-Type 请求体中的数据类型,如 ‘Application/Json; charset=UTF-8’等
    Accept-Language 客户端的语言环境,如 ‘zh-cn’ 等
    Accept_Encoding 客户端支持的数据压缩格式,如 ‘gzip’ 等
    User-Agent 客户端的软件环境
    Connection : keep-alive 告知服务器这是一个持久连接
    Content-Length 请求体的长度
    Cookie 记录着用户保存在本地的用户数据
  • 请求体 :发送给服务器端的数据

    在使用 POST-Multipart 上传请求中请求体就是上传文件的二进制数据。

    在使用 GET 请求时,请求体为空。

    在普通的 POST 请求中,请求体就是表单数据。

  • 响应状态行 : 服务器返回给客户端的状态信息,一般包含 HTTP 版本号、状态码和状态码对应的英文名称。

    一个典型的状态行如下:

    1
    HTTP/1.1 200 OK	

HTTP 响应

基本与 HTTP 请求相同。

HTTP 的版本

HTTP 的主要版本如下

Version Feature
< HTTP 1.1 不支持持久连接;无请求头和响应头;客户端的前后请求是同步的。
HTTP 1.1 增加请求头和响应头;支持持久连接;客户端的不同请求之间是异步的。
HTTP 2.0 向下兼容 HTTP 1.1,但只用于 https 网址。

HTTP 缓存

缓存是一种保存资源副本并在下次请求时直接使用该副本的技术。当 web 缓存发现请求的资源已经被存储,它会拦截请求,返回该资源的拷贝,而不会去源服务器重新下载。这样带来的好处有:缓解服务器端压力,提升性能(获取资源的耗时更短了)。对于网站来说,缓存是达到高性能的重要组成部分。缓存需要合理配置,因为并不是所有资源都是永久不变的:重要的是对一个资源的缓存应截止到其下一次发生改变(即不能缓存过期的资源)。

缓存操作的目标

虽然 HTTP 缓存不是必须的,但重用缓存的资源通常是必要的。然而常见的 HTTP 缓存智能存储 GET 响应。

缓存控制

Cache-control

HTTP/1.1 定义的 Cache-Control 头用来区分对缓存机制的支持情况,请求头和响应头都支持这个属性,你可以通过它提供的不同的值来定义缓存策略。

  • 禁止进行缓存

    1
    2
    Canche-Control: no-store,
    Canche-Control: no-cache, no-store
  • 强制确认缓存

    每次有请求发出时,缓存会将次请求发送到服务器,服务器端会验证请求中所描述的缓存是否过期,若未过期,则缓存才使用本地缓存副本

    1
    Cache-Control: must-revalidate
  • 私有缓存和公共缓存

    public 指令表示该响应可以被任何中间人缓存。若指定了 public ,则一些通常不被中间人缓存的页面,将被缓存。

    private 则表示该响应是专用于某单个用户的,中间人不能缓存此响应。

  • 缓存过期机制

    max-age=<seconds> 指令表示资源能够被缓存的最大时间,这个时间是距离请求发起的时间的秒数。一般用来缓存应用中不会改变的文件,通过手动设置一定的时长以保证缓存有效。

    1
    Cache-Control: max-age=10000
  • 缓存验证确认

    当使用了 must-revalidate 指令,那就意味着缓存在考虑使用一个资源时,必须先验证它的状态,已过期的缓存将不被使用

Pargma

Pargma 是 HTTP/1.1 标准中定义的一个 header 属性,请求中包含 Pargma 的效果跟在头信息中定义 Cache-Control: no-cache 相同,但是 HTTP 的响应头不支持这个属性,所以它不能完全替代 Cache-Control 头。

新鲜度

在过期时间之前,缓存资源是新鲜的,否则是陈旧的。一个陈旧的资源是不会被直接清除的,当客户端发起一个请求时,检索到已经有一个对应的缓存副本,则会在此次请求上附加一个 If-None-Match 头,然后再发送给服务器,以此来检查此资源是否依然是新鲜的,若返回 304 (Not Modified) ,则表示该副本是新鲜的,否则返回新的资源。

缓存验证

用户点击刷新按钮时会开始缓存验证。如果缓存的响应头信息里含有 Cache-control: must-revalidate 的定义,在浏览的过程中也会触发缓存验证。另外,在浏览器偏好设置里设置 Advanced->Cache 为强制验证缓存也能达到相同的效果。

当缓存的文档过期后,需要进行缓存验证或者重新获取资源。只有在服务器返回强校验器或者弱校验器时才会进行验证。

ETag

作为缓存的一种强校验器,ETag 响应头是一个对用户代理不透明的值。对于像浏览器这样的 HTTP UA,不知道 ETag 代表什么,不能预测它的值是多少。如果资源请求的响应头里含有 ETag, 客户端可以在后续的请求的头中带上 If-None-Match 头来验证缓存。

Last-Modified 响应头可以作为一种弱校验器。说它弱是因为它只能精确到一秒。如果响应头里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。

当向服务端发起缓存校验的请求时,服务端会返回 200 ok 表示返回正常的结果或者 304 Not Modified表示浏览器可以使用本地缓存文件。304 的响应头也可以同时更新缓存文档的过期时间。

需要注意的是 If-None-Match 的优先级高于 If-Modified-Since,两者同时存在的话,按照前者进行校验。

Vary 头的响应

Vary HTTP 响应头决定了对于后续的请求头,如何判断是请求一个新的资源还会使用缓存的文件。

当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary都匹配,才能使用缓存的响应。

HTTP Cookies

HTTP Cookie(也叫Web Cookie或浏览器Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器,如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

服务器通过在响应头里面添加一个 Set-Cookie 选项,来使浏览器保存下 Cookie,之后对该服务器的每一次请求中都通过 Cookie 请求头部将 Cookie 信息发送给服务器。

  • Set-Cookie 响应头部和 Cookie 请求头部

    服务器使用 Set-Cookie 响应头部向用户代理发送 Cookie 信息

    1
    Set-Cookie: <name>=<value>

    保存 Cookie 信息后,对该服务器发起的每一次新请求,浏览器都会将保存的 Cookie 信息通过 Cookeie 请求头再发送给服务器。

  • 会话期 Cookie

    会话期 Cookie 是最简单的 Cookie:浏览器关闭后它会被自动删除,即它仅在会话期内有效。

    会话期 Cookie 不需要指定过期时间或者有效期

  • 持久性 Cookie

    持久性 Cookie 指定了特定的过期时间或有效期,不会随着浏览器的关闭而被删除。

    1
    2
    Set-Cookie: id=asfwa; 
    Expires=Wed, 21 Oct 2015 07:28:00 GMT;

    设定的过期时间只和客户端有关,而不是服务端。

  • Cookie 的 SecureHttpOnly 标记

    标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务器,但由于 Cookie 固有的不安全性,敏感信息不应该通过 Cookie 传输。

    为避免跨域脚本攻击,通过 JavaScript 的 Document.cookie API 无法访问带有 HttpOnly 标记的 Cookie,它们只应发送给服务器。

  • Cookie 的作用域

    通过 DomainPath 标识可以定义 Cookie 的作用域:即 Cookie 应该发送给哪些 URL。

    Domain 指定哪些主机可以接受 Cookie,如果不指定,则默认为当前文档的主机,且不包含子域名。如果指定了,则会包含子域名。

    Path 指定主机下的哪些路径可以接受 Cookie,以字符 %x2F (即 /) 作为路径分隔符,子路径也会被匹配。

  • SameSite Cookies

    SameSite Cookie 允许服务器要求某个 Cookie 在跨站请求时不会被发送,从而可以组织跨站请求伪造攻击。

  • JavaScript 通过 document.cookies 访问 Cookie

    通过 Document.cookie 属性可创建新的 Cookie,也可以通过该属性访问非 HttpOnly 标记的 Cookie。

当机器处于不安全环境时,切记不能通过 Cookie 存储传输敏感信息。

  • 会话劫持和 XSS

    在 Web 应用中, Cookie 常用来标记用户或授权会话。因此,如果 Web 应用的 Cookie 被窃取,可能导致授权用户的会话受到攻击。

    HttpOnly 类型的 Cookie 由于阻止了 JavaScript 对其的访问性能而在一定程度上缓解了此类攻击。

  • 跨站请求伪造

    通过以下方式可以一定程度上阻止宽展请求伪造:

    • 对用户输入进行过滤来阻止 XSS
    • 任何敏感操作都需要确认
    • 用于敏感信息的 Cookie 只能拥有较短的生命周期

追踪和隐私

  • 第三方 Cookie

    每个 Cookie 都会有与之关联的域(Domain),如果 Cookie 的域和页面的域相同,那么我们称这个 Cookie 为第一方 Cookie ,如果 Cookie 的域和页面的域不同,则称之为第三方Cookie。一个页面包含图片或存放在其他域上的资源(如图片广告)时,第一方的 Cookie 也只会发送给设置它们的服务器。通过第三方组件发送的第三方 Cookie 主要用于广告和网络追踪。

  • 禁止追踪 Do-Not-Track

    虽然并没有法律或者技术手段强制要求使用DNT,但是通过DNT可以告诉Web程序不要对用户行为进行追踪或者跨站追踪。

  • 欧盟 Cookie 指令

  • 僵尸 Cookie 和删不掉的 Cookie

HTTP 访问控制

跨域资源共享(CORS)是一种使用额外的 HTTP 头来使运行在一个 origin 上的 web 应用被准许访问来自不同源服务器上的指定的资源的机制,即当一个资源从与该资源本身所在的服务器不同的域、协议或端口请求一个资源时,就会发起一个跨域 HTTP请求。

跨域资源共享标准新增了一组 HTTP 首部字段,允许服务器声明哪些源站通过浏览器有权限访问哪些资源。对那些可能对服务器数据产生副作用的 HTTP 请求方法,浏览器必须首先使用 OPTIONS 方法发起一个预检请求(Preflight Request),从而获知服务端是否允许该跨域请求。服务端确认允许后,才发起实际的 HTTP 请求。

HTTP 消息

HTTP 消息是客户端和服务器之间交换数据的方式。它们分为两种类型:由客户端发送的用来在服务器上触发动作的消息和从服务器得到的回应。

HTTP 消息由跨越多行的用 ASCII 编码的文本信息组成。在 HTTP/1.1 和更早期的版本的协议中,消息通过连接明文发送。在 HTTP/2 中,为了优化和性能提升,人类可读的消息被分割成 HTTP 帧。

HTTP Request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 请求头的名称是大小写不敏感的

// Start line
// [HTTP Method] [Request Target] [HTTP Version]
GET /img/me.png HTTP/1.1
GET http://developer.mozilla.org/en-US/docs/Web/HTTP/Messages HTTP/1.1

// Headers
// Request Headers : 对请求的设置
User-Agent
Accept
Accept-Language
Accept-Encoding

// General Headers : 作用于消息整体
Connection

// Entity Headers : 作用与请求的 body 部分,如果 body 部分没有数据,则没有这部分头
Content-Type
Content-Length

// Body
// 大体上分为两类 : 单一资源 body 和多资源 body

HTTP Response

1
2
3
4
5
6
7
8
// Status line
// [HTTP Version] [Status Code] [Status Text]
HTTP/1.1 404 Not Found

// Headers
// 与 HTTP Request 结构相似

// Body

HTTP/2 帧

HTTP/1.1 的消息在性能上有着一系列的缺点:

  • Header 不像 body,它不会被压缩
  • 两个报文之间的 header 通常非常相似,但它们仍然在连接中重复传输
  • 无法复用。当在同一个服务器打开几个连接时:TCP 热连接比冷连接更加有效

所以在 HTTP/2 中,消息被分割成嵌入到流中的帧。Headers 和 Body 的帧是分开的,这使得 Headers 帧也可以被压缩。多个流可以被组合在一起,这是一种称为多路复用的技术,它使得 TCP 连接下的传输更有效率。

HTTP 会话

在类似 HTTP 的客户端-服务器协议中,会话由三个部分组成:

  • 客户端建立一个 TCP 连接
  • 客户端发送请求,等待回应
  • 服务器处理请求,做出回应

从 HTTP/1.1 起,连接在完成第三部后不再被关闭,客户端被允许发起新的请求,这意味着第二和第三部可以重复进行多次。

客户端-服务器协议中,在 HTTP 中打开一个连接,意味着在底层传输层初始化连接。使用 TCP 时,HTTP 服务器默认的端口号是 80。

HTTP 、Scoket 和 TCP 的区别

HTTP 是应用层的协议,TCP 是传输层的协议,而 Socket 是从传输层抽象的一个抽象层,本质是接口。

  1. TCP 连接与 HTTP 连接的区别

    HTTP 是基于 TCP的,客户端向服务器端发送一个 HTTP 请求时,第一步就是要建立与服务端的 TCP 连接。

  2. TCP 连接与 Socket 连接的区别

    Socket 层只是在 TCP/UDP 传输层上做的一个抽象接口层。

    基于 TCP 协议的 Socket 连接同样需要通过三次握手建立连接,是可靠的。

    基于 UDP 协议的 Socket 连接不需要建立连接的过程,不管对方能不能收到都会发送过去,是不可靠的。

  3. HTTP 连接与 Socket 连接的区别

    • HTTP 是短连接,基于 TCP 协议的 Socket 连接是长连接。尽管 HTTP 1.1 开始支持持久连接,但仍无法保证始终连接。

      而基于 TCP 协议的 Socket 连接一旦建立成功,除非一方主动断开,否则连接状态一直保持。

    • HTTP 连接,服务器无法主动发送消息,而 Socket 连接,双发请求的发送没有先后限制。

      HTTP 采用 ‘请求-响应’ 机制,在客户端没有发送请求给服务端时,服务端无法推送消息给客服端。

      Socket 连接双方类似于 P2P 的关系,可以随时互相发送消息。

内存管理理解与分析

iOS 内存管理

iOS 中的程序内存结构


在 iOS 程序中,内存可以粗略的分为五个区域:

Name Descroption
由操作系统自动分配和释放,常用来存放函数的参数值、局部变量的值等。优点是快速高效,缺点是有限制,数据
一般由程序员分配和释放,常用来存储对象
全局区 用来存储已经初始化的全局变量和静态变量,程序结束时才会被释放回收
常量区 用来存储常量的区域,程序结束时才会被释放回收
代码段 用来存放程序的执行代码,直到程序结束才会释放回首

在 iOS程序中,只有堆区中存放的数据是需要手动释放回收的,其它区域存储的数据的释放和回收都由系统进行管理。当一个 iOS 程序启动后,它的全局区、常量区和代码区就已经确定了。

  • 栈区 (stack) 是由编译器自动分配和释放,用来存放函数的参数值、局部变量等。栈是系统数据结构,对应进程/线程是唯一的。优点是快速高效,缺点是有限制,数据不灵活。

  • 堆区 (heap) 由程序员分配和释放,如果程序员不释放,程序结束时,可能由操作系统回收。优点是灵活方便,数据适应面广泛,但是效率有一定降低。

  • 全局区/静态区 (static) 存放全局变量和静态变量,初始话的全局变量和静态变量存放在一块区域,未初始化的全局变量和静态变量在另一块区域,程序结束后由系统自动释放。

  • 文字常量区,用来存储常量字符串,程序结束后由系统释放。

  • 代码区,存放函数的二进制代码

iOS 中的内存管理


因为 iOS 程序的内存分配中,只有堆区是有程序员进行管理的,所以 iOS 的内存管理大致上就是可以认为是堆区内存的管理。

在 Objective-C 中,使用引用计数来确定一个对象所占有的内存空间是否应该被回收。它的工作原理可以描述为:

Objective-C 中的每一个对象都有一个类型为 unsigned long 的 retainCount 的属性,这个属性由拥有它的对象进行维护。当我们新创建出这个对象的一个实例时,这个对象实例的 retainCount 值为1,每当一个新的引用指向对象,对象的 retainCount 值就会增加1,每当这个对象实例的引用减少一个,retainCount 的值就减少1。当着对象实例的 retainCount 的值为0时,代表这个对象实例没有被引用,系统会自动将这个对象实例的内存空间回收并同时调用这个实例对象的 dealloc 方法。

需要注意的几个问题:

  • 常量是没有引用计数的
  • 使用对象实例的属性值进行赋值,不会引用这个对象
  • 释放对象实例时会调用 dealloc 方法,如果没有调用则会造成内存泄漏
  • 对引用计数为1的对象实例发送 release 消息时,系统不会再对其进行 retainCount - 1 的操作。

MRC 和 ARC

使用对象实例的引用计数来进行 iOS的内存管理,分为两种方式:

  • MRC :手动引用计数,由程序员手动的管理对象实例的引用计数
  • ARC :自动引用计数,是基于 MRC 的,系统自动的管理对象实例的引用计数

实际上在 iOS 5 之后,Apple 就开始推荐使用 ARC 来进行 iOS 程序的内存管理工作,目前 MRC 已经非常少见。

ARC 中,编译器会在编译时在代码中插入合适的 retain 和 release 语句。

ARC 中的修饰符

ARC 中有四种修饰符

Name Description
__strong 强引用,默认值,持有所指向对象的所有权
__weak 弱引用,不持有所指向对象的所有权,所指向的对象销毁后,引用会自动置为 nil
__autoreleasing 自动释放对象的引用,一般用来传递参数
__unsafe_unretained 为兼容 MRC 出现的修饰符,可看成 MRC 下的 weak
属性的内存管理

常见的属性修饰符

Name Description
assign 直接赋值,一般用来修饰基本数据类型。修饰 Objc 对会造成野指针
retain 保留新值,再释放旧值,再设置新值
copy 拷贝新值,再释放旧值,再设置新值
weak ARC 新引入,可代替 assign,自动置 nil
strong ARC 新引入,可代替 retain
block 的内存管理

使用@property声明一个 block 时,使用 copy 来修饰。

block 会对内部使用的对象进行强应用,在使用时可能会造成循环引用,可通过添加弱引用标记来解决:

1
__weak typeof(self) weakSelf	=	self;

Autorelease & AutoreleasePool

在实际的情境中,经常会遇到不知道一个对象实例再什么时候不再使用,因而造成不知道应该何时才能将其释放的情况。Objective-C 中提供了 autorelease 方法来解决这个问题。

当给一个对象实例发送 autorelease 消息时,它会被添加到合适的自动释放池中,当自动释放池销毁时,会给自动释放池中的所有对象实例发送 release 消息。

autorelease 不会改变对象的引用计数。

创建自动释放池的两种方法:

1
2
3
4
5
6
7
// 1
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
[pool release];
// 2
@autoreleasepool {

}

值得注意的是自动释放池实质上只是在销毁时给其中的所有对象发送了 release 消息,并不保证对象一定会被销毁。

内存管理问题和解决方案

僵尸对象和野指针

僵尸对象是指内存已经被回收的对象,而野指针是指向僵尸对象的指针。

向野指针发送消息会导致程序崩溃,就是经典的 : EXC_BAD_ACCESS 错误。

所以为了避免产生僵尸对象和野指针,在对象释放后,应将其指针置为 nil。

循环引用

当对象之间相互拥有彼此的强引用,形成闭环引用时,就称为循环引用。

循环引用会造成程序内存消耗过高、程序闪退等问题。

以下几种情况可能会造成循环引用:

  • 由于父类指针可以指向子类对象,当父类对象和子类对象相互引用时,就造成了循环引用

  • 作为对象属性的 block 中强引用了对象,造成循环引用,解决方法如下:

    1
    2
    3
    4
    5
    __weak typeof(self) weakSelf = self;
    self.testObject.testCircleBlock = ^{
    __strong typeof (weakSelf) strongSelf = weakSelf;
    [strongSelf doSomething];
    };
  • 使用 strong 修饰符修饰代理属性,造成循环引用

  • 作为属性的 NSTimer,造成循环引用

循环中对象占用内存大

常见于循环次数较大,循环体生成的对象占用内存较大的情景。

可通过在循环中创建自己的 autoreleasePool 或及时释放占用内存大的 临时变量来解决。

RunLoop 探索与分析

RunLoop 基础

什么是RunLoop?

RunLoop 是一种让线程能随时处理事件但并不退出的机制,是一个用来调度工作的和协调接受的事件的循环。

iOS系统中,提供了 NSRunLoop 和 CFRunLoopRef 两个对象来实现 RunLoop。RunLoop 对象管理其需要处理的事件和消息,并提供了一个入口函数来执行事件循环的逻辑。线程执行了这个函数之后,就会一直处于这个函数内部的循环中,直到这个循环结束,函数返回。

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

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

RunLoop与线程的关系

线程和 RunLoop 之间是一一对应的,其关系保存在一个全局的 Dictionary 中。线程刚创建是并没有 RunLoop,如果你不主动获取,那它一直不会有。RunLoop 的创建是在第一次获取时发生的,RunLoop 的销毁是在线程结束时发生的。

你只能在一个线程内部获取它的 RunLoop(主线程除外)。

RunLoop的对外接口

在 CoreFoundation 中关于 RunLoop 的类有以下几个:

  • CFRunLoopRef
  • CFRunLoopModeRef
  • CFRunLoopSourceRef
  • CFRunLoopTImerRef
  • CFRunLoopObserverRef

一个 RunLoop 包含若干个 Mode ,每个 Mode 又包含若干个 Source/Timer/Observer。每次调用 RunLoop 的主函数时,只能指定其中一个 Mode。如果需要切换 Mode ,只能退出 RunLoop,再重新指定一个 Mode 进入。这样做的目的是为了分隔开不同组的 Source/Timer/Observer,使其不能互相影响。

CFRunLoopSourceRef 是事件产生的地方,有两个版本 Source0 和 Source1:

  • Source0 只包含了一个回调(函数指针),它并不能主动触发事件。使用时,你需要先调用 CFRunLoopSourceSignal(source),将这个 Source 标记为待处理,然后手动调用 CFRunLoopWakeUp(runLoop) 来唤醒 RunLoop ,让其处理这个事件。
  • Source1 包含了一个 mach_port 和一个回调(函数指针),被用于通过内核和其它线程相互发送消息,它能主动唤醒 RunLoop 的线程。

CFRunLoopTimerRef 是基于时间的触发器,他和 NSTimer 是 toll-free bridge 的,可以混用。包含一个时间长度和一个回调(函数指针)。当其加入到 RunLoop 中时,RunLoop 会注册对应的时间点,当时间点到时,RunLoop 会被唤醒执行那个回调。

CFRunLoopObserverRef 是观察者,每个 Observer 都包含了一个回调(函数指针),当 RunLoop的状态发生变化时,观察者就能通过回调接收到这个变化。

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

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

RunLoop 的Mode

CFRunLoopMode 和 CFRunLoop 的结构大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
struct __CFRunLoopMode {
CFStringRef _name;
CFMutableSetRef _source0;
CFMutableSetRef _source1;
CFMutableArrayRef _observers;
CFMutableArrayRef _timers;
}

struce __CFRunLoop {
CFMutableSetRef _commonModes;
CFMutableSetRef _commonModeItems;
CFRunLoopModeRef _currentMode;
CFMutableSetRef _modes;
}

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

CFRunLoop对外暴露的管理 Mode 的接口只有两个:

1
2
CFRunLoopAddCommonMode(CFRunLoopRef runloop, CFStringRef modeName);
CFRunLoopRunInMode(CFStringRef modeName);

CFRunLoopModeRef 暴露的管理 Mode Item 接口有:

1
2
3
4
5
6
CFRunLoopAddSource(CFRunLoopRef runloop, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopAddObserver(CFRunLoopRef runloop, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopAddTimer(CFRunLoopRef runloop, CFRunLoopTimerRef timer, CFStringRef modeName);
CFRunLoopRemoveSource(CFRunLoopRef runloop, CFRunLoopSourceRef source, CFStringRef modeName);
CFRunLoopRemoveObserver(CFRunLoopRef runloop, CFRunLoopObserverRef observer, CFStringRef modeName);
CFRunLoopRemoveTimer(CFRunLoopRef runloop, CFRunLoopTimerRef timer, CFStringRef modeName);

你只能通过 mode name 来操作内部的 mode,当你传入一个新的 mode name 时但 RunLoop内部没有对应的 mode 时,RunLoop 会自动的帮你创建对应的 CFRunLoopModeRef。对于一个 RunLoop ,其内部的 mode 只能添加不能删除。

Apple公开提供的 Mode 只有两个:kCFRunLoopDefaultMode(NSDefaultRunLoopMode) 和 UITrackingRunLoopMode,你可以用这两个 Mode Name 来操作其对应的 Mode。

同时 Apple 还提供了一个操作 Common 标记的字符串 : kCFRunLoopModes(NSDefaultRunLoopCommonModes),你可以用这个字符串来操作 Common Items,或标记一个 Mode 为 “Common”。使用时要注意区分这个字符串和其它 Mode Name。

RunLoop 的内部逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// DefaultMode 启动
void CFRunLoopRun(void) {
CFRunLoopRunSpecific(CFRunLoopGetCurrent(),kCFRunLoopDefaultMode, 1.0e10, false);
}

// 用指定的 Mode 启动,允许设置 RunLoop 超时时间
int CFRunLoopRunInMode(CFStringRef ModeName, CFTimeInterval seconds, Boolean stopAfterHandle) {
return CFRunLoopSpecific(CFRunLoopGetCurrent(), modeName , seconds ,returnAfterSourceHandle)
}

// RunLoop 的实现
int CFRunLoopRunSpecific(runloop, modeName ,seconds , stopeAfterHandle) {
// 根据 ModeName 找到对应 Mode
CFRunLoopModeRef currentMode = __CFRunLoopFinMode(runloop, modeName, false);
// 如果 Mode 里没有 source/timer/observer,直接返回
if (__CFRunLoopModeIsEmpty(currentMode)) {
return;
}

// 通知 Observer , RunLoop 即将进入 loop
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopEntry);
// 进入 loop
__CFRunLoopRun(runloop, currentMode, seconds, returnAfterSourceHandle) {
Boolean sourceHandledThisLoop = NO;
int retVal = 0;
do {
// 通知 Observers : RunLoop 即将触发 Timer 回调
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeTimers);
// 通知 Observers : RunLoop 即将触发 Source0 (非port) 回调
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeSources);
// 执行被加入的 block
__CFRunLoopDoBlocks(runloop, currentMode);
// RunLoop 触发 Source0(非port) 回调
sourceHandledThisLoop = __CFRunLoopDoSource0(runloop, currentMode, stopAfterHandele);
// 执行被加入的 block
__CFRunLoopDoBlocks(runloop, currentMode);

// 如果有 Source1 处于 ready 状态,直接处理这个 Source1 然后跳转去处理消息
if (__Source0DidDispatchPortLastTime) {
Boolean hasMsg = __CFRunLoopServiceMachPort(dispatchPort, &msg);
if (hasMsg)
{
goto handle_msg;
}
}

// 通知 Observers : RunLoop 的线程即将结束进入休眠(Sleep)
if (!sourceHandleThisLoop) {
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopBeforeWaiting);
}

// 调用 mach_msg 等待接受 mach_port 的消息。线程进入休眠,直到被下面一个事件唤醒
// 1.一个基于 port 的 Source 的事件
// 2.一个 Timer 时间到了
// 3.RunLoop 自身的超时时间到了
// 4.被其它调用者手动唤醒了
__CFRunLoopServiceMachPort(waitSet, &msg, sizeof(msg_buffer), &livePort) {
mach_msg(msg, MACH_RCV_MSG, port);
}

// 通知 Observers : RunLoop 的线程刚刚被唤醒了
__CFRunLoopDoObservers(runloop, currentMode, kCFRunLoopAfterWaiting);

// 收到消息,处理消息
handle_msg;

// 如果一个 Timer 的时间到了,触发这个 Timer 的回调
if (msg_is_timer) {
__CFRunLoopDoTimers(runloop, currentMode, mach_absolute_time());
} else if (msg_is_dispatch) {
// 如果有 dispatch 到 main_queue 的 block ,执行 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE_(msg);
} else {
// 如果有一个 Source1 发出事件了, 处理这个事件
CFRunLoopSourceRef source1 = __CFRunLoopModeFindSourceForMachPort(runloop, currentMode, livePort);
sourceHandleThisLoop = __CFRunLoopDoSource1(runloop, currentMode, source1, msg);
if (sourceHandleThisLoop) {
mach_msg(reply, MACH_SEND_MSG, reply);
}
}
// 执行加入到 Loop 的 block
__CFRunLoopDoBlocks(runloop, currentMode);

if (sourceHandleThisLoop && stopAfterHandle) {
// 进入 loop 时参数说处理完事件就返回
retVal = kCFRunLoopRunHandledSource;
} else if (timeOut) {
// 超出传入参数标记的超时时间了
retVal = kCFRunLoopRunTimeOut;
} else if (__CFRunLoopIsStoped(runloop)) {
// 被外部调用者强行停止了
retVal = kCFRunLoopRunStoped;
} else if (__CFRunLoopModeIsEmpty(runloop, currentMode)) {
// Source/Timer/Observer 一个都没有了
retVal = kCFRunLoopRunFinished;
}

// 如果没超时,mode 里没空, loop 也没有被停止,那就继续 loop
} while (retVal == 0)
}
}

RunLoop有什么用?

App 启动后 RunLoop 的状态 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
CFRunLoop {
current mode = kCFRunLoopDefaultMode
common modes = {
UITrackingRunLoopMode
kCFRunLoopDefaultMode
}

common mode items = {

// source0 (manual)
CFRunLoopSource {order =-1, {
callout = _UIApplicationHandleEventQueue}}
CFRunLoopSource {order =-1, {
callout = PurpleEventSignalCallback }}
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}

// source1 (mach port)
CFRunLoopSource {order = 0, {port = 17923}}
CFRunLoopSource {order = 0, {port = 12039}}
CFRunLoopSource {order = 0, {port = 16647}}
CFRunLoopSource {order =-1, {
callout = PurpleEventCallback}}
CFRunLoopSource {order = 0, {port = 2407,
callout = _ZL20notify_port_callbackP12__CFMachPortPvlS1_}}
CFRunLoopSource {order = 0, {port = 1c03,
callout = __IOHIDEventSystemClientAvailabilityCallback}}
CFRunLoopSource {order = 0, {port = 1b03,
callout = __IOHIDEventSystemClientQueueCallback}}
CFRunLoopSource {order = 1, {port = 1903,
callout = __IOMIGMachPortPortCallback}}

// Ovserver
CFRunLoopObserver {order = -2147483647, activities = 0x1, // Entry
callout = _wrapRunLoopWithAutoreleasePoolHandler}
CFRunLoopObserver {order = 0, activities = 0x20, // BeforeWaiting
callout = _UIGestureRecognizerUpdateObserver}
CFRunLoopObserver {order = 1999000, activities = 0xa0, // BeforeWaiting | Exit
callout = _afterCACommitHandler}
CFRunLoopObserver {order = 2000000, activities = 0xa0, // BeforeWaiting | Exit
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
CFRunLoopObserver {order = 2147483647, activities = 0xa0, // BeforeWaiting | Exit
callout = _wrapRunLoopWithAutoreleasePoolHandler}

// Timer
CFRunLoopTimer {firing = No, interval = 3.1536e+09, tolerance = 0,
next fire date = 453098071 (-4421.76019 @ 96223387169499),
callout = _ZN2CAL14timer_callbackEP16__CFRunLoopTimerPv (QuartzCore.framework)}
},

modes = {
CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },

CFRunLoopMode {
sources0 = { /* same as 'common mode items' */ },
sources1 = { /* same as 'common mode items' */ },
observers = { /* same as 'common mode items' */ },
timers = { /* same as 'common mode items' */ },
},

CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = 0, {
callout = FBSSerialQueueRunLoopSourceHandler}}
},
sources1 = (null),
observers = {
CFRunLoopObserver >{activities = 0xa0, order = 2000000,
callout = _ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv}
)},
timers = (null),
},

CFRunLoopMode {
sources0 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventSignalCallback}}
},
sources1 = {
CFRunLoopSource {order = -1, {
callout = PurpleEventCallback}}
},
observers = (null),
timers = (null),
},

CFRunLoopMode {
sources0 = (null),
sources1 = (null),
observers = (null),
timers = (null),
}
}
}

系统注册了五个默认的 Mode:

1.kCFRunLoopDefaultMode :App 的默认 Mode,通常主线程就是在这个 Mode 下运行的。

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

3.UIInitializationRunLoopMode :在刚启动 App 时进入的第一个 Mode,启动完成后就不再使用。

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

5.kCFRunLoopCommonModes :这是一个占位的 Mode,没有实际作用。

当 RunLoop 进行回调时,一般都是通过一个很长的函数调用出去(call out),当你在你的代码中下断点时,通常能在调用栈中看到这些函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
/// 1. 通知Observers,即将进入RunLoop
/// 此处有Observer会创建AutoreleasePool: _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopEntry);
do {

/// 2. 通知 Observers: 即将触发 Timer 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeTimers);
/// 3. 通知 Observers: 即将触发 Source (非基于port的,Source0) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeSources);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 4. 触发 Source0 (非基于port的) 回调。
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE0_PERFORM_FUNCTION__(source0);
__CFRUNLOOP_IS_CALLING_OUT_TO_A_BLOCK__(block);

/// 6. 通知Observers,即将进入休眠
/// 此处有Observer释放并新建AutoreleasePool: _objc_autoreleasePoolPop(); _objc_autoreleasePoolPush();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopBeforeWaiting);

/// 7. sleep to wait msg.
mach_msg() -> mach_msg_trap();


/// 8. 通知Observers,线程被唤醒
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopAfterWaiting);

/// 9. 如果是被Timer唤醒的,回调Timer
__CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__(timer);

/// 9. 如果是被dispatch唤醒的,执行所有调用 dispatch_async 等方法放入main queue 的 block
__CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE__(dispatched_block);

/// 9. 如果如果Runloop是被 Source1 (基于port的) 的事件唤醒了,处理这个事件
__CFRUNLOOP_IS_CALLING_OUT_TO_A_SOURCE1_PERFORM_FUNCTION__(source1);


} while (...);

/// 10. 通知Observers,即将退出RunLoop
/// 此处有Observer释放AutoreleasePool: _objc_autoreleasePoolPop();
__CFRUNLOOP_IS_CALLING_OUT_TO_AN_OBSERVER_CALLBACK_FUNCTION__(kCFRunLoopExit);
}

AutoreleasePool

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

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

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

在主线程执行的代码,通常是写在诸如事件回调、Timer 回调内的。这些回调会被 RunLoop 创建好的 AutoreleasePool 环绕着,所以不会出现内存泄漏。

事件响应

Apple 注册了一个 Source1 (基于 mach port) 用来接收系统事件,其回调函数为 __IOHIDEventSystemClientQueueCallBack()。

当一个硬件事件发生后,首先由 IOKit.framework 生成一个 IOHIDEvent 事件并由 SpringBoard 接收。SpingBoard 只接收按键、触摸、加速、接近传感器等几种 Event,随后用 mach port 转发给需要的 App 进程。随后 Apple 注册的那个 Source1 就会触发回调,并调用 _UIApplicationHandleEventQueue() 进行应用内的分发。

_UIApplicationHandleEventQueue() 会把 IOHIDEvent 处理并包装成 UIEvent 进行处理或分发,其中包括识别 UIGesture、处理屏幕旋转、发送给 UIWindow等。通常事件比如 UIButton 点击、touchesBegin/Move/End/Cancle 事件都是在这个回调中完成的。

手势识别

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

Apple 注册了一个 Observer 检测 BeforeWaiting 事件,这个 Observer 的回调函数是 _UIGestureRecognizerUpdateObsever(), 其内部会获取所有刚被标记为待处理的 GestureRecognizer,并执行 GestureRecognizer 的回调。

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

界面更新

当在操作 UI 时,比如改变了 Frame、更新了 UIView/CALayer 的层次时,或者手动调用了 UIView/CALayer 的 setNeedsLayout/setNeedsDisplay方法后,这个 UIView/CALayer 就被标记为待处理,并被提交到一个全局的容器去。

苹果注册了一个 Observer 监听 BeforeWaiting(即将进入休眠) 和 Exit (即将退出Loop) 事件,回调去执行一个很长的函数:

_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()。这个函数里会遍历所有待处理的 UIView/CAlayer 以执行实际的绘制和调整,并更新 UI 界面。

1
2
3
4
5
6
7
8
9
10
11
_ZN2CA11Transaction17observer_callbackEP19__CFRunLoopObservermPv()
QuartzCore:CA::Transaction::observer_callback:
CA::Transaction::commit();
CA::Context::commit_transaction();
CA::Layer::layout_and_display_if_needed();
CA::Layer::layout_if_needed();
[CALayer layoutSublayers];
[UIView layoutSubviews];
CA::Layer::display_if_needed();
[CALayer display];
[UIView drawRect];

定时器

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

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

CADisplayLink 是一个和屏幕刷新率一致的定时器(但实际实现原理更复杂,和 NSTimer 并不一样,其内部实际是操作了一个 Source)。如果在两次屏幕刷新之间执行了一个长任务,那其中就会有一帧被跳过去(和 NSTimer 相似),造成界面卡顿的感觉。在快速滑动TableView时,即使一帧的卡顿也会让用户有所察觉。Facebook 开源的 AsyncDisplayLink 就是为了解决界面卡顿的问题,其内部也用到了 RunLoop。

PerformSelector

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

当调用 performSelector:onThread: 时,实际上其会创建一个 Timer 加到对应的线程去,同样的,如果对应线程没有 RunLoop 该方法也会失效。

关于GCD

实际上 RunLoop 底层也会用到 GCD 的东西,比如 RunLoop 是用 dispatch_source_t 实现的 Timer。但同时 GCD 提供的某些接口也用到了 RunLoop, 例如 dispatch_async()。

当调用 dispatch_async(dispatch_get_main_queue(), block) 时,libDispatch 会向主线程的 RunLoop 发送消息,RunLoop会被唤醒,并从消息中取得这个 block,并在回调 CFRUNLOOP_IS_SERVICING_THE_MAIN_DISPATCH_QUEUE() 里执行这个 block。但这个逻辑仅限于 dispatch 到主线程,dispatch 到其他线程仍然是由 libDispatch 处理的。

关于网络请求

iOS 中,关于网络请求的接口自下至上有如下几层:

1
2
3
4
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。

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

RunLoop 怎么用?

AFNetWorking

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+ (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 不至于退出,并没有用于实际的发送消息。

1
2
3
4
5
6
7
8
9
10
- (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 中。

AsyncDisplayKit

AsyncDisplayKit 是 Facebook 推出的用于保持界面流畅性的框架,其原理大致如下:

UI 线程中一旦出现繁重的任务就会导致界面卡顿,这类任务通常分为3类:排版,绘制,UI对象操作。

排版通常包括计算视图大小、计算文本高度、重新计算子式图的排版等操作。

绘制一般有文本绘制 (例如 CoreText)、图片绘制 (例如预先解压)、元素绘制 (Quartz)等操作。

UI对象操作通常包括 UIView/CALayer 等 UI 对象的创建、设置属性和销毁。

其中前两类操作可以通过各种方法扔到后台线程执行,而最后一类操作只能在主线程完成,并且有时后面的操作需要依赖前面操作的结果 (例如TextView创建时可能需要提前计算出文本的大小)。ASDK 所做的,就是尽量将能放入后台的任务放入后台,不能的则尽量推迟 (例如视图的创建、属性的调整)。

为此,ASDK 创建了一个名为 ASDisplayNode 的对象,并在内部封装了 UIView/CALayer,它具有和 UIView/CALayer 相似的属性,例如 frame、backgroundColor等。所有这些属性都可以在后台线程更改,开发者可以只通过 Node 来操作其内部的 UIView/CALayer,这样就可以将排版和绘制放入了后台线程。但是无论怎么操作,这些属性总需要在某个时刻同步到主线程的 UIView/CALayer 去。

ASDK 仿照 QuartzCore/UIKit 框架的模式,实现了一套类似的界面更新的机制:即在主线程的 RunLoop 中添加一个 Observer,监听了 kCFRunLoopBeforeWaiting 和 kCFRunLoopExit 事件,在收到回调时,遍历所有之前放入队列的待处理的任务,然后一一执行。

转载整理自 : ibireme 的博客 深入理解RunLoop