普通视图

发现新文章,点击刷新页面。
今天 — 2026年3月7日首页

iOS 知识点 - 渲染机制、动画、卡顿小集合

作者 齐生1
2026年3月6日 17:53

一、基本骨架

从代码到像素,都经历了什么?一帧画面是怎么到屏幕上的?

┌──────────────────────────────────────────────────────────────────────────────┐
│                     一帧的完整生命周期 (Render Loop)                            │
│                                                                              │
│   VSYNCVSYNCVSYNC₃        │
│     │                               │                             │          │
│     │  ┌─────────── App 进程 ───────────────┐                      │          │
│     │  │ ① Handle EventCommit Transaction│                   │          │
│     │  │   (触摸/定时器)    ┌────────────────┐  │                   │          │
│     │  │                   │LayoutDisplay ││                   │          │
│     │  │                   │PreparePackage││                   │          │
│     │  │                   └────────────────┘│                    │          │
│     │  └──────────┬──────────────────────────┘                    │          │
│     │             │ Layer Tree 发送                                │          │
│     │             ▼                                               │          │
│     │  ┌─────────── Render Server (独立进程) ─────┐                 │          │
│     │  │ ③ Render PrepareRender Execute(GPU)│              │          │
│     │  │  (编译绘制指令)       (逐层合成到纹理)      │                 │          │
│     │  └──────────────────────────┬────────────────┘              │          │
│     │                             │ 最终纹理就绪                    │          │
│     │                             ▼                               │          │
│     │                          ┌──────────────────┐               │          │
│     │                          │ ⑤ Display/硬件合成│◀─── 帧上屏 ────│          │
│     │                          └──────────────────┘               │          │
│     │                                                             │          │
│     │◀─── 1 frame (16.67ms @60Hz / 8.33ms @120Hz) ──►│            │          │
│     │◀──────────── 2 frames: 事件到上屏的最小延迟 (Double Buffering) ──────►│   │
│                                                                              │
├──────────────────────────────────────────────────────────────────────────────┤
│                                                                              │
│   超时在 App 端 (①②)  ──→  Hang (卡顿/无响应)  +  Commit Hitch (掉帧)          │
│   超时在 GPU 端 (③④)  ──→  Render Hitch (掉帧/动画抖动)                        │
│                                                                              │
│   Hang:  主线程被占 > 250ms,用户感知 "按不动" "界面冻结"                          │
│   Hitch: 帧未在 VSYNC deadline 前就绪,用户感知 "动画跳了一下"                     │
│                                                                              │
│   ┌──────────┐  ┌──────────┐  ┌──────────────┐  ┌───────────────────┐        │
│   │CPU: Event│→ │CPU: Commit│→│GPU: Render   │→ │Hardware: Display  │        │
│   │ 事件处理  │  │ 提交变更   │  │ 合成 + 离屏   │  │ 像素点亮           │        │
│   └──────────┘  └──────────┘  └──────────────┘  └───────────────────┘        │
└──────────────────────────────────────────────────────────────────────────────┘

渲染机制定义了每一帧画面从产生到上屏幕的流水线

动画是连续多帧的有规律变化,利用渲染流水线实现;

卡顿是流水线中任意环节超时,导致帧被丢弃;

  • 核心概念关系: |概念 | 本质 | 与其他概念的关系 | | -------------- | ------------------------- | ------------------------------ | | Render Loop | 系统以屏幕刷新率(60/120Hz)驱动的持续循环 | 所有可见变化的底层引擎 | | CALayer | 视觉内容的载体,持有位图和属性 | 动画的作用对象,渲染的输入 | | Core Animation | 动画+渲染的基础框架 | 管理 Layer Tree,驱动 Render Server | | 动画 (Animation) | 属性随时间的插值变化 | 在 Render Loop 中被逐帧求值 | | Hang(卡顿) | 主线程被占用导致事件无法及时处理 | 用户感知为"按不动""无反应" | | Hitch(掉帧) | 某帧未能在 VSYNC 截止时间前就绪 | 用户感知为动画跳跃、滚动卡顿|

二、Render Loop(渲染循环):渲染机制拆解,一帧是怎样诞生的

2.1 五个阶段

Rnder Loop 是一个以 VSYNC 为节拍、流水线式并行的循环。在 Double Buffering 模式下,一帧从事件到上屏幕需要经过 2 个 VSYNC 周期。

                    VSYNC₁               VSYNC₂              VSYNC₃
                      │                    │                   │
  ┌─── App 进程 ───────┤                    │                   │
  │  ① Event Phase   │                    │                   │
  │  ② Commit Phase  │                    │                   │
  └───────────────────┤                    │                   │
                      │  ┌─ Render Server──┤                   │
                      │  │ ③ Prepare Phase │                   │
                      │  │ ④ Execute Phase │                   │
                      │  └─────────────────┤                   │
                      │                    │  ⑤ Display       │
                      │                    │     帧上屏         │
阶段 进程 做什么 关键耗时原因
Event App 接收触摸、定时器等事件,决定 UI 是否需要变化
Commit App Layout → Display(drawRect) → Prepare(图片解码) → 打包 Layer Tree 发给 Render Server 布局复杂、视图层级深、大图解码
Render Prepare Render Server 遍历 Layer Tree,编译为 GPU 绘制指令流水线 Layer 数量多、需要 Offscreen Pass
Render Execute GPU 逐层合成到最终纹理 Offscreen Pass、大面积模糊/阴影
Display 硬件 把纹理推上屏幕

2.2 Commit 阶段的四个子步骤

Commit 是 App 端最关键的阶段,它本身又分为四步:

Commit Transaction
  │
  ├─ 1. Layout        调用 layoutSubviews / SwiftUI body
  │                    → setNeedsLayout 触发
  │
  ├─ 2. Display       调用 drawRect / draw(_:)
  │                    → setNeedsDisplay 触发
  │                    → 生成 backing store (位图)
  │
  ├─ 3. Prepare        图片解码 + 色彩空间转换
  │                    → 大图 / 非标准格式图开销大
  │
  └─ 4. Package        递归打包 Layer Tree 发送
                       → 层级越深越慢
  • Commit Transaction 是一个 RunLoop 循环结束时自动提交的隐式事务;
  • Backing Store 是 Layer 的位图缓存。

2.3 Double Buffering 与 Triple Buffering

Double Buffering(双缓冲) 是 iOS 渲染流水线的默认工作模式,指系统同时维护 两个帧缓冲区,让 App 准备下一帧和屏幕显示当前帧可以并行进行,互不干扰。

  • Double Buffering(默认):App 和 Render Server 各占一个 VSYNC 周期,总延迟 2 帧。
  • Triple Buffering(降级模式):当 Render Server 来不及时,系统自动切换,给 Render Server 多一帧的时间。帧延迟增加到 3 帧,但能避免更严重的掉帧。

为什么需要缓冲区?

如果只有一个缓冲区(Single Buffering),屏幕 正在读取这个缓冲区显示画面 的同时,GPU 也在往里写新内容,就会出现 画面撕裂(Screen Tearing)——上半截是旧帧,下半截是新帧。


三、CALayer 与三棵树:动画的根基

3.1 Layer 是什么?

CALayer 是一个 模型对象,它不做绘制,它只持有:

  • 几何信息: bounds / position / transform / anchorPoint
  • 视觉属性: backgroundColor / opacity / cornerRadius / shadow
  • 内容: contents 位图

View 和 Layer 的关系: iOS 上每个 UIView 都自动持有一个 backing layer。View 负责事件响应(触摸、手势)和响应链,Layer 负责视觉呈现。你改 view.frame 其实改的是view.layer 的属性。Layer 不处理事件、不参与响应链。

3.2 三棵 Layer Tree

┌─────────────┐    ┌──────────────────┐    ┌─────────────┐
│  Model Tree │    │ Presentation Tree│    │ Render Tree │
│  (图层树)    │    │   (呈现树)        │    │  (渲染树)    │
│             │    │                  │    │             │
│ 你代码改的值  │    │ 动画进行中的当前值  │    │ 实际渲染用    │
│ = 动画目标值  │    │ = 屏幕上的即时值   │    │  (私有,不可访问)│
└─────────────┘    └──────────────────┘    └─────────────┘
       │                    ▲
       │    layer.presentationLayer
       └────────────────────┘
  • Model Tree(图层树): 比如 layer.position = newPos 改的就是它。它始终保存 “最终目标值”。
  • Presentation Tree(呈现树): 动画进行时,layer 实际所在位置是 layer.presentationLayer
  • Render Tree(渲染树): Core Animation 内部使用,无法访问。

关键推论: 给 layer 加动画后,Model Tree 里的值就已经是终点值了。动画结束后如果 removedOnCompletion = YES(默认),layer 就直接呈现 Model Tree 的值。如果你没改 Model Tree 的值,layer 就会"跳回去"——这就是动画结束后 layer 回到原位的经典问题。

presentationLayer 的使用场景: 用户在动画飞行途中点击/拖拽 layer 时,需要用 presentationLayer 获取当前真实位置来做 hitTest 或启动新动画。

3.3 CATransaction - 变更打包器

所有对 Layer 的属性修改都被 CATransaction 捕获:

  • 隐式事务: 哪怕不写 begin/commit,系统也会在每个 RunLoop 循环自动包裹一次。
  • 显式事务: [CATransaction begion] ... [CATransaction commit],可以控制动画时长、completionBlock 等。

隐式事务是 UIView 隐式动画(改 layer 属性自动产生 0.25s 动画)的底层机制。UIView 的 animateWithDuration: 本质上就是开一个显式事务并配置参数。


四、动画系统

动画 = 内容(什么在变) + 时间(多久完成) + 变化规律(怎么变)

要素 对应 API 说明
内容 keyPath (如 position, opacity, transform.rotation.z) 必须是 CALayer 上标记为 Animatable 的属性
时间 duration + timingFunction timingFunction 控制"时间的流速"(加速/减速/弹性)
变化规律 动画子类决定(Basic = 两点插值,Keyframe = 多点插值,Spring = 弹簧物理)
  • 动画类的继承体系:
CAAnimation (基类:timingFunction, delegate, removedOnCompletion)
  │
  ├─ CAPropertyAnimation (抽象:keyPath, additive, cumulative)
  │    │
  │    ├─ CABasicAnimation (fromValue / toValue / byValue)
  │    │    │
  │    │    └─ CASpringAnimation (mass / stiffness / damping / initialVelocity)
  │    │
  │    └─ CAKeyframeAnimation (values / keyTimes / path / calculationMode)
  │
  ├─ CATransition (type / subtype — 转场快照动画)
  │
  └─ CAAnimationGroup (animations[] — 组合多个动画)

4.1 CABasicAnimation — 两点插值

提供起止状态,系统通过插值(Interpolation)算出任意时刻的值。三个属性的语义:

  • fromValue:起始值(绝对值)
  • toValue:结束值(绝对值)
  • byValue:变化量(相对值,"变化了多少")

4.2 CAKeyframeAnimation — 多点插值

关键帧动画 = N 段 BasicAnimation 的串联。提供一组 values 和对应的 keyTimes(归一化 0~1),系统在相邻关键帧之间插值。

calculationMode 决定插值方式:linear(默认)

4.3 CASpringAnimation — 弹簧物理

继承自 CABasicAnimation,用弹簧力学模型驱动动画曲线:mass(质量越大,运动越慢,但衰减也越慢)等。

4.4 CATransition — 两张快照之间的过渡

CATransition 不指定 from/to 值。它的工作方式:

  1. 把动画添加到 layer 时,拍下当前 layer 的快照(开始状态)
  2. 紧接着你对 layer 做修改(比如替换子视图、改文字)
  3. 修改后的 layer 是结束状态
  4. 系统在两张快照之间播放指定的过渡效果

4.5 CAAnimationGroup — 组合动画

把多个动画放在 animations 数组里同时执行。注意:

  • Group 的 duration 是一个 硬截止:到时间所有子动画停止,不管子动画是否结束。
  • 各子动画独立执行,不互相等待。

五、Hang(卡顿/无响应):主线程被占的代价

Hang = 主线程无法在合理时间内处理用户事件。

WWDC23 统计过一期人类感知阈值,大概如下:

  0ms          100ms         250ms         500ms
   │─── 感觉即时 ──│── 微妙可感 ──│── 明显延迟 ──│── 严重卡顿 ──▶
                    │              │              │
              目标上限   Micro Hang(系统开始上报)    Hang

5.1 Hang 的三种类型

类型 主线程 CPU 表现 典型原因
Busy Main Thread 高(60~100%) 主线程在拼命算东西 大量布局计算、同步图片处理、JSON 解析
Blocked Main Thread 极低(~0%) 主线程在等锁/等IO/等网络 同步网络请求、信号量等待、锁竞争、同步文件IO
Asynchronous Hang 可高可低 不是当前事件导致的,而是之前调度到主线程的任务占了时间 dispatch_async(main) 的耗时任务、@MainActor 下的同步代码
同步 Hang:
  用户点击 → [────── 主线程处理耗时 ──────] → 响应
              ←─── 这段就是 hang ───→

异步 Hang:
  之前调度的任务 → [──── 主线程被占 ────]
                           ↑ 用户点击来了,但得排队
                           ←── 这段是 hang ──→

5.2 Swift Concurrency 中的陷阱

WWDC23 Session 10248 中详细阐述的一个经典问题:

struct BackgroundThumbnailView: View {
    var body: some View {  // body 隐式继承 @MainActor
        ProgressView()
            .task {  // .task 闭包继承外部 actor 隔离 → 也在 MainActor
                image = background.thumbnail  // 同步属性 → 在主线程执行!
            }
    }
}
  • 问题: .task 闭包继承 body@MainActor 隔离,同步属性 thumbnail 在主线程执行。await 只在调用 async 函数时才切换线程。
  • 解法: 把 thumbnail 改为 async getter,使其能在 Cooperative Thread Pool 上执行:
public var thumbnail: UIImage {
    get async { /* compute and cache */ }
}
// 使用处
.task {
    image = await background.thumbnail  // 现在能离开 @MainActor 了
}

六、Hitch(掉帧):动画不流畅的元凶

Hitch = 某一帧没能在 VSYNC deadline 前就绪,导致前一帧重复显示。

单次 hitch time(毫秒)不方便跨测试对比。Apple 定义了 Hitch Time Ratio:

Hitch Time Ratio = 总 hitch 时间 / 总持续时间   (单位: ms/s)
等级 Hitch Time Ratio 用户感知
Good < 5 ms/s 基本无感
Warning 5~10 ms/s 能注意到部分中断
Critical > 10 ms/s 严重影响体验,必须立即修复

6.1 Hitch 的两种类型

类型 超时发生在 常见原因
Commit Hitch App 端 Commit 阶段 复杂布局、drawRect 耗时、大图解码、深层级打包
Render Hitch Render Server / GPU Offscreen Pass 过多、大面积模糊/阴影、复杂遮罩

6.2 Offscreen Pass(离屏渲染)—— Render Hitch 的主要元凶

  • 当屏渲染:GPU 的任务是把所有 layer 从后往前逐个画到一块最终纹理上(就是你屏幕看到的那一帧画面):
最终纹理(屏幕画面)
┌──────────────────┐
│                  │
│  第1层:蓝色背景   │  ← GPU 先画这个
│  第2层:白色卡片   │  ← 再叠上这个
│  第3层:文字       │  ← 最后叠上这个
│                  │
└──────────────────┘

GPU 直接在最终纹理上一层层往上画,画完就上屏。
这就是"正常渲染",也叫"当屏渲染"
  • 离屏渲染:GPU 无法直接在最终纹理上绘制某个 layer,必须先在 离屏纹理 上画好再拷贝回来。每次 Offscreen Pass 都是额外的 纹理切换 + 像素拷贝

    • 为什么无法直接在最终纹理上绘制?
      • 如下图,阴影其实在 “最底层”,要先画;
      • 但是阴影的形状取决于 “上层的圆形和长条”,还没画呢。
      ┌─────────────────────────────────┐
      │         最终纹理                  │
      │                                 │
      │        ●●●●●                    │
      │       ●●●●●●●   ← 圆形          │
      │        ●●●●●                    │
      │       ████████  ← 长条           │
      │                                 │
      │  阴影的形状 = 圆形+长条的轮廓       │
      │  但 GPU 还没画圆形和长条呢!        │
      │  它怎么知道阴影该长什么样?          │
      └─────────────────────────────────┘
      
      • 解决办法 = 离屏渲染:
      步骤1:GPU 切到临时纹理,先把圆形和长条画上去
      ┌── 临时纹理 ──┐
      │    ●●●●●     │
      │   ●●●●●●●   │  → 现在知道轮廓了
      │    ●●●●●     │
      │   ████████   │
      └──────────────┘
      
      步骤2:把轮廓变黑 + 模糊 = 阴影形状
      ┌── 临时纹理 ──┐
      │   ░░░░░░░    │
      │  ░░░░░░░░░   │  → 这就是阴影
      │   ░░░░░░░    │
      │  ░░░░░░░░░░  │
      └──────────────┘
      
      步骤3:把阴影拷贝回最终纹理
      
      步骤4:在最终纹理上再画一次圆形和长条(盖在阴影上面)
      
      • 圆形和长条被画了两次,还多了纹理切换和拷贝。这就是离屏渲染慢的原因。
  • 四大触发场景:

场景 为什么必须离屏 怎么避免
阴影 GPU 不知道阴影形状,得先画内容才能反推 设 shadowPath,直接告诉 GPU 形状,不用反推
遮罩 (mask) 先画内容,再用 mask 裁剪,裁掉的像素不能污染最终纹理 用 cornerRadius + masksToBounds 代替自定义 mask layer
圆角 + 裁剪内容 子视图超出圆角范围需要被裁掉,和遮罩同理 确认子视图不超出 bounds 时去掉 masksToBounds
模糊/毛玻璃 需要拷贝底层像素到临时纹理再做模糊 不可避免,控制数量和面积

七、遇到问题怎么查?

用户反馈"卡"
  │
  ├─ 按钮按不动 / 界面冻结 → 这是 Hang
  │    │
  │    ├─ Time Profiler 看 CPU 高 → Busy Main Thread
  │    │    → 减少主线程计算、用 async/await 移到后台
  │    │
  │    └─ Thread States 看线程 Blocked → Blocked Main Thread
  │         → 找到阻塞的系统调用(锁/IO/信号量),异步化
  │
  └─ 滚动/动画跳帧 → 这是 Hitch
       │
       ├─ Animation Hitches 模板看 Commit 阶段超时 → Commit Hitch
       │    → 简化布局、减少 drawRect、预处理图片、扁平化层级
       │
       └─ Render/GPU 阶段超时 → Render Hitch
            → View Debugger 看 offscreen count
            → 设置 shadowPath、用 cornerRadius 代替 mask

八、GPU 优化

  • 图层混合(Blending):当 layer 不是完全不透明时(opacity < 1 或 backgroundColor 为 nil/透明),GPU 需要把当前 layer 和底下的 layer 做像素混合计算。
    • 优化方式:给 view 设不透明背景色、设 opaque = YES、避免不必要的透明。
  • shouldRasterize(光栅化缓存):把一个复杂的 layer 子树一次性渲染成位图缓存,后续帧直接复用。适合内容不常变的复杂视图(如带阴影+圆角+多子视图的卡片)。但缓存有 100ms 未使用自动释放的限制,且 内容变化时需要重新光栅化,用不好反而更慢
  • 像素对齐(Pixel Alignment):frame 的坐标不是整数像素时,GPU 需要做抗锯齿混合。用 CGRectIntegral 或 SnapKit 的 snp.makeConstraints 保持像素对齐。
昨天以前首页

iOS 知识点 - IAP 是怎样的?

作者 齐生1
2026年3月4日 20:04

一、IAP 基本概念

  • 定义: In-App Purchase 是苹果提供的支付机制,允许用户在 App 内购买虚拟商品或订阅服务,所有数字内容和服务的交易必须通过 IAP 完成(苹果会抽成 15%~30%),否则会拒审。

  • 类型:

类型 描述 使用场景 特点
Consumable(消耗型) 用完即消失,可反复购买 金币、体力、道具 不可恢复,需自己记录消耗
Non-Consumable(非消耗型) 永久可用,购买一次即可 解锁关卡、去广告、付费功能 可恢复,支持跨设备恢复
Auto-Renewable Subscription(自动续订订阅) 周期性付费,自动续订 VIP 会员、内容订阅 苹果处理续订、退款、过期提醒
Non-Renewing Subscription(非续订订阅) 周期性付费,但需手动续订 课程、限时会员 不自动续订,需要自己管理过期

消耗型商品必须在验证成功后调 finishTransaction,否则 Apple 会认为你还没发货,下次启动继续提醒。

  • 四个核心名词:

    • 商品(Product):
      • 在 App Store Connect 后台配置的可购买项目。
      • 每个商品都有唯一的 productIdentifier,App 需要用这个 ID 去 Apple 查询商品的实时价格和货币信息,返回类型是 SPProduct(StoreKit 91) 或 Product(StoreKit 2)。
    • 订单(Order):
      • 自己服务器创建的记录。
      • 在调用 Apple 支付之前创建,拿到 order_id,用于后期的对账(钱对应哪个商品、给哪个用户、是购买还是赠送等等)。
      • Apple 不知道这个东西的存在,业务层概念。
    • 交易(Transaction):
      • Apple 侧产生的记录。
      • 在 Apple 的支付弹窗上确认付款后,Apple 会生成一笔交易。
      • 交易有多种状态:
      purchasing(支付中) → purchased(已支付) → finished(已完成)
                        → failed(失败)
                        → deferred(等待家长审批)
                        → restored(恢复购买)
      
    • 收据(Receipt):
      • Apple 侧产生的付款凭证,证明用户确实付了钱。
      • 需要拿该凭证去服务器校验,服务器确认为真后才能发货,有两种格式:
        • StoreKit 1: 一个 Base64 编码的二进制文件(ASN.1),存在 App 沙盒里(Bundle.main.appStoreReceiptURL)。
        • StoreKit 2: 一个 JWS(JSON Web Signature)字符串,带有 Apple 的数字签名,更安全,不存本地,通过 Transaction API 获取。
  • 流程概述:

用户点击购买
  │
  ├─ ① App 用 productIdentifier 向 Apple 查询商品信息
  │     └─ Apple 返回 Product(价格、货币、描述)
  │
  ├─ ② App 向自己服务器创建订单,拿到 order_id
  │
  ├─ ③ 调用购买 API,Apple 弹出支付确认框
  │     └─ 用户输入密码 / Face ID / Touch ID
  │
  ├─ ④ Apple 返回交易结果(Transaction)
  │     ├─ purchased → 继续验证
  │     ├─ failed → 提示用户
  │     └─ deferred → 等待家长审批
  │
  ├─ ⑤ 拿 receipt/transaction 发给自己服务器验证
  │     └─ 服务器向 Apple 验证真伪(或本地验签 JWS)
  │     └─ 服务器确认后发货(加金币/开会员/解锁功能)
  │
  └─ ⑥ 调用 finishTransaction,告诉 Apple "我已发货"

二、StoreKit 1 与 StoreKit 2

维度 StoreKit 1(已废弃,但仍可用) StoreKit 2(推荐)
语言 Objective-C / Swift 均可 Swift only
异步模式 Delegate 回调 async/await
最低版本 iOS 3+ iOS 15+
交易监听 SKPaymentTransactionObserver Transaction.updates(AsyncSequence)
恢复购买 手动调 restoreCompletedTransactions() 自动可用,Transaction.currentEntitlements
收据验证 /verifyReceipt(服务端,已废弃) JWS 本地验签 / App Store Server API
订阅状态 自己解析 receipt 推算 Product.SubscriptionInfo.status 直接获取
退款处理 收不到通知 Transaction.revocationDate 有值 = 已退款
家庭共享 不支持 内置支持

2.1 StoreKit 1 核心类

SKProductsRequest          → 请求商品信息
  └─ SKProductsRequestDelegate  → 回调商品列表
       └─ SKProduct            → 单个商品(价格、标题、描述)

SKPayment                  → 支付请求对象
SKPaymentQueue             → 交易队列(单例)
  └─ SKPaymentTransactionObserver  → 交易状态回调
       └─ SKPaymentTransaction     → 单笔交易(状态、receipt)

SKReceiptRefreshRequest    → 强制刷新本地收据
  • StoreKit 1 典型代码流程:
// 1. 查询商品
let request = SKProductsRequest(productIdentifiers: ["com.app.coin100"])
request.delegate = self
request.start()

// 2. 收到商品信息
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    let product = response.products.first!
    // 展示价格:product.localizedTitle, product.price
}

// 3. 发起购买
let payment = SKPayment(product: product)
SKPaymentQueue.default().add(payment)

// 4. 监听交易状态
func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    for tx in transactions {
        switch tx.transactionState {
        case .purchased:
            // 验证收据 → 发货 → finish
            verifyAndDeliver(tx)
            queue.finishTransaction(tx)
        case .failed:
            queue.finishTransaction(tx)
        case .restored:
            queue.finishTransaction(tx)
        case .deferred, .purchasing:
        break
        }
    }
}

2.2 StoreKit 2 核心类

Product                         → 商品(静态方法 .products(for:) 查询)
Product.purchase()              → 发起购买,返回 PurchaseResult
Transaction                     → 交易(自带签名验证)
Transaction.updates             → AsyncSequence,监听新交易
Transaction.currentEntitlements → 当前有效权益(自动含恢复)
Transaction.finish()            → 确认已发货
Product.SubscriptionInfo        → 订阅状态(续订、过期、宽限期)
  • StoreKit 2 典型代码流程:
// 1. 查询商品
let products = try await Product.products(for: ["com.app.coin100"])
let product = products.first!

// 2. 发起购买
let result = try await product.purchase()

switch result {
case .success(let verification):
    // 3. 验证交易
    switch verification {
    case .verified(let transaction):
        // 发货
        await deliverContent(transaction)
        // 4. 告诉 Apple 已发货
        await transaction.finish()
    case .unverified(_, let error):
        // 签名验证失败,可能被篡改
        handleError(error)
    }
case .userCancelled:
    break
case .pending:
    // 等待家长审批 / 支付确认
    break
}

// 5. App 启动时监听未完成的交易
Task {
    for await result in Transaction.updates {
        if case .verified(let transaction) = result {
            await deliverContent(transaction)
            await transaction.finish()
        }
    }
}

三、购买流程

3.1 正常购买流程

    App                     Apple                   自己服务器
     │                        │                        │
     │──Product.products()──> │                        │
     │<──返回商品信息─────────  │                        │
     │                        │                        │
     │──创建订单────────────────────────────────────────>│
     │<──返回 order_id──────────────────────────────────│
     │                        │                        │
     │──product.purchase()──> │                        │
     │    (用户确认支付)        │                        │
     │<──Transaction───────── │                        │
     │                        │                        │
     │──发送 transaction/receipt 验证─────────────────> │
     │                        │          │──验证签名/调 Apple API──>│
     │                        │          │<──确认有效──────────────│
     │<──验证通过,已发货─────────────────────────────────│
     │                        │                        │
     │──transaction.finish()─>│                        │
     │                        │                        │

3.2 异常场景处理

场景 现象 处理方式
支付成功但 App 崩溃/网络断开 没调 finish,下次启动 Apple 会重新推送交易 App 启动时监听 Transaction.updates,重新验证并发货
用户取消支付 result == .userCancelled 不做任何处理
家庭共享/家长审批 result == .pending 提示用户等待,后续通过 Transaction.updates 接收结果
重复购买(消耗型) 正常,消耗型可重复买 每次都走完整验证流程
重复购买(非消耗型) Apple 提示"你已购买过" 不会重复扣费,返回原始交易
退款 Apple 后台处理 服务端收到 S2S 通知 REFUND,或客户端检查 transaction.revocationDate
掉单(钱扣了但没收到交易) 极少见,通常是网络问题 服务端定期用 App Store Server API 查询用户交易历史

3.3 收据验证

客户端验证 服务端验证(推荐)
安全性 低,可被越狱设备绕过 高,服务器可信环境
实现复杂度 简单 需要后端配合
适用场景 个人开发者、低价值商品 商业应用、涉及虚拟货币

四、服务端通知(Server-to-Server Notifications)

Apple 会主动推送事件到你配置的服务器 URL(在 App Store Connect 中设置)。

4.1 主要通知类型

通知类型 含义
SUBSCRIBED 用户首次订阅
DID_RENEW 自动续订成功
EXPIRED 订阅过期
DID_FAIL_TO_RENEW 续订失败(信用卡过期等)
GRACE_PERIOD_EXPIRED 宽限期结束
REFUND 用户退款
REVOKE 家庭共享撤销
CONSUMPTION_REQUEST Apple 要求你提供消耗信息(用于退款裁决)
ONE_TIME_CHARGE 一次性购买通知(2025 新增)

4.2 通知格式

{
  "signedPayload": "<JWS 字符串>"
}

解码 JWS 后得到:

{
  "notificationType": "DID_RENEW",
  "subtype": "AUTO_RENEW",
  "data": {
    "signedTransactionInfo": "<JWS>",
    "signedRenewalInfo": "<JWS>"
  }
}

五、App Store Connect 配置

商品要在 App Store Connect 对应 App 的 App 内购买项目中配置并审核。

  • 沙盒测试:
    • App Store Connect → 用户和访问 → 沙盒测试员 → 添加测试 Apple ID
    • 手机 App Store 登入沙盒账号,测试环境会使用沙盒账号模拟支付。

六、常见的架构策略

6.1 项目分层

View 层
  └─ 购买按钮、价格展示、订阅状态 UI

ViewModel / Manager 层
  └─ IAPManager(单例)
     ├─ 查询商品
     ├─ 发起购买
     ├─ 监听交易
     └─ 管理订阅状态

Service 层
  └─ IAPService(与服务器交互)
     ├─ 创建订单
     ├─ 验证收据
     └─ 查询购买记录

Apple 层
  └─ StoreKit 2 API

6.2 防掉单策略

                    正常流程                         异常恢复
                  ┌──────────┐                   ┌──────────────┐
用户购买  ───────> │ 服务器验证 │──> 发货            │ App 启动      │
                  │ + finish │                   │ Transaction   │
                  └──────────┘                   │ .updates 推送 │
                                                 │ 未 finish 的  │
                                                 │ 交易          │──> 重新验证发货
                                                 └──────────────┘

服务端兜底:
  - 定期调 App Store Server API 查用户交易历史
  - 对比自己数据库,找出 Apple 有但自己没发货的交易
  - 补发

七、一些 2025 年新动向

  • StoreKit 1 已被标记为 Deprecated(WWDC 2024),虽然仍能用,但新项目应该用 StoreKit 2
  • /verifyReceipt 接口已废弃,改用 App Store Server API
  • S2S Notifications V1 已废弃,改用 V2
  • iOS 18.4+ 新增:
    • appTransactionID:每个 Apple 账户唯一标识
    • Offer Codes 扩展到消耗型/非消耗型商品(之前只支持订阅)
    • Advanced Commerce API:支持复杂的订阅附加组件
  • Xcode 16.2+ 必须升级,否则 iOS 18.2 上可能出现购买失败

网络知识点 - TCP/IP 四层模型知识大扫盲

作者 齐生1
2026年2月26日 11:46

一、计网基础概念

第一章先总体回顾一下

1.1 OSI 七层模型与 TCP/IP 四层模型

OSI 模型 TCP/IP 核心职责 常见协议 iOS 关联
7.应用层 4. 应用层 提供应用服务接口,定义数据格式与交互语义 HTTP, HTTPS, DNS, WebSocket NSURL、URLSession
6.表示层 (并入应用层) 加密、压缩、格式转化 SSL/TLS, JSON, JPEG, UTF-8
5.会话层 (并入应用层) 会话管理、状态保持 TLS 握手、RPC、Session
4.传输层 3. 传输层 端到端通信、可靠传输 TCP、UDP、QUIC Socket
3.网络层 2. 网络层 路由寻址、分片 IP、路由、ICMP、ARP
2.数据链路层 1. 网络接口层 MAC寻址、帧封装 MAC、Wi-Fi、Ethernet
1.物理层 (并入网络接口层) 比特流传输 光线、电缆、无线信号

网络通信是一条从 应用层 → 内核网络栈 → 网络接口 → 传输介质 → 对端主机 的完整路径。

1.2 数据封装与解封装

数据在网络中是如何传递的?

  • 以 TCP 为例:
你的 App 数据:{"name":"张三"}
      │
      ▼ ④ 应用层:加 HTTP 头(方法、路径、Host、…)
┌──────────────────────────────────────┐
│ HTTP Header │ JSON: {"name":"张三"}   │ ← 消息(Message)
└──────────────────────────────────────┘
      │
      ▼ ③ 传输层:加 TCP 头(源端口、目的端口、序列号、…)
┌────────────────────────────────────┐
│ TCP Header │ HTTP Header │ JSON    │ ← 段(Segment)
└────────────────────────────────────┘
      │
      ▼ ② 网络层:加 IP 头(源IP、目的IP、TTL、协议号=6)
┌──────────────────────────────────────────────┐
│ IP Header │ TCP Header │ HTTP Header │ JSON  │ ← 包(Packet)
└──────────────────────────────────────────────┘
      │
      ▼ ① 网络接口层:加 MAC 头(源MAC、目的MAC) + 尾部校验(FCS)
┌────────────────────────────────────────────────────────────────┐
│ MAC Header │ IP Header │ TCP Header │ HTTP Header │ JSON │ FCS │ ← 帧(Frame)
└────────────────────────────────────────────────────────────────┘
  • 以 UDP 为例:
你的 App 数据:{"query":"example.com"}
      │
      ▼ ④ 应用层:加上应用层协议头(例如 DNS / 自定义协议)
┌──────────────────────────────────────────────┐
│ DNS Header │ JSON: {"query":"example.com"}   │ ← 消息(Message)
└──────────────────────────────────────────────┘
      │
      ▼ ③ 传输层:加 UDP 头(源端口、目的端口、长度、校验和)
┌──────────────────────────────────────────────┐
│ UDP Header │ DNS Header │ JSON               │ ← 数据报(Datagram)
└──────────────────────────────────────────────┘
      │
      ▼ ② 网络层:加 IP 头(源IP、目的IP、TTL、协议号=17)
┌────────────────────────────────────────────────────────┐
│ IP Header │ UDP Header │ DNS Header │ JSON             │ ← 包(Packet)
└────────────────────────────────────────────────────────┘
      │
      ▼ ① 网络接口层:加 MAC 头(源MAC、目的MAC) + 尾部校验(FCS)
┌────────────────────────────────────────────────────────────────────────────┐
│ MAC Header │ IP Header │ UDP Header │ DNS Header │       JSON       │  FCS │ ← 帧(Frame)
└────────────────────────────────────────────────────────────────────────────┘

接下来按照数据封装的顺序,依次回顾一下各个层级的知识。


二、应用层(应用层 + 表示层 + 会话层)

App 层逻辑,包括 DNS、HTTP、HTTPS、WebSocket、缓存、认证体系。

2.1 DNS 域名解析

作用:  将域名 https://some.com 解析为 IP 地址,是一切请求的起点。

  • 从 输入网址 -> 页面返回,过程是怎样的?

    • 解析顺序: 浏览器缓存 → OS 缓存 → hosts → 本地 DNS → 根服务器 → 顶级域服务器 → 权威服务器;
    • 返回 IP 后进行 TCP(三次握手)→ TLS 握手 → HTTP 请求。
    ┌──────────────────────────────────────┐
    │   访问 https://api.example.com/path   │
    └──────────────────────────────────────┘
    ① 用户输入网址
       └──> 浏览器解析 URL
             协议 = https、主机 = api.example.com、端口 = 443、路径 = /path
    
    ② DNS 解析域名(api.example.com → IP)
       │
       ├─ 1) 查 [本机缓存](浏览器 DNS 缓存 → OS DNS 缓存 → hosts 文件)
       │
       ├─ 2) 未命中 → 请求 [本地 DNS 服务器](路由器 / 运营商)
       │
       └─ 3) 仍未命中 → [本地 DNS 服务器] 发起 [迭代查询](由它代跑,浏览器只等结果)
             │
             ├─ 问 [根 DNS](.)
             │  └─ 回答:"去问 .com 的服务器"
             ├─ 问 [顶级域 DNS](.com)
             │  └─ 回答:"去问 example.com 的权威服务器"
             └─ 问 [权威 DNS](example.com)
                └─ 回答:"api.example.com = 203.0.113.8" ✓
                   └─ 本地 DNS 缓存结果(根据 TTL),返回给浏览器            
    
    ③ 建立 TCP 连接(三次握手)
    
    ④ TLS 握手(HTTPS 特有,在 TCP 之上建立加密信道)
    
    ⑤ 发送 HTTP 请求(数据从上到下逐层封装)
       └──> [1.2 数据封装与解封装] 知识点
    
    ⑥ 服务器处理请求并返回响应
       │
       ├─ 1) 解封帧(拆 MAC -> ... -> 拿到 HTTP 请求)
       │
       ├─ 2) 业务处理(路由匹配 -> 鉴权 -> 查库 -> 生成结果)
       │
       └─ 3) 构建 HTTP 响应,逐层封装,发回客户端
    
    ⑦ 浏览器接收响应
    
    ⑧ 连接关闭(四次挥手,或保持复用)
       ├─ HTTP/1.1 Keep-Alive → 连接放入连接池,后续请求复用
       ├─ HTTP/2 → 同一连接上继续多路复用
       └─ 不再需要时 → 四次挥手断开:
    
  • DNS 劫持

    • 现象:
      • App -> 运营商 DNS 服务器(可能被劫持)-> 返回错误的 IP
    • 解决方案:HTTPDNS(绕过运营商,App 直连 DNS 服务)
      • App -> 通过 HTTP 直接请求 HTTPDNS 服务器(绕过运营商)-> 返回正确的 IP

2.2 HTTP 协议

HTTP(80)、HTTPS(443)

HTTP 是一种基于 “请求-响应” 的无状态的应用层协议,每次请求都是独立的。

最初就是为浏览器与 Web 服务器设计的。

2.2.1 HTTP 基本信息

  • HTTP 请求 = 请求行 + 请求头 + 空行 + 请求体
POST /api/users HTTP/1.1                ← 请求行:方法、路径、版本
Host: api.example.com                   ← 请求头:多行参数
...
...
                                        ← 空行:分隔头和体
{"name":"张三","email":"z@example.com"}  ← 请求体
  • HTTP 响应 = 状态行 + 响应头 + 空行 + 响应体
HTTP/1.1 201 Created                     ← 状态行:版本、状态码、描述
Content-Type: application/json           ← 响应头
...
...
                                         ← 空行
{"id":456,"name":"张三","created":true}   ← 响应体


2.2.2 HTTP 常见的请求方法

方法 语义 幂等? 安全? 有请求体? 典型场景
GET 获取资源 通常没有 获取用户列表、详情页
POST 创建资源/提交数据 注册、下单、上传
DELETE 删除资源 通常没有 删除订单
HEAD 同 GET 但只要头部 没有 检查资源是否更新
  • 幂等: 执行 1 次和执行 N 次效果相同。
    • GET 请求 10 次,拿到的是同一份数据,幂等✅;
    • DELETE 删 10 次,资源还是被删了(第 2 次返回 404),幂等✅;
    • 但 POST 创建订单 10 次,可能创建 10 哥订单,不幂等❌;
  • 安全: 不会修改服务器资源。
    • GET、HEAD 不会修改服务器资源,只读,安全✅;
    • POST/DELETE 有写操作,不安全❌;

思考: 实际项目中,为什么大部分是 POST 而非 GET?大部分场景不是只需要 “读” 吗?

  • 为安全、加密、签名、防重放、复杂度等。
  1. 请求体加密

    • 很多项目会对请求参数做 AES 等对称加密,将整个参数序列化后加密放在 request body 中传输。GET 请求没有 body,参数只能拼在 URL query string 里,无法做 body 级别的加密。
  2. 签名机制与 HTTP 方法绑定

    NSString *source = [NSString stringWithFormat:@"POST&%@&%@", pathEncoding, paramEncoding];
    NSString *hash = [self HmacSha1:kAppKey data:source];
    
    • 常见的 API 签名方案会把 HTTP 方法作为签名原文的一部分,客户端和服务端按同一规则生成签名并校验。一旦签名协议绑定了 POST,改用 GET 会导致签名不一致、请求被拒绝。
  3. 公共参数太多,URL 长度受限

    • 每个请求通常会自动携带大量公参(设备信息、版本号、签名、时间戳、MD5、...),GET 的请求参数在 URL 里:
      • URL 容易超长,超出 中间代理/CDN 的长度限制;
      • 参数结构复杂时,编码笨重;
  4. 敏感信息不易暴露在 URL 中

    • GET 请求的参数在 URL 里,会被以下环节明文记录:
      • 服务端 access log
      • CDN 日志
      • 浏览器/WebView历史记录
      • 网络抓包/运营商等
    • 而用户凭证(uid/sid/token)、设备指纹、签名等都是敏感数据。
  5. 防重放机制的需要

    • 为了防止请求被截获后重放,通常每个请求都会带上 nonce(随机数)和 millisecond(时间戳),并参与签名计算,GET 请求的缓存机制反而会造成干扰。
    • POST 天然不会被缓存,与防重放设计更契合。
  6. 统一方案降低复杂度

    • 如果 GET 和 POST 混用,就需要【签名逻辑、加密方案、服务端解析逻辑、网关/中间件的安全策略】 等都需要区分两套,维护成本升高。

2.2.3 HTTP 常见的状态码

|状态码 | 说明 |
| --- |  -- |
|`1xx`(上传前试探)|`100`: 服务器说"继续发吧",用于大文件上传前的试探<br>`101`: 协议升级,WebSocket 握手就用这个|
|`2xx`(成功)|`200`: 最常见的 OK|
|`3xx`(重定向)|`301`: 永久重定向|
|`4xx`(客户端错误)|`400`: 请求格式错误<br>`401`: 没登录或Token过期<br>`403`: 登录了但没权限<404>资源不存在|
|`5xx`(服务端错误)|`500`: 服务器崩了|

2.3 HTTP 缓存机制

HTTP 缓存分为两阶段机制: 强缓存(freshness)→ 协商缓存(validation)

“先看时间(强缓存),再问服务器(协商缓存)。”

  1. 首次请求
    • 服务器返回资源 + 缓存控制信息(如 Cache-Control, ETag, Last-Modified)。
  2. 强缓存阶段(freshness)
    • 客户端检查本地缓存是否仍在有效期(由 Cache-Control: max-age 或 Expires 判断)。
    • 若未过期 → 直接使用本地副本,不访问服务器。
  3. 协商缓存阶段(validation)
    • 强缓存过期或被标记需验证,则 客户端带验证头请求服务器
      • If-None-Match: <etag>
      • 或 If-Modified-Since: <time>
    • 服务器判断资源是否变化:
      • 未变化 → 304 Not Modified(仅返回头部,客户端复用旧内容);
      • 已变化 → 200 OK(新资源内容)。
  • 常见 Header:
分类 Header 作用
强缓存 Cache-Control: max-age=3600 指定可直接使用的秒数
Expires: 老式写法,被 Cache-Control 覆盖
协商缓存 ETag / If-None-Match 内容标签验证
Last-Modified / If-Modified-Since 修改时间验证
其他 Vary 声明缓存与哪些请求头有关
private / public 是否允许代理缓存
  • 常见配置:
场景 Header 示例
静态资源 Cache-Control: public, max-age=31536000, immutable
动态接口 Cache-Control: no-cache + ETag
敏感数据 Cache-Control: no-store

2.4 Cookie、Session、Token —— 认证三兄弟

HTTP 是无状态协议 ———— 服务器不记得你是谁。每次请求都是独立的,但是有很多场景需要 “记住用户”(登录态、用户身份等),于是就有了他们仨。

名称 存储位置 主要作用 特点
Cookie 浏览器 / 客户端 存放少量数据,携带 Session ID 每次请求自动携带到服务器
Session 服务端 保存用户状态(如登录态) 有状态,需要共享。
依赖 Cookie 或 URL 中的 Session ID
Token 客户端 身份凭证(常为加密签名) 服务端无状态验证,跨端通用

Web 用 Session,App 常用 Token 鉴权。

  • Cookie

    • 本质: HTTP 头中由服务器通过 Set-Cookie 下发的 键值对。客户端保存后,在后续请求 自动携带 Cookie 头。
    • 示例:
      • Set-Cookie: session_id=abc123; HttpOnly; Secure; Max-Age=3600
      • Cookie: session_id=abc123
  • Session(服务端状态)

    • 流程:

      1. 用户登录 -> 服务端验证成功,生成唯一 Session ID;
      2. 服务端保存登录信息(uid、权限等)到内存或 redis;
      3. Session ID 下发给客户端(通常通过 Cookie);
      4. 后续请求客户端自动携带 Session ID, 服务器查表恢复状态。
    • 特点:

      • 状态保存在服务器(有状态);
      • 适合小规模 / 单机服务;
      • 分布式时需要共享 Session (如 Redis 集中存储)。
  • Token(无状态身份验证)

    • 原理: 服务端不保存状态,只验证 Token 的合法性。
    • 常见类型:
      • JWT(JSON Web Token):Header.Payload.Signature 三段式 Base64 编码。
    • 验证流程:
      1. 登录成功后生成 Token(带用户信息 + 过期时间 + 签名)。
      2. 客户端保存(如 iOS Keychain).
      3. 每次请求带头部: Authorization: Bearer <token>
      4. 服务端验签(是否过期、签名是否匹配)。
    • 特点:
      • 无需服务器保存状态;
      • 一旦签发,撤销复杂(需要黑名单机制);
      • Token 通常短期有效,需要配合 Refresh Token 使用。

Session 与 Token 对比

项目 Session Token
状态保存 服务端 客户端(无状态)
可扩展性 弱(需共享 Session) 强(验证即可)
安全性 依赖 Cookie 保护 依赖签名/加密
登出控制 服务端可立即失效 需黑名单或等待过期
常见场景 Web 登录态 移动端 / API 鉴权

2.5 HTTPS 与 TLS

TLS 是 SSL 的后继协议,SSL 已被淘汰。

HTTPS = HTTP + TLS。TLS 在 TCP 之上、HTTP 之下,提供三大安全保障:

保障 含义 实现方式
加密(Encryption) 防窃听 对称加密(AES)
认证(Authentication) 防伪造 数字证书 + CA 体系
完整性(Integrity) 防篡改 MAC(消息认证码)

HTTPS = HTTP + 加密 + 认证 + 完整性

TLS 握手流程:

image.png

  1. 客户端发送 随机数与算法列表
  2. 服务端返回 证书与随机数
  3. 客户端 验证证书,生成 会话密钥
  4. 使用 非对称算法 安全交换 对称密钥,后续双方使用 对称密钥加密通信

image.pngTLS 1.3 优化了流程:

  • 握手仅需 1-RTT(一次往返),更快;
  • 默认强加密算法(如 AES-GCM、ChaCha20);
  • 支持 0-RTT 快速重连。

iOS 强制使用 TLS1.2+(ATS),支持证书 Pinning。

类型 用途 特点
非对称加密(RSA、ECDHE) 握手阶段,用于密钥协商 安全但慢
对称加密(AES、ChaCha20) 传输阶段,用同一密钥加解密数据 快速

2.6 WebSocket 协议

HTTP 是"你问我答"(请求-响应)模式。客户端不问,服务器不答。但很多场景需要服务器 主动推送.

  • 在 WebSocket 之前,人们用各种"土办法"模拟:
    方案 原理 缺点
    轮询(Polling) 客户端定时发 HTTP 请求(如每 3 秒一次) 浪费带宽和电量,延迟高
    长轮询(Long Polling) 客户端发请求,服务器有数据时才响应,否则挂起直到超时 服务器资源开销大
    SSE(Server-Sent Events) 服务器单向推送 只能服务器→客户端,不支持双向

而 WebSocket 才是真正的解决方案:全双工、持久连接、双方可以随时发数据。

  • iOS 实践:  使用 URLSessionWebSocketTask。

WebSocket 握手

  • 本质: 就握手本质就是一个 HTTP 请求,只不过请求目的不是获取数据,而是 请求协议升级
  • 步骤:
    1. 客户端发送一个特殊的 HTTP 请求 GET /chat HTTP/1.1 ← 还是普通的 HTTP 请求 Host: server.example.com Upgrade: websocket ← "我想升级为 WebSocket 协议" Connection: Upgrade ← "这是一个升级请求" Sec-WebSocket-Key: dGhlIHNhbXBsZQ== ← 随机 Base64 值(防伪造) Sec-WebSocket-Version: 13 ← WebSocket 协议版本 Origin: example.com ← 来源(可选,用于安全校验)
    2. 服务器同意升级 HTTP/1.1 101 Switching Protocols ← 101 = "我同意切换协议了" Upgrade: websocket Connection: Upgrade Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGz... ← 基于 Key 计算的值(验证对方确实懂 WebSocket)
    3. 协议升级完成
      • 从此刻起,这条 TCP 连接不再说 HTTP 了,双方改说 WebSocket 的帧协议(Frame Protocol),双向随时发数据,没有请求-响应的限制。
握手前(HTTP):                  握手后(WebSocket):
┌──────────────┐               ┌──────────────┐
│   HTTP/1.1    │  ── 升级 ──→  │   WebSocket   │
│   文本协议     │               │   二进制帧协议  │
│   请求-响应    │               │   全双工       │
│   头部几百字节  │               │   帧头 2~14字节 │
└──────────────┘               └──────────────┘
       ↑                              ↑
  还是跑在 TCP 之上              还是同一条 TCP 连接
  (如果 wss:// 则还有 TLS)     (连接没断,只是协议变了)
  • Sec-WebSocket-Key / Sec-WebSocket-Accept 是做什么的?
    • 简单的 防伪造机制, 不是加密,验证对方是否是 WebSocket 服务器。

WebSocket vs HTTP 对比

维度 HTTP WebSocket
通信模式 请求-响应(半双工) 全双工
连接 短连接或 Keep-Alive 持久连接
头部开销 每次请求带完整头部(几百字节) 握手后每帧只有 2~14 字节头部
服务器推送 不支持(只能客户端发起) 支持
URL 协议 http:// / https:// ws:// / wss://
适用场景 API 调用、资源获取 实时通信、推送、游戏

iOS 一般使用原生的 URLSessionWebSocketTask(iOS 13+)。

为什么有些项目使用自定义协议(例如[包长度|protobuf序列化数据]),不使用 WebSocket?

WebSocket 自定义协议
建连开销 TCP 握手 + HTTP 升级握手(多一个 RTT) TCP 握手后直接发数据
帧头开销 2~14字节(opcode/mask/长度) 4字节(只写长度就够了)
心跳 固定的 ping/pong 自定义 Protobuf 心跳(可带业务数据)
压缩 整个连接统一压缩策略 不同消息可选不同的压缩策略
传输层 只能 TCP 可以 TCP / KCP 等随时切换
浏览器兼容 ✅这是它存在的意义 ❌但是 App 不需要

2.5 HTTP 版本演进(HTTP/1.0 -> HTTP/1.1 → HTTP/2 → HTTP/3)

  • HTTP/1.0

    • 引入请求头、状态码。
    • 问题:
      • 最初版本,每次请求都需要重新建立连接。
  • HTTP/1.0 -> HTTP/1.1()

    • 问题: HTTP/1.0 每次请求都要新建 TCP 连接,用完就断开,资源浪费;
    • 改进:
      • 持久连接(Keep-Alive): 默认不断开,复用同一个 TCP 连接发送多个请求;
      • 管道化(Pipelining): 连续发送 多个请求,不用等待上一个响应回来。
    • 仍有问题:
      • 对头阻塞(HTTP 层): HTTP/1.1 规定响应必须按照请求顺序返回,如果第 1 个请求慢了,后续全阻塞。
  • HTTP/1.1 -> HTTP/2(大幅性能提升,显著减少延迟与连接数量)

    • 改进:
      • 二进制分帧: 不再是文本协议,把数据拆成带有 Stream ID 标记的帧(Frame)传输;
      • 多路复用: 一个 TCP 连接上 并行传输 多个请求和响应的帧,互不阻塞;
        • ‼️每个帧都标记了 Stream ID,接收方根据 ID 把帧重新组装成各自的响应。帧可以交错发送,谁先好就先发谁—— HTTP 层的队头阻塞 解决了。
      • 头部压缩(HPACK): HPACK 用静态表+动态表+哈夫曼编码大幅压缩头部;
      • 服务器推送(Server Push): 服务器主动推送资源;
    • 仍有问题:
      • ‼️多路复用虽然解决了 HTTP 层的队头阻塞,但底层 TCP 的对头阻塞还在——TCP 丢一个包,整个连接上所有 Stream 都得等重传。
        • ‼️因为底层用的还是 一条 TCP 连接,TCP 的特性 保证字节流严格有序到达所有 Stream 共享一条字节流,空洞就等,等到补齐为止。
  • HTTP/2 -> HTTP/3(基于 QUIC,连更低的头部延迟与更快的握手)

    • 改进:

      特性 HTTP/2 (TCP) HTTP/3 (QUIC/UDP)
      传输层 TCP UDP(自己实现可靠传输)
      队头阻塞 TCP 层有 完全消除(各 Stream 独立)
      握手延迟 TCP 握手 + TLS 握手 QUIC 内置 TLS,1-RTT
      重连 全新握手(WiFi→蜂窝) 0-RTT 恢复(快速重连)
      连接迁移 基于四元组,换 IP 就断 Connection ID 标识,换 IP 不断
      • ‼️解决 TCP 层对头阻塞
        • 不用 TCP 了,在 UDP 上自己造一个传输层,让每个 Stream 都有独立的序号和重传机制,每个 Stream 有自己独立的字节流,互不干扰。
      • 对 iOS 端的意义: 用户在 WiFi 和蜂窝之间可以做到无感切换
      传统 TCP(HTTP/2):
      用户走出公司(WiFi)→ 走上街(蜂窝) → IP 地址变了 → TCP 连接断了 → 重新握手
      QUIC(HTTP/3):
      用户走出公司(WiFi)→ 走上街(蜂窝) → IP 变了但 Connection ID 没变 → 连接无缝切
      

三、传输层:TCP、UDP、QUIC、KCP

3.1 TCP

TCP 是一个 面向连接、可靠、有序、全双工 的传输协议。在建立连接前,双方需要达成 3 个共识:

  1. 确认通信能力: 双向通信,双方都能收能发。
  2. 交换初始序列号(ISN): 每个字节都有序号,双方要各自告知自己的起始序号。
  3. 防止历史连接的干扰: 网络中的旧报文不能让新连接误认为是有效的。

三次握手就是为了解决这三件事。


3.1.1 TCP 三次握手

image.png

  • 过程:
    • 第 1 次: C -> 连接请求 (SYN) -> S
    • 第 2 次: S -> 收到请求后,发送连接请求的确认请求 (SYN+ACK) -> C
    • 第 3 次: C -> 收到请求后,发送确认请求的确认请求 (ACK) -> S

常见问题:

1. 为什么不是 2/4 次握手,而是 3 次握手?

  • 2 次握手,双端状态可能不一致,存在 “半开连接” 和 “旧连接干扰” 问题。

  • 3 次握手 的作用:

    1. 确认双向通信可达:
      • 3 次握手保证 C、S 双方相互得知对方的收发能力正常;
      • 如果只有 2 次握手,S 无法得知 C 的接收能力是否正常。
    2. 防止旧连接干扰:
      • 3 次握手通过新的 ISN 和确认机制,可以识别出过期连接并丢弃。
      • 如果只有 2 次握手,S 可能错误建立 “脏连接”,造成状态混乱。
    3. 建立可靠传输基础:
      • 3 次握手双方交换初始序列号,建立有序的、基于字节流的协议。

2. 如果第 3 次握手丢失,服务端、客户端分别如何处理?

第 3 次握手丢失会造成短暂的 “半开连接”,但 TCP 的 重传与超时机制 最终会恢复一致性。

  • 客户端
    • 进入 ESTABLISHED,认为连接成功,继续等待 S 响应或者发送数据。
  • 服务端
    • 仍在 SYN_RECEIVED,等待 ACK,定时器到期后会重传 SYN+ACK。
    • C 收到重传的 SYN+ACK 后,会再次发送 ACK;
    • 若多次重传仍无回应,S 会放弃连接,并释放 半连接资源
    • C 尝试发数据,若 S 未进入 ESTABLISHED,可能收到 RST 响应。

3. 为什么 ISN(初始序列号)不固定?

  • 防止旧连接干扰:
    • 网络中可能残留旧包,若新连接的 ISN 与旧连接相同,旧报可能被误认为是当前连接的数据。
  • 增强安全性:
    • 防止攻击者通过预测序列号来伪造 TCP 报文(TCP 盲注入攻击、会话劫持);
    • RFC 要求 ISN 必须 随时间变化且不可预测,现代操作系统(Linux/iOS/macOS)也都采用 “时间戳+随机扰动” 动态生成方式。

4. 3 次握手的过程中是否携带数据?

标准的 TCP 行为 是不会在握手阶段携带数据的:

  • 前 2 次握手 不携带数据,是因为 连接尚未建立接收方未分配接收缓冲区,若携带数据,可能造成安全与资源浪费问题(攻击者伪造 SYN 报文携带大量数据)。
  • 第 3 次握手 理论上可以携带数据,因为 C 已经进入 ESTABLISHED 状态了。但是大多实现出于简化和安全考虑,也不在 ACK 中携带数据。
    • 实现简化:
      • S 的 接收缓冲区和应用层 socket 可能还没准备好,处理逻辑复杂。
    • 安全问题:
      • 防止 DoS 攻击和数据误处理(若 ACK 丢失或重传,可能导致同一份数据被重复处理,S 在连接未确认时读渠道未认证的数据,会产生安全隐患(伪造、注入等风险))。

特殊情况: TCP Fast Open (TFO) 允许在 SYN 报文中携带应用数据并提前发出,实现 “0- RTT 建连”,但需要 S 支持。

5. “半开连接” 是怎么产生的?

  • 概念: 半开连接(Half-Open Connection) 是指连接的两端状态不同步:一方认为连接已建立或仍存在,另一方实际上未建立或已断开。
  • 后果: 半连接堆积(如 SYN Flood 攻击)。
  • 检测手段: 内核(SYN Cookies、防火墙限速)、应用层(心跳检测、超时挥手)。
  • 握手阶段丢包: 第 3 次握手丢失,S 进入 SYN_RECEIVED,客户端已进入 ESTABLISHED
    • 服务端 会将半开连接存放在 半连接队列 中。
  • 已连接后异常断线: C 退出 App,S仍认为连接存在,处于 ESTABLISHED,等待数据。
    • 需要依赖 KeepAlive 心跳 检测。
  • 关闭阶段异常: 某端未正确完成四次挥手(如 FIN/ACK 丢失)。
    • 双方状态不一致(FIN_WAIT / CLOSE_WAIT)。

扩展一下

1. 全双工 vs 半双工

全双工: 指通信双方可以 同时 进行发送和接收数据。

半双工: 双方都可以发送数据,但 不能同时,只能 “你说完我再说”。

模式 含义 能否同时发送/接收 TCP 中体现
单工 单向通信 几乎不用
半双工 双向但不同时 早期物理层通信
全双工 双向同时 TCP 是全双工协议
半关闭 一方发送关闭,但可接收 仅一方向关闭 TCP 四次挥手中的状态

2. 全连接队列 vs 半连接队列

服务端 TCP 内核实现层面的关键概念,

全连接队列: 存放 3 次握手已完成,但还未被应用层 accept() 接收的连接。

半连接队列: 存放已收到客户端 SYN,但 3 次握手尚未完成 的连接。

握手过程:

步骤 状态 队列
C -> 发送 SYN S 进入 SYN_RECIEVED 加入 半连接队列
S -> 发送 SYN+ACK,等待 ACK C 进入 ESTABLISHED 半连接队列等待确认
C -> 回 ACK 包 S 进入 ESTABLISHED 半连接队列 移入 全连接队列

特点:

  • 如果第 3 次握手迟迟不到,连接会在队列中等待一段时间,知道超市或超过最大重传次数,就删除这个 半连接
  • 全连接队列 已🈵,新握手完成的连接会被 丢弃/拒绝
  • 应用层调用 accept() 是,才会将连接从 全连接队列 中取出。

3.1.2 TCP 四次挥手

image.png

  • 过程:
    • 第 1 次: C -> FIN -> S,客户端主动发送 FIN 关闭发送通道,服务端收到 FIN 后 关闭接收通道
    • 第 2 次: S -> ACK -> C,服务器仍可继续 发送剩余数据
    • 第 3 次: S -> FIN -> C,服务端发送完所有数据后,主动发送 FIN 关闭发送通道,客户端收到 FIN 后 关闭接收通道
    • 第 4 次: C -> ACK -> S,服务端收到 ACK 后 立即关闭连接,客户等待 2MSL 后 彻底关闭连接,确保最后的 ACK 能被对方收到并防止旧包干扰。

常见问题:

1. 为什么是 “4 次挥手”,而不是 “3 次”?

  • 因为 TCP 是 全双工协议,双方的发送和接收通道是 独立的,每个方向都必须 单独关闭,因此有了 “4 次” 挥手。
    • 当一方(主动方)发出 FIN,只表示 “不再发送数据”,但是 “还能接收数据”;
    • 另一方(被动方)可能还没有发完数据,所以不能立即 FIN;
    • 另一方(被动方)必须等到自己也准备关闭时,再单独发出 FIN;
    • 因此需要一次 FIN 一次 ACK,再一次 FIN 一次 ACK,总共四次,确保双方都能 安全、完整地 关闭。

2. TIME_WAIT 是什么?

  • 主动关闭连接的一方,在发送最后一个 ACK 后会先进入 TIME_WAIT 状态,等待一段时间(2MSL)再彻底关闭连接(进入 CLOSED)。
    • MSL(Max Segment Lifetime): 网络中一个 TCP 报文可能存在的最长时间。
  • 为什么不直接进入 CLOSED 呢?
    1. 保证 “最后一个 ACK” 可靠传输
      • 如果 C 立即关闭,S 没收到 ACK,会重发 FIN;
      • 若 C 已关闭,就无法回应,导致服务端一直处于 LAST_ACK 状态。
    2. 防止旧连接的干扰
      • 如果立即关闭,并重新使用相同四元组(IP、PORT、协议),网络中延迟处理的旧包可能被当作新连接的数据;
      • TIME_WAIT 期间确保旧包在网络中全部消失后才关闭。

3. 为什么 TIME_WAIT 要持续 2 x MSL?

  • 一段 MSL 是为了 确保本连接最后发送的 ACK 能到达对方
  • 另一段 MSL 是为了 确保旧连接中的报文在网络中彻底消失

4. TIME_WAIT 常见问题

问题 原因 对策
TIME_WAIT 太多,占用端口 客户端大量主动关闭连接 1. 调整 tcp_tw_reuse、tcp_tw_recycle(Linux 旧版);2. 使用长连接;3. 服务端主动关闭
TIME_WAIT 导致端口耗尽 并发短连接太多 增大临时端口范围;采用连接池或 HTTP keep-alive
服务端出现大量 CLOSE_WAIT 服务端未调用 close()正常关闭 检查应用层逻辑,确保及时释放

5. TIME_WAITCLOSE_WAIT 区别

状态 所在端 触发条件 表示意义
TIME_WAIT 主动关闭方 发完最后 ACK 等待 2MSL 等待旧包消失,保证连接彻底关闭
CLOSE_WAIT 被动关闭方 收到对方 FIN,尚未发送自己的 FIN 应用层未 close(),导致资源占用

6. RST 什么时候出现?

  • 已关闭的连接 发送数据;
  • 未建立的连接 发送请求;
  • 半连接超时被清理;
  • 异常关闭;

3.1.3 TCP vs UDP

一句口诀:

  • TCP 是一种面向连接、可靠的、一对一的、面向字节流的、首部 20-60 字节的、传输层通信协议。
  • UDP 是一种无连接的、不可靠的、任意连接个数的、面向报文的、首部 8 字节的、传输层通信协议。
维度 TCP UDP
连接性 面向连接(三次握手) 无连接的(不需要事先建立连接)
可靠性 可靠(数据包校验、包重排、丢弃重复数据包、ACK 机制、超时重传机制、拥塞控制、流量控制) 不可靠(可能丢包、乱序)
通信模式 一对一 一对任意数量(单播、多播、广播)
数据传输单元 面向字节流(无边界,可能粘包/拆包) 面向报文(有边界,保留报文长度)
首部开销 较大,20~60 字节 很小,固定 8 字节
传输效率 相对较低(需建立连接、确认、控制机制) 极高(无控制开销,延迟低)
适用场景 要求 数据准确完整 的场景,如 FTP、HTTP、SMTP、SSH 要求 实时性高、速度快 的场景,如音视频、通话、直播、DNS 查询、游戏等
  • TCP 特性详解
    1. 核心特性:

      • 面向连接: 三握四挥
      • 可靠传输: 一系列复杂机制确保数据 准确、有序、不重复 地送达
      • 全双工通信: 连接建立后,双方可同时进行数据收发。
    2. 可靠性保障机制: 0. 三次握手、四次挥手

      1. 序列号与确认应答(ACK): 每个字节都有唯一序列号,接收方通过返回 ACK 确认收到。
      2. 超时重传(RTO): 发送数据后启动计时器,若在 RTO(超时重传时间)内未收到 ACK,则重发数据。RTO 动态计算,通常略大于 RTT(往返时延)。
      3. 数据包排序: 利用序列号对数据包排序。
      4. 丢弃数据包: 根据序列号识别并丢弃重复包。
      5. 校验和: 校验数据在传输中是否出错,出错则丢弃等待重传。
      6. 流量控制: 通过接收方通告的窗口大小,动态调整发送速率,防止接收方缓冲区溢出。
      7. 拥塞控制: 通过拥塞窗口和四种算法(慢开始、拥塞避免、快重传、快恢复)感知并应对网络拥塞,降低整体丢包率。
        • RTT 往返时延
          • 由链路的传播时间、末端系统出的处理时间、路由缓存中的排队和处理时间组成。
          • 最后一个因素会 随着网络拥塞程度而变化,所以 RTT 一定程度上也反映网络拥塞程度
    3. 面向字节流与粘包问题

      • 字节流: TCP 将数据视为无结构的字节流,不保留消息边界。发送方多次写入数据可能被合并(粘包)或拆分(拆包)发送。
      • 粘包解决方案:
        • 先传包大小,再传包内容;
        • 固定包长度;
        • 设置结束标志;
    4. 核心机制:

      • 滑动窗口:
        • 实现流量控制的核心数据结构,它标识了在无需等待确认的情况下,发送方 能连续发送的数据范围,极大 提高传输效率
      • 流量控制:
        • 目的是 保护接收方,防止接收方被淹没,控制对象为端到端(rwnd),接收方通过 ACK 包中的 “窗口大小” 字段,告知发送方自己 剩余的缓冲区容量,从而 控制发送方的发送速率
      • 拥塞控制:
        • 目的是 保护网络,防止网络过载,控制对象为全网(cwnd),通过感知网络拥塞程度(如丢包、RTT增长),动态调整 “拥塞窗口” 的大小,控制 向网络注入数据的全局速率
    5. TCP 缓冲区

      • 发送缓冲区: 存储 已发送未确认、以及待发送 的数据,每个字节都有序列号,在收到 ACK 确认的数据才会从缓冲区移除。
      • 接收缓冲区: 存储 已接收但未被应用层读取,以及乱序到达 的数据,其剩余空间大小通过窗口通告给发送方,确保接收缓冲区不溢出。

  • UDP 特性详解
    1. 核心特性

      • 无连接: 直接发送数据,无需建立连接,开销小。
      • 不可靠传输: 不提供 ACK、重传、排序 等保障机制,数据可能 丢失、重复、乱序
      • 面向报文: 对应用层交下来的报文,既不合并也不拆分,一次发送一个完整的报文,保留消息边界。
    2. 优缺点

      • 优点:
        • 速度快、延迟低: 无连接、无控制开销。
        • 头部开销小: 仅 8 字节。
        • 支持多播/广播: 可以高效向多个目标发送数据。
        • 无阻塞控制: 在网络拥塞时仍能保持发送速率,适合实时应用。
      • 缺点:
        • 不保证可靠性: 需要由 应用层 自行处理 丢包、重复、乱序 等问题。
        • 易导致网络拥塞: 缺乏拥塞控制,若大量发送可能加剧网络拥塞。
    3. UDP 报文结构

      • 头部(8 字节):源端口(2)、目的端口(2)、长度(2)、校验和(2)。
    4. 不可靠性的应用层解决方案

      1. 增加序列号
      2. 引入ACK与重传机制
      3. 实现流量控制
    5. UDP缓冲区

      • 发送缓冲区: 发送时,将数据放入缓冲区后立即发送,并从缓冲区清除不停留;
      • 接收缓冲区: 接收时,将数据放入缓冲区供应用读取。

单独讲一下 拥塞控制 与 流量控制
  • 拥塞控制:

    • 本质: 拥塞控制的本质是发送方通过一个 拥塞窗口 变量,来动态探测并适应网络传输能力,尽可能高效利用可用带宽。
      • 拥塞窗口(cwnd):
        • 发送方根据自己 对网络拥塞程度的评估 而维护的一个窗口值,它代表了 “在当前网络情况下,我能安全发送多少数据,而不造成拥塞”。
      • 发送窗口的最终大小:
        • 发送方在任一时刻,实际能发送的数据了 = min(cwnd,rwnd)
      • 慢启动门限(ssthresh):
        • 一个状态切换的阈值。当 cwnd < ssthresh,使用慢开始算法;当 cwnd >= ssthresh,使用拥塞避免算法。
    • 影响: 网络拥塞时 TCP 继续发包可能会导致数据包丢失或时延,这时 TCP 就会重传,导致网络更加拥塞
    • 拥塞前兆:
      • 数据包延迟显著增加;
      • 丢包率上升;
      • 网络吞吐量下降;
        1. 超时重传(RTO 超时): 严重拥塞信号。
          • 网络非常拥堵,必须 “急刹车”,然后从最低速重新开始指数增长。
        2. 收到 3 个重复的 ACK: 轻度拥塞信号。
          • 网络只是轻度丢包,数据还在流动,可以适度减速。
    • 拥塞控制:
    初始状态
        ↓
    慢启动(指数增长)
        ↓ (cwnd >= ssthresh)
    拥塞避免(线性增长)
        ↓
    [网络事件发生]
        ├─ 超时重传 ──→ 慢启动重启(cwnd=1)
        └─ 3个重复ACK ──→ 快重传 + 快恢复 ──→ 拥塞避免
    

  • 流量控制:

    • 本质: 本质是控制供需平衡,解决收发双方的速率匹配问题,防止发送方发送速率超过接收方的处理能力,导致接收方缓冲区溢出和数据丢失
    • 核心机制:滑动窗口
    发送缓冲区(发送方维护)
    ┌───────────────────────────────────────────────────┐
    │ 已发送且已确认 │  已发送未确认 │  可发送未发送 │ 不可发送 │
    │  (可清除)     │  (等待ACK)   │  (在窗口内)  │(超出窗口)│
    ├───────────────────────────────────────────────────┤
    ◄── 已确认部分 ──►◄─────────── 发送窗口 ────────────►
    
    接收缓冲区(接收方维护)
    ┌──────────────────────────────────────┐
    │ 已接收已确认  │ 可接收未接收   │ 不可接收 │
    │ (待应用读取)  │  (在窗口内)   │(超出窗口)│
    ├──────────────────────────────────────┤
    ◄── 已使用部分 ─►◄─────── 接收窗口 ───────►
    

四、网络层:IP

4.1 作用

逻辑寻址 + 路由转发

  • 核心协议:IP、ICMP、ARP、NAT、路由协议(RIP/OSPF/BGP)

4.2 IP 基础

版本 地址长度 示例
IPv4 32 位 192.168.1.1
IPv6 128 位 2001:db8::1
  • IP 报文由 【头部 + 数据】 组成,头部含源 IP、目标 IP、TTL、协议号。
  • 若包太大,网络层负责分片与重组。
    • 负责寻址与分片;
    • TTL 防死循环;
    • 协议号区分上层(6=TCP,17=UDP)。

4.3 ICMP

  • Internet Control Message Protocol,用于诊断和错误报告。
  • 常见命令:ping(测试连通性)、traceroute(路由追踪)。

4.4 路由机制

  • 静态路由:手动配置;
  • 动态路由:路由器间自动交换信息(RIP、OSPF、BGP)。
  • 默认网关:未知目标的转发出口。

五、网络接口层(链路层、物理层):WiFi、蜂窝

5.1 职责

  • 封装帧、寻址、差错检测(FCS)、介质访问控制。
  • 负责同一局域网内的 节点到节点通信

5.2 MAC 地址与 ARP

  • 每个网卡唯一的 48 位地址(00-1C-42-7A-xx-xx)。
  • ARP(地址解析协议):根据 IP 查 MAC 地址。
    • 逆向为 RARP。

5.3 常见协议

类型 协议 作用
有线 Ethernet 主流局域网协议
无线 Wi-Fi(IEEE 802.11) 无线局域网标准
点对点 PPP 拨号链路协议

六、三方库:AFNetworking

6.1 AFNetworking

AFNetworking 本质上就是对苹果 NSURLSession 的面向对象封装,把 delegate 回调模式变成更易用的 Block 模式,同时加了一套序列化体系。

整体结构:

AFNetworking 4.0
│
├── 核心层
│   ├── AFURLSessionManager        会话管理器(核心中的核心)
│   └── AFHTTPSessionManager       HTTP 便捷管理器
│
├── 序列化层
│   ├── AFHTTPRequestSerializer    请求序列化(拼参数、设 Header)
│   │   ├── AFJSONRequestSerializer
│   │   └── AFPropertyListRequestSerializer
│   ├── AFHTTPResponseSerializer   响应序列化(解析数据)
│   │   ├── AFJSONResponseSerializer
│   │   ├── AFXMLParserResponseSerializer
│   │   └── AFImageResponseSerializer
│   └── AFSecurityPolicy           SSL 证书校验策略
│
├── 辅助层
│   └── AFNetworkReachabilityManager  网络状态检测
│
└── UIKit 扩展 (可选)
    ├── UIImageView+AFNetworking
    ├── UIButton+AFNetworking
    └── AFNetworkActivityIndicatorManager
  • AFURLSessionManager(一切的基础)
    • 持有:
      • NSURLSession *session ← 苹果原生会话
      • NSOperationQueue *operationQue
      • AFSecurityPolicy *securityPolicy
      • AFNetworkReachabilityManager *reachabilityManager
      • NSMutableDictionary *mutableTaskDelegatesKeyedByTaskIdentifier (每个 Task 对应一个 delegate, 管理回调)
    • 做了什么?
      • 实现 NSURLSessionDelegate 全家桶
      • 把 delegate 回调 →转化成→ Block 回调
      • 给每个 NSURLSessionTask 配一个 AFURLSessionManagerTaskDelegate,这个内部 delegate 负责收集数据、计算进度、最终回调。
      • 管理 Task 的生命周期 (创建/取消/暂停/恢复)
      • SSL 证书校验 (通过 AFSecurityPolicy)
    • 关键方法:
      • 数据任务(用于普通 HTTP 请求,请求与响应体都在内存中): dataTaskWithRequest:completionHandler:
      • 上传任务(用于 “上传本地文件” 到服务器,底层基于 http(s)): uploadTaskWithRequest:fromFile:progress:completionHandler:
      • 下载任务(用于 “下载文件” 到本地磁盘): downloadTaskWithRequest:destination:progress:completionHandler:
  • AFHTTPSessionManger(继承自 AFURLSessionManager, HTTP的便捷入口)
    • 新增:
    • 提供 RESTful 风格方法:
    • 内部流程:

序列化体系

  • 这是 AFNetworking 最核心的设计模式 —— 请求/响应序列化器可替换
请求序列化器 (怎么把参数变成 HTTP 请求)
────────────────────────────────────────

AFHTTPRequestSerializer (默认)
  · Content-Type: application/x-www-form-urlencoded
  · 参数编码: key1=value1&key2=value2
  · 设置通用 Header: User-Agent / Accept-Language

AFJSONRequestSerializer
  · Content-Type: application/json
  · 参数编码: JSON 格式 {"key1":"value1"}
  
  响应序列化器 (怎么把响应数据变成对象)
────────────────────────────────────────
AFHTTPResponseSerializer (默认)
  · 不做任何解析, 直接返回 NSData

AFJSONResponseSerializer
  · 验证 Content-Type 是否为 JSON
  · NSJSONSerialization 解析 → NSDictionary / NSArray

AFImageResponseSerializer
  · 验证 Content-Type 是否为图片
  · 解析 → UIImage

6.1.1 一个请求在 AF 内部的完整流程

manager.POST(url, parameters, headers, success, failure):   业务调用
    
    
 AFHTTPRequestSerializer.request:          请求序列化
    
    
    返回 NSMutableURLRequest
    
    
 AFURLSessionManager.dataTask:           创建 Task,绑定 delegate,注册回调。
    
    
 task.resume()   请求发出去了
    
    
 NSURLSession delegate 回调 (AF 接管):        接收数据,回调 AFURLSessionManagerTaskDelegate
    
    
 AFURLSessionManagerTaskDelegate:        汇总数据
    
    
 AFJSONResponseSerializer.response:     响应序列化
    
    
    返回 NSDictionary
    
    
 dispatch_group:           回到主线程

6.1.2 AFNetworking 使用优化

  • AFHTTPSessionManager: 可以注入 Cronet 以支持 QUIC / HTTP2‘
  • AFHTTPRequestSerializer: 可以增加 AES128 加密等;
  • AFJSONResponseSerializer 可以增加对应的 AES128 加密、加 GZip/Brotli 解压逻辑等。

6.1.3 AFNetworking vs 直接使用 NSURLSession

NSURLSession AF 封装后
要自己实现 4 个 delegate 协议 Block 回调,几行代码搞定
要自己拼 URL、编码参数 manager.POST(url, params) 一行搞定
要自己管理 Task 生命周期 AF 自动管理
要自己解析 JSON responseSerializer 自动解析
要自己做 SSL 校验 AFSecurityPolicy 配置即可
要自己检测网络状态 AFNetworkReachabilityManager 现成的

6.1.4 AFNetworking vs Protobuf

  • AF 序列化: 把参数 → 变成 HTTP 协议能理解的格式
  • Protobuf: 把数据 → 变成一种紧凑的二进制编码格式

AF 序列化把字典变成 HTTP 能传的文本(HTTP 协议规范),Protobuf 把对象变成 Socket 能传的二进制。 前者为了兼容 HTTP 标准,后者为了追求极致的小和快。

AF 序列化 Protobuf
格式 文本 (JSON / 表单) 二进制
可读性 人能直接看 看不懂,需要 .proto 文件才能解
体积 小(1/3~1/5)
解析速度 快(10~100x)
Schema 无,运行时动态解析 有,编译时生成代码(GPBMessage 子类)
用在哪 HTTP 请求/响应 TCP 长连接的数据包
为什么选它 HTTP 标准就是 JSON/表单 长连接要求低延迟、省流量

七、总结

落实到 App 开发中,常见链路可能是这样的:

整体链路关系:

┌─────────────────────── 应用层 ───────────────────────────┐
│                                                         │
│  业务协议:      HTTP API / Protobuf / RTC                │
│  安全:          AES 加密 / HMAC-SHA1 签名   / 防重放       │
│  域名管理:            业务统一管理                         │
│  域名解析:           HTTPDNS / 系统 DNS                   │
│                                                         │
├─────────────────────── 传输层 ───────────────────────────┤
│  TCP (GCDAsyncSocket)                                   │
│  UDP + KCP (GCDAsyncUdpSocket + KCP)                    │
│  QUIC (Cronet)                                          │
│                                                         │
├─────────────────────── 网络层 ───────────────────────────┤
│  IP (v4/v6)                                             │
│                                                         │
├─────────────────────── 接口层 ───────────────────────────┤
│  WiFi / 蜂窝 / 断网检测                                   │
└─────────────────────────────────────────────────────────┘

HTTP 链路:

┌─────────────────────── 应用层 ───────────────────────────┐
                                                         
   业务入口                                              
     ObjC:  xxxxHTTP.facebookLogin(...)                  
     Swift: xxxxAPI(params).observable.subscribe(...)    
                                                        
                                                        
   调度中心                                              
     ObjC:  业务统一错误码处理等                             
     Swift: MoyaProvider  (插件链处理)                     
                                                         
                                                         
   请求模型 (两侧做同样的事, 语言不同)                       
       · 填充公共参数: uid, sid, device_id, version...     
       · 生成防重放:   nonce (UUID) + millisecond (时间戳)  
       · 计算签名:     token (HMAC-SHA1)                   
                      sign  (HMAC-SHA1)                   
       · 合并业务参数                                       
                                                         
                                                         
   序列化 & 加密                                          
     ObjC:  HTTPAESRequestSerialization                   
     Swift: MoyaAESHandler.prepare                        
       · 按路径判断是否需要 AES128 加密请求体                  
       · 设置请求头 X-ENCRYPTED-VERSION                     
                                                         
                                                         
   HTTP                                                
     ObjC:  AFNetworking    AFHTTPSessionManager         
     Swift: Alamofire       Session                      
                                                         
                                                         
   NSURLSession (两侧共用)                                
     · SessionConfiguration 注入 Cronet                    
     · Cronet 尝试 QUIC  降级 HTTP/2  降级 HTTP/1.1       
                                                          
├─────────────────────── 传输层 ───────────────────────────┤
                                                         
          QUIC (Cronet, UDP)  TCP (系统)                
                                                        
                                                        
                       TLS 握手                           
                                                         
├─────────────────────── 网络层 ───────────────────────────┤
                                                          
  IP 路由                                                  
                                                          
├─────────────────────── 接口层 ───────────────────────────┤
                                                          
  WiFi / 蜂窝                                             
                                                          
└──────────────────────────────────────────────────────────┘

TCP 链路:

┌─────────────────────── 应用层 ───────────────────────────┐
│                                                         │
│  ① 建连 (App启动 / 登录成功触发)                           │
│                          │                               │
│                          ▼                               │
│  ② 认证 (连接建立后)                                      │
│       · 构建请求: sid + deviceId + version                │
│                          │                               │
│                          ▼                               │
│  ③ 封包:Protobuf 等                                      │
│                          │                               │
│                          ▼                               │
│  ④ Socket 发送                                            │
│                          │                               │
│                          ▼                               │
├─────────────────────── 传输层 ────────────────────────────┤
│                                                          │
│  TCP (GCDAsyncSocket)  /  KCP + UDP (GCDAsyncUdpSocket)  │
│                                                          │
├─────────────────────── 网络层 ────────────────────────────┤
│                                                          │
│  IP 路由                                                  │
│                                                          │
├─────────────────────── 接口层 ───────────────────────────┤
│                                                          │
│  WiFi / 蜂窝                                             │
│                                                          │
└──────────────────────────────────────────────────────────┘

保护机制:

┌─────────────────────── 应用层 ───────────────────────────┐
│                                                         │
│  心跳保活                                                │
│     →  NSTimer 定时发送心跳包                             │
│                                                         │
│  守护进程 (ConnectorDaemon)                             │
│    ├── TCP 守护: 连续 n 次失败 → 切备用域名                │
│    └── KCP 守护: n 秒内 n 次超时 → 切回 TCP               │
│                                                        │
│  重连策略                                                │
│    TCP 断开      → 0.5s 自动重连                          │
│    bind 失败     → 1s 重试 bind                          │
│    域名故障      → 切备用域名                              │
│    KCP 故障      → 切回 TCP 连同一域名                     │
│    前后台切换    → 发探测包验证, 失败则重连                   │
│                                                          │
└──────────────────────────────────────────────────────────┘
❌
❌