普通视图

发现新文章,点击刷新页面。
昨天以前首页

iOS 26 libass字幕渲染问题兼容解决实践

作者 LoyalToOrigin
2026年4月22日 12:06

背景

在 iOS 26 上,视频播放器使用的 libass 字幕渲染器遭遇了严重的兼容性问题。当字幕指定的字体在系统中找不到时,libass 的 CoreText 后端会尝试 fallback 到系统字体路径:

/System/Library/PrivateFrameworks/FontServices.framework/CorePrivate/PingFangUI.ttc

然而,这个路径在 iOS 26 的沙盒机制下被系统拦截,导致 fallback 失败,内嵌 ASS/SSA 字幕中的中文字符完全无法渲染。用户看到的只是一片空白或乱码。


问题分析

1. 问题表现

字幕类型 问题描述
内嵌 ASS/SSA 中文字符完全不显示,字体 fallback 失败
外挂 ASS/SSA 某些字体无法渲染,fallback 到被拦截的路径
SRT(内嵌提取后) 带有 <font face="xxx"> 标签的 SRT,freetype 尝试加载指定字体失败

2. 根本原因

iOS 26 引入了一个沙盒安全限制,阻止了 libass 对系统字体的访问。libass 的字体回退机制无法获取 PingFang 字体,导致整个字幕渲染失败。


解决方案

方案概述

视频播放
    ↓
禁用内嵌字幕(避免 libass 走系统字体路径)
    ↓
提取内嵌字幕流为 SRT(通过 FFmpeg)
    ↓
SRT 无字体定义,通过 freetype 渲染器 + 指定中文字体显示
    ↓
同时对外挂 ASS 字幕做字体名替换(指向 CoreText 已注册的字体)

核心实现

1. 内嵌字幕提取

通过 FFmpeg 提取视频中的字幕流,转换为无字体定义的 SRT 格式:

// 使用 FFmpeg 提取字幕流
FFmpegWrapperAPI *ffAPI = [[FFmpegWrapperAPI alloc] init];
ffAPI.inputPath = videoPath;

// -map 0:s:N 选择特定字幕流
NSString *command = [NSString stringWithFormat:@"-map 0:s:%d", trackIndex];

[ffAPI runFFmpegAPI:videoPath
         outputPath:srtOutputPath
             prefix:nil
            command:command
              async:YES];

2. 字体名替换(正则方案)

ASS 字幕中的字体名出现在两处:

  • [V4+ Styles] 定义行:Style: Name,Fontname,Fontsize,...
  • [Events] Dialogue 行内覆盖标签:{\fn字体名}
// 替换 Dialogue 行内的 {\fn任意字体名} 覆盖标签
NSRegularExpression *fnTagRegex = [NSRegularExpression
    regularExpressionWithPattern:@"\\{\\\\fn[^}\\\\]+"
    options:NSRegularExpressionCaseInsensitive
    error:&regexError];
modifiedText = [fnTagRegex stringByReplacingMatchesInString:modifiedText
                                                     options:0
                                                       range:NSMakeRange(0, modifiedText.length)
                                                withTemplate:[NSString stringWithFormat:@"{\\fn%@", kTargetFontName]];

// 替换 Style 定义行的 Fontname 字段
NSRegularExpression *styleLineRegex = [NSRegularExpression
    regularExpressionWithPattern:@"^(Style\\s*:\\s*[^,]+,)([^,]+)(,.*)$"
    options:0
    error:&regexError];
// ...

SRT 字幕中的字体名出现在 HTML 标签中:

// 替换 <font face="任意内容">
NSRegularExpression *fontFaceRegex = [NSRegularExpression
    regularExpressionWithPattern:@"<font\\s+face\\s*=\\s*([\"'])[^\"']*\\1"
    options:NSRegularExpressionCaseInsensitive
    error:&regexError];

🔴 踩坑实录

坑一:VLC 索引与 FFmpeg 索引的映射错误

问题描述:用户选择中文内嵌字幕,但实际显示的是英文字幕。

根因分析

  • VLC 的 videoSubTitlesIndexes 数组索引 0 是 "Disable"
  • 内嵌字幕从索引 1 开始:索引 1 → 第一条字幕,索引 2 → 第二条字幕
  • FFmpeg 的字幕流索引从 0 开始:第一条字幕流是 0,第二条是 1

错误的映射:

用户选择 VLC 索引 1(第一条字幕)→ 错误地映射为 FFmpeg 索引 1 → 提取了第二条字幕

代码修复

// 修复前(错误)
int ffmpegTrackIndex = trackIndex + 1;

// 修复后(正确)
int ffmpegTrackIndex = (int)subtitleIndex - 1;

// VLC 索引 1 → FFmpeg 索引 0
// VLC 索引 2 → FFmpeg 索引 1

日志验证

[updateSubtitleUrl] iOS 26 拦截内嵌字幕: VLC subtitleIndex=1,
→ FFmpeg trackIndex=0  // 修复后正确映射到第一条字幕流

坑二:SRT 字幕的 <font> 标签问题

问题描述:内嵌字幕提取为 SRT 后,部分 SRT 仍无法显示中文。

日志分析

[ExtractSub] SRT 前 200 字:
1
00:00:00,000 --> 00:00:03,018
<font face="方正准圆简体" size="21"><b>...

根因分析:虽然 FFmpeg 提取时没有字体定义,但某些视频的字幕流本身已包含 <font face="xxx"> 标签。这些标签导致 VLC 的 freetype 渲染器尝试加载指定字体,同样失败并 fallback 到被拦截的路径。

修复:在加载 SRT 前,批量替换所有 <font face="xxx"> 标签:

NSString *srtText = [NSString stringWithContentsOfFile:srtUrl.path encoding:NSUTF8StringEncoding error:nil];
NSString *replaced = [self replaceSrtFontNamesInText:srtText];
[replaced writeToFile:srtUrl.path atomically:YES encoding:NSUTF8StringEncoding error:nil];

坑三:字符串匹配无法覆盖所有字体

问题描述:硬编码的字体名列表无法覆盖所有可能出现的字体名。

原方案

NSArray *fontNamesToReplace = @[
    @"微软雅黑", @"微软雅黑", @"SimHei", @"SimSun",
    @"黑体", @"宋体", @"楷体", // ...
];

问题:总有漏网之鱼,如 方正准圆简体Noto Sans CJK SC 等。

改进方案:正则 + 字符串匹配兜底

正则覆盖任意字体名,字符串匹配处理边缘情况:

// 正则:替换所有 {\fn任意字体名} → {\fnSource Han Sans CN}
// 兜底:字符串匹配常见字体名
modifiedText = [self fallbackReplaceFontNamesInText:modifiedText];

完整架构图

┌─────────────────────────────────────────────────────────────────┐
                    iOS 26 字幕兼容架构                            
├─────────────────────────────────────────────────────────────────┤
                                                                 
  ┌─────────────┐     ┌─────────────┐     ┌─────────────┐        
   handleiOS26       updateSubtitleUrl  convertSubtitle    
   SubtitleOn          (内嵌拦截)          (外挂处理)         
     Playing                                               
  └──────┬──────┘     └──────┬────────┘  └──────┬────────┘       
                                                              
                                                              
  ┌──────────────────────────────────────────────────────────┐   
                @available(iOS 26.0, *) 守卫                    
  └──────────────────────────────────────────────────────────┘   
                                                              
                                                              
  ┌──────────────┐   ┌──────────────┐   ┌──────────────┐         
   禁用内嵌字幕       FFmpeg 提取       字体名替换             
   (libass)         SRT              (正则+兜底)            
  └──────────────┘   └──────┬───────┘   └──────────────┘         
                                                                
                                                                
                    ┌──────────────┐                             
                     freetype 渲染                              
                     + 指定中文字体                              
                    └──────────────┘                             
                                                                 
└─────────────────────────────────────────────────────────────────┘

参考资料

Swift 方法派发机制深度解析 —— 兼与 Objective-C `objc_msgSend` 对比

作者 visual_zhang
2026年4月20日 16:44

基于 Swift 5.10(含部分 Swift 6 行为)与现代 Objective-C(ARC + LLVM clang)。 文中代码片段为最小化示例,仅作为论点的佐证。


核心要点

派发方式 调用开销 触发条件 可被 Hook 典型场景
Static Dispatch(直接派发) 最低,可内联 struct/enum 方法、final、全局函数、@inlinable 值类型、性能敏感路径
V-Table Dispatch(虚表派发) 一次间接跳转 class 的非 final 方法(无 @objc 普通 Swift 类继承
Witness Table Dispatch 一次表查 + 一次间接跳转 通过协议变量调用协议方法 面向协议编程
Message Dispatch(OC objc_msgSend SEL→IMP 查表(带缓存) @objc dynamic、继承自 NSObject 且未优化 是(Swizzle/KVO) OC 互操作、AOP

一句话总结:Swift 默认追求"能静态就静态",只在继承、协议、互操作三条路径上才退化为表派发或消息派发;OC 则把所有方法调用统一成 objc_msgSend 的动态消息查找,灵活但每次调用都要付出查表代价


1. 为什么要谈"派发"

方法派发(method dispatch)就是编译器/运行时如何把"调用某方法"翻译成"跳转到某段机器指令"

派发方式直接决定三件事:

  • 性能:是否能内联、是否要查表、是否能命中分支预测。
  • 可扩展性:能不能在运行时替换实现(Swizzle、KVO、Mock)。
  • 二进制兼容:库的方法表布局变化是否会破坏调用方。

OC 与 Swift 在这三个维度上的设计哲学截然相反,下面分别拆解。


2. Objective-C:一切皆消息

2.1 objc_msgSend 的本质

OC 中 [obj doSomething:arg] 在编译期不会被解析为某个具体函数地址,而是被翻译成:

((void (*)(id, SEL, id))objc_msgSend)(obj, @selector(doSomething:), arg);

objc_msgSend 是一段手写汇编,做的事情大致是:

1. 取 obj->isa 拿到 Class
2. 在 Class 的 method cache 里按 SEL hash 查 IMP
3. 命中 → 直接 jmp IMP(尾调用,不入栈)
4. 未命中 → __class_lookupMethodAndLoadCache3 走 method list / 父类链
5. 查到 → 写回 cache
6. 仍找不到 → 进入 forwarding(resolveInstanceMethod / forwardingTargetForSelector / forwardInvocation)

⚠️ 实战提示objc_msgSend 的 cache 是 per-class 的开放寻址哈希表,命中率通常 > 95%。这意味着"OC 方法慢"的直觉并不准确——在热路径上一次调用通常只多花几个时钟周期,远低于一次 cache miss。真正的成本不在派发本身,而在无法内联导致优化器失去全局视野。

2.2 消息派发带来的能力

消息派发让以下能力成为零成本默认值:

  • Method Swizzling:替换 Class 的 method list 即可全局劫持。
  • KVO:runtime 动态生成 NSKVONotifying_XXX 子类并替换 isa
  • 响应链 / Target-ActionUIApplication sendAction:to:from:forEvent: 完全建立在 SEL 之上。
  • 消息转发forwardInvocation: 让一个对象"假装"实现某协议,是 OC 多代理与 RPC 框架的基石。

代价是:编译期不知道 IMP 是什么,全程无法内联,也无法做跨方法优化


3. Swift:四种派发方式共存

Swift 没有"统一派发"的设计,而是根据声明上下文挑选最便宜的合法方式。理解 Swift 性能模型的关键,就是搞清"什么场景用哪种"。

3.1 Static Dispatch(直接派发)

调用直接编译成 call <symbol>,可被内联、可被常量折叠。触发条件:

  • structenum 的所有方法(值类型不存在继承)
  • class 中标了 final 的方法、或 final class 的全部方法
  • private 方法(编译器能证明无覆写)
  • 全局函数、static 函数
  • @inlinable / @_transparent 修饰的方法
struct Counter {
    var value = 0
    mutating func tick() { value += 1 }
}

var c = Counter()
c.tick()

c.tick()-O 下会被完全内联,最终汇编里看不到调用指令,只剩一条 add

3.2 V-Table Dispatch(虚表派发)

Swift 的 class 与 C++ 类似,每个类有一张 V-Table(在 metadata 末尾),按声明顺序存放方法指针。调用时:

1. 从 obj 的 metadata 偏移取 V-Table
2. 按方法在表中的固定 index 取 IMP
3. call IMP

只有一次表查 + 一次间接跳转,比 objc_msgSend 少了 SEL hash 与 cache 命中判断,但因为是间接调用,仍然无法跨方法内联

class Animal {
    func speak() { print("...") }
}
final class Dog: Animal {
    override func speak() { print("woof") }
}

Animalspeak 通过 V-Table 派发;Dog 因为 final,其调用点会被去虚化(devirtualize)回 static dispatch。

3.3 Witness Table Dispatch(协议见证表)

通过协议变量调用协议方法时,Swift 用一张 Protocol Witness Table(PWT):每个"类型 × 协议"对生成一张表,表里按协议方法的固定 index 存放该类型的具体实现。

protocol Drawable {
    func draw()
}
struct Circle: Drawable { func draw() { /* ... */ } }

func render(_ d: Drawable) {
    d.draw()
}

render 拿到的 d 是一个 existential container(值 + 类型 metadata + PWT 指针)。d.draw() 实际是:

1. 从 existential container 取 PWT
2. 按 draw 在协议中的 index 取 witness
3. call witness(witness 内部再 call 真正的实现)

⚠️ 实战坑some Drawable(opaque return type)和 Drawable(existential)的派发完全不同。前者编译期就确定了具体类型,可以走 static dispatch + 单态化(specialization),后者必须走 PWT。把热路径里的 func make() -> Drawable 改成 func make() -> some Drawable,在 SwiftUI / 集合操作里能拿到数量级的性能提升。

3.4 Message Dispatch(走 objc_msgSend

Swift 在以下两种情况会退化到 OC 的消息派发

  • 显式标注 @objc dynamic
  • 类继承自 NSObject,且方法满足 @objc 暴露规则,没有被去虚化优化
class MyVC: UIViewController {
    @objc dynamic func reload() { /* ... */ }
}

只有 @objc dynamic 的方法是保证objc_msgSend 的,因此也只有它能被 KVO / Method Swizzling 劫持。仅 @objc 不带 dynamic 的方法,编译器仍可能选择 V-Table 派发。


4. 派发规则速查表

把"声明位置 × 修饰符"组合起来,就是一张完整的决策表:

声明上下文 默认派发 final @objc @objc dynamic
struct / enum 方法 Static 不允许 不允许
class 直接定义的方法 V-Table Static V-Table(兼可 OC 调) Message
class extension 中的方法 Static Static V-Table Message
protocol 要求的方法 Witness Message(要求 @objc protocol Message
protocol extension 默认实现 Static 不允许 不允许
NSObject 子类的方法 V-Table Static V-Table Message

几条容易踩的经验法则:

  • extension 中定义的方法即使被子类重写也不会触发动态派发,重写会被静默忽略。要支持覆写就把方法定义放回类主体。
  • 协议 extension 的"默认实现"是 static 的,不会走 PWT。如果某个类型实现了同名方法,但调用方持有的是协议变量,仍可能调到 default 实现(这是经典面试题)。
  • @objcdynamic:前者只是给 OC runtime 暴露符号,后者才强制走消息派发。

5. 性能:到底差多少

简化的相对开销(命中 cache、无优化干扰的情况下):

派发方式 相对开销 备注
Inlined static ~1× 实质上没有调用
Direct call (static) ~1× 一条 call
V-Table ~1.5–2× 一次 load + 间接 call
Witness Table ~2× 与 V-Table 量级相同
objc_msgSend(cache 命中) ~3–5× 多了 SEL hash 与 cache 比对
objc_msgSend(cache miss) 数十× 走 method list 查找

真实业务里这些差异通常被淹没,例如 UI 主线程一次 reloadData 的耗时可能是 10ms 量级,几百次 objc_msgSend 完全可以忽略。性能差异真正显著的场景是:

  • 集合的内层热循环map / filter / 自定义 reduce)
  • 每帧调用的渲染回调CADisplayLink、SwiftUI 的 body 求值)
  • 大量小对象的属性 getter/setter(特别是泛型容器)

6. 选型与最佳实践

6.1 写 Swift 类型时

  • 默认优先 struct,需要引用语义或 OC 互操作再用 class
  • class 不需要继承时直接 final class,让编译器去虚化。
  • 协议返回值能用 some P 就别用 P,能用 any P 就别忘加 any 让代码意图清晰。
  • 性能敏感的 ABI 稳定库导出 API 时配合 @inlinable + @usableFromInline

6.2 需要动态能力时

  • 要被 KVO 监听 → @objc dynamic var ...
  • 要被 Swizzle / Aspect → @objc dynamic func ...
  • 要在 OC 代码里调用 → @objc(不必加 dynamic
  • 要做 Mock / Stub → 优先用协议依赖注入,而不是 Swizzle

6.3 OC 仍不可替代的场景

公平起见,OC 的消息派发模型在以下场景仍有 Swift 难以替代的优势:

维度 OC 占优的原因
编译速度 没有泛型单态化、类型推断爆炸,增量编译显著快于 Swift
运行时反射 class_copyMethodList / class_copyIvarList 等一整套 runtime API
二进制体积 没有 witness table / metadata 膨胀,纯 OC 类的 metadata 极小
AOP / Hook 生态 Aspect、Stinger、KVO 都建立在 objc_msgSend 之上,Swift 难以等价替代
C / C++ 互操作 与 C 二进制接口零成本互通

工程实践中,底层基础库(埋点、网络、监控)继续用 OC,业务层迁 Swift,往往是兼顾性能与生产力的最稳妥分工。


7. 一个综合案例

下面这段代码同时触发了四种派发,是理解 Swift 派发模型的一个好对照:

@objc protocol Refreshable { func refresh() }

class Base: NSObject, Refreshable {
    func refresh() { print("base") }
}
final class Leaf: Base {
    override func refresh() { print("leaf") }
}

let a: Refreshable = Leaf()
let b: Base       = Leaf()
let c: Leaf       = Leaf()
a.refresh()
b.refresh()
c.refresh()
  • a.refresh()Refreshable@objc protocol,走 objc_msgSend
  • b.refresh()Base 继承 NSObject,编译器保守起见走 V-Table(若 Base 也是 final,可去虚化)。
  • c.refresh()Leaffinal,编译器去虚化为 Static,可被内联。

把同一个方法在三种持有方式下分别调用,得到三种不同的派发路径——这是 OC 永远不会出现的现象,也是 Swift 性能调优最容易被忽略的细节。

【SwiftyJSON】拯救你的 as? [String: Any]——链式 JSON 访问的正确姿势

作者 探索者dx
2026年4月20日 09:22

【SwiftyJSON】拯救你的 as? [String: Any]——链式 JSON 访问的正确姿势

iOS三方库精读 · 第 15 期


一、一句话介绍

SwiftyJSON 是一个用于 iOS/macOS 的 JSON 解析辅助库,它通过链式下标访问和安全类型转换,让原本需要大量 as? 强转和 guard let 解包的 JSON 解析代码,变成像访问字典一样直观的单行操作。

属性 信息
⭐ GitHub Stars 22k+
最新稳定版 5.0.2
License MIT
支持平台 iOS 13+ / macOS 11+
语言 Swift(纯 Swift,无 OC 接口)

二、为什么选择它

原生痛点

原生 JSONSerialization 解析复杂 JSON 的体验:

// ❌ 原生方式:每层都要 as? + guard,代码量爆炸
guard let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
      let user = json["user"] as? [String: Any],
      let profile = user["profile"] as? [String: Any],
      let bio = profile["bio"] as? String,
      let score = profile["score"] as? Double else {
    return
}

5 层嵌套,1 个字段。如果某层返回 null,整个 guard 失败,无法优雅降级。

SwiftyJSON 方式:

// ✅ SwiftyJSON:一行,安全,不崩溃
let bio   = json["user"]["profile"]["bio"].stringValue   // 不存在则 ""
let score = json["user"]["profile"]["score"].doubleValue // 不存在则 0.0

核心优势:

  1. 链式下标:无论嵌套多深,中间路径不存在也不崩溃
  2. 类型转换属性.stringValue / .intValue / .boolValue 自动转换 + 默认值
  3. Optional 版本.string / .int / .bool 返回 Optional,可 if let
  4. 数组/字典直接遍历.arrayValue / .dictionaryValue
  5. null 安全.isNull.exists() 区分"不存在"和"存在但为 null"

三、核心功能速览

基础层(新手必读)

环境集成
// SPM
// URL: https://github.com/SwiftyJSON/SwiftyJSON.git
// from: "5.0.2"
# CocoaPods
pod 'SwiftyJSON', '~> 5.0'
创建 JSON 对象
import SwiftyJSON

// 从 Data 创建(最常用)
let json = JSON(data)

// 从字典/数组创建
let json2 = JSON(["name": "Alice", "age": 25])

// 从字符串创建
let json3 = JSON(parseJSON: "{\"key\": \"value\"}")
值访问:xValue vs x(Optional)
// .stringValue → String(不存在时返回 "")
// .string      → String?(不存在时返回 nil)
let name1 = json["user"]["name"].stringValue  // "Alice" 或 ""
let name2 = json["user"]["name"].string       // "Alice" 或 nil

// 其他类型同理
json["count"].intValue      // Int,默认 0
json["count"].int           // Int?
json["score"].doubleValue   // Double,默认 0.0
json["score"].double        // Double?
json["active"].boolValue    // Bool,默认 false
json["active"].bool         // Bool?

进阶层(最佳实践)

数组遍历
// arrayValue: 返回 [JSON],安全(不存在返回 [])
for item in json["user"]["repos"].arrayValue {
    let name  = item["name"].stringValue
    let stars = item["stars"].intValue
    print("\(name): ⭐ \(stars)")
}

// 快速 map
let repoNames = json["user"]["repos"].arrayValue
    .map { $0["name"].stringValue }
    .filter { !$0.isEmpty }
字典遍历
// dictionaryValue: 返回 [String: JSON]
for (key, value) in json["user"]["metadata"].dictionaryValue {
    print("\(key): \(value)")
}
Null 处理
let field = json["user"]["lastLogin"]

// 区分"不存在"和"存在但为 null"
print(field.exists())  // false → 路径不存在
print(field.isNull)    // true  → 路径不存在或值为 null

// 带默认值的安全访问
let last = json["user"]["lastLogin"].string ?? "从未登录"
整数索引访问数组
let firstTag = json["user"]["tags"][0].stringValue    // "swift"
let lastRepo  = json["user"]["repos"][2]["name"].stringValue  // "TodoApp"
SwiftyJSON 转回 Data / 字典
// 转回 Data(用于 Codable 混用)
let rawData = try? json["user"]["repos"].rawData()

// 转回 [String: Any]
let rawDict = json.dictionaryObject  // [String: Any]?
let rawArr  = json.arrayObject       // [Any]?
与 Codable 混用(最佳实践)
// 用 SwiftyJSON 做"柔性"部分,Codable 做"结构化"部分
let json = JSON(data)

// 1. 取出子 JSON(SwiftyJSON 处理不确定的动态结构)
let extraInfo = json["response"]["extra"]  // 动态字段,结构不定

// 2. 将确定结构的部分转为 Codable
if let reposData = try? json["user"]["repos"].rawData() {
    let repos = try? JSONDecoder().decode([Repo].self, from: reposData)
}

深入层(源码视角)

JSON 的枚举本质

SwiftyJSON 的核心是一个 JSON 结构体,内部用枚举表示类型:

public struct JSON {
    // 内部存储联合类型
    fileprivate var rawArray: [Any] = []
    fileprivate var rawDictionary: [String: Any] = [:]
    fileprivate var rawString: String = ""
    fileprivate var rawNumber: NSNumber = 0
    fileprivate var rawNull: NSNull = NSNull()
    fileprivate var rawBool: Bool = false

    public internal(set) var type: Type = .null
}

subscript 访问时,如果类型不匹配或 key 不存在,返回一个 JSON.null 单例而非崩溃。这是链式访问安全性的核心保障。

性能注意

每次 subscript 访问都会创建新的 JSON 实例(值类型复制),深层链式访问在循环中可能造成性能开销。热路径代码建议:

// ❌ 在循环中重复深层访问
for _ in 0..<10000 {
    let _ = json["a"]["b"]["c"]["d"].stringValue
}

// ✅ 缓存中间节点
let profile = json["a"]["b"]  // 只创建一次
for _ in 0..<10000 {
    let _ = profile["c"]["d"].stringValue
}

四、实战演示

场景:解析 GitHub API 响应

// 解析 https://api.github.com/search/repositories?q=swift 的响应
func parseSearchResult(data: Data) -> [String] {
    let json = JSON(data)

    // 总数
    let total = json["total_count"].intValue
    print("找到 \(total) 个仓库")

    // 取前 5 个仓库名
    return json["items"].arrayValue.prefix(5).map { repo in
        let name  = repo["full_name"].stringValue
        let stars = repo["stargazers_count"].intValue
        let lang  = repo["language"].string ?? "Unknown"
        return "\(name)\(stars) [\(lang)]"
    }
}

五、源码亮点

进阶层:链式安全的实现

// SwiftyJSON 的 subscript 关键实现
public subscript(key: String) -> JSON {
    get {
        if type == .dictionary {
            if let value = rawDictionary[key] {
                return JSON(value)
            }
        }
        return JSON.null  // ← 不崩溃,返回 null JSON
    }
}

JSON.null 是一个静态单例,所有对它的 subscript 访问都继续返回自身,形成"null 传播链",这就是为什么 json["a"]["b"]["c"]["d"] 即便 "a" 不存在也不会 crash。

深入层:与 Codable 的本质区别

维度 SwiftyJSON Codable
解析时机 运行时,按需访问 解码时一次性反序列化
类型错误 运行时,返回默认值 编译时 / 解码时抛错
内存占用 保留完整 JSON 树 只保留 struct/class 数据
适用场景 探索、动态结构 固定 API 模型

六、踩坑记录

问题 1:.string 返回 nil.stringValue 返回空字符串

  • 原因:JSON 中该字段是 null 或类型是 Number,.string 只在类型是 String 时返回非 nil
  • 解决:根据场景选择:string ?? "默认值".stringValue;如果需要 Number → String 转换:
    let val = json["count"].string ?? json["count"].numberValue.stringValue
    

问题 2:修改 SwiftyJSON 的值没有生效

  • 原因JSON 是值类型(struct),赋值后修改的是副本
  • 解决
    var json = JSON(data)
    json["user"]["name"] = "New Name"  // ✅ 使用 subscript setter
    

问题 3:Swift Package Manager 找不到模块

  • 原因:SwiftyJSON 的 SPM 包名是 SwiftyJSON,但有时大小写不一致
  • 解决:确保 import SwiftyJSON(大驼峰),检查 SPM 依赖是否成功解析

问题 4:OC 项目无法使用 SwiftyJSON

  • 原因:SwiftyJSON 是纯 Swift,OC 不能直接 import
  • 解决:OC 项目用 NSJSONSerialization + YYModel / MJExtension,或在 Swift 桥接层封装

问题 5:解析性能在大量数据时较差

  • 原因:SwiftyJSON 在内部创建大量临时 JSON 实例
  • 解决:大量数据(10w+ 条)时改用 Codable,小量动态数据 SwiftyJSON 够用

七、延伸思考

JSON 解析方案全景对比

方案 类型安全 动态 JSON OC 支持 性能 推荐场景
SwiftyJSON 运行时 ✅ 最好 中等 探索/动态结构
Codable 编译时 ⚠️ 需 AnyCodable 固定 API 模型
YYModel (OC) 运行时 OC 项目
ObjectMapper 运行时 中等 Swift,已有项目
NSJSONSerialization 简单/OC 场景

推荐原则

新项目 Swift:优先 Codable,复杂动态 JSON 用 SwiftyJSON 辅助。 老项目 OCNSJSONSerialization + YYModel / MJExtension。 混合项目:在 Swift 层用 Codable 建模,可选 SwiftyJSON 处理边界情况。


八、参考资源


九、本期互动

小作业

用 SwiftyJSON 解析一个真实 API(如 GitHub / 豆瓣 / OpenWeather),要求:处理嵌套 3 层以上的 JSON,包含数组遍历和 null 字段处理,最终展示在 UITableView 中。评论区分享你选的 API 和最复杂的解析路径。

思考题

SwiftyJSON 用"null 传播"(路径不存在时返回 JSON.null 而非 crash)来保证安全性,而 Swift Codable 用 Optionalthrows 来保证类型安全。这两种设计哲学各有什么权衡?在什么情况下"静默返回默认值"比"抛出错误"更合适?

读者征集

下一期我们将深入 R.swift(编译时安全的资源访问)。你在项目中遇到过资源文件名拼写错误导致运行时崩溃吗?你目前是如何管理图片/字体/颜色等资源的?欢迎评论区分享你的资源管理方案。


📅 本系列每周五晚更新 ✅ 第11期:DGCharts · ✅ 第12期:Hero · ✅ 第13期:Realm · ✅ 第14期:Moya · ➡️ 第15期:SwiftyJSON · ○ 第16期:R.swift

【Moya】为什么你的 Alamofire 代码需要再封装一层?

作者 探索者dx
2026年4月20日 09:21

【Moya】为什么你的 Alamofire 代码需要再封装一层?

iOS三方库精读 · 第 14 期


一、一句话介绍

Moya 是一个建立在 Alamofire 之上的网络抽象层库,它用 TargetType 协议将所有 API 接口声明为 Swift 枚举 case,让网络请求从"散落在各处的字符串 URL"变成"编译器可检查的类型化接口",同时内置单元测试 Stubbing 和 Plugin 拦截机制。

属性 信息
⭐ GitHub Stars 15k+
最新稳定版 15.0.3
License MIT
支持平台 iOS 13+
语言 Swift(纯 Swift,无 OC 接口)
依赖 Alamofire 5.x

二、为什么选择它

原生痛点

直接使用 Alamofire 或 URLSession 时,常见的问题:

// ❌ 硬编码 URL,散落在各处
AF.request("https://api.example.com/users/\(userId)/profile",
           method: .get,
           parameters: ["include": "avatar"],
           headers: ["Authorization": "Bearer \(token)"])
  • URL 字符串:运行时才发现拼写错误
  • 参数类型parameters: [String: Any],传错类型编译器不报错
  • 认证 Token:每个请求都要手动加 headers
  • Mock 测试:需要替换整个 URLSession,实现复杂
  • 接口文档:散落在业务代码里,难以统一维护

Moya 的解决方案:

  1. TargetType 协议:将每个 API 的 URL、方法、参数、headers 集中声明
  2. 类型安全:枚举 case 关联值确保参数类型正确,编译时报错
  3. Plugin 系统:一处注入 Token,所有请求自动携带
  4. 内置 StubbingsampleData + StubbingProvider,无需 mock URLSession

三、核心功能速览

基础层(新手必读)

环境集成
// SPM
// URL: https://github.com/Moya/Moya.git
// from: "15.0.3"
// Products: Moya(基础)/ RxMoya(RxSwift)/ CombineMoya(Combine)
# CocoaPods
pod 'Moya', '~> 15.0'
pod 'Moya/RxSwift'    # 可选
pod 'Moya/Combine'    # 可选
TargetType 完整示例
import Moya

enum UserAPI {
    case login(email: String, password: String)
    case profile(userId: Int)
    case updateAvatar(data: Data)
    case logout
}

extension UserAPI: TargetType {
    var baseURL: URL { URL(string: "https://api.example.com/v2")! }

    var path: String {
        switch self {
        case .login:              return "/auth/login"
        case .profile(let id):   return "/users/\(id)"
        case .updateAvatar:      return "/users/avatar"
        case .logout:            return "/auth/logout"
        }
    }

    var method: Moya.Method {
        switch self {
        case .login, .updateAvatar: return .post
        case .logout:               return .delete
        case .profile:              return .get
        }
    }

    var task: Task {
        switch self {
        case .login(let email, let pw):
            return .requestParameters(
                parameters: ["email": email, "password": pw],
                encoding: JSONEncoding.default
            )
        case .updateAvatar(let data):
            let formData = MultipartFormData(provider: .data(data),
                                              name: "file",
                                              fileName: "avatar.jpg",
                                              mimeType: "image/jpeg")
            return .uploadMultipart([formData])
        default:
            return .requestPlain
        }
    }

    var headers: [String: String]? { ["Content-Type": "application/json"] }

    // 单元测试用的 Stub 数据
    var sampleData: Data {
        switch self {
        case .login:
            return """{"token":"test_token","userId":1}""".data(using: .utf8)!
        default:
            return Data()
        }
    }
}
发起请求
let provider = MoyaProvider<UserAPI>()

// 回调方式
provider.request(.login(email: "test@example.com", password: "123456")) { result in
    switch result {
    case .success(let response):
        let json = try? response.mapJSON()
        print(json ?? "")
    case .failure(let error):
        print(error)
    }
}

// 直接 map 到 Codable 模型
provider.request(.profile(userId: 42)) { result in
    if case .success(let response) = result {
        let user = try? response.map(User.self)
    }
}

进阶层(最佳实践)

Plugin 系统:拦截所有请求
// 统一注入认证 Token
struct TokenPlugin: PluginType {
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        var req = request
        if let token = AuthManager.shared.token {
            req.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
        }
        return req
    }

    // 401 自动触发 token 刷新
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        if case .success(let response) = result, response.statusCode == 401 {
            AuthManager.shared.refreshToken()
        }
    }
}

// 统计 API 耗时
struct MetricsPlugin: PluginType {
    func willSend(_ request: RequestType, target: TargetType) {
        Analytics.trackStart(api: target.path)
    }
    func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
        Analytics.trackEnd(api: target.path)
    }
}

// 组合多个 Plugin
let provider = MoyaProvider<UserAPI>(plugins: [
    TokenPlugin(),
    MetricsPlugin(),
    NetworkLoggerPlugin()  // Moya 内置日志插件
])
单元测试:Stubbing
// 无需真实网络,立即返回 sampleData
let testProvider = MoyaProvider<UserAPI>(stubClosure: MoyaProvider.immediatelyStub)

// 延迟返回(模拟网络延迟)
let testProvider2 = MoyaProvider<UserAPI>(
    stubClosure: MoyaProvider.delayedStub(0.5)  // 延迟 0.5s
)

// 测试代码
func testLogin() {
    testProvider.request(.login(email: "test@example.com", password: "123")) { result in
        switch result {
        case .success(let response):
            XCTAssertEqual(response.statusCode, 200)
            let model = try? response.map(LoginResponse.self)
            XCTAssertNotNil(model?.token)
        case .failure:
            XCTFail()
        }
    }
}
Combine 集成
import Combine

class UserViewModel: ObservableObject {
    @Published var user: User?
    @Published var error: String?

    private var cancellables = Set<AnyCancellable>()
    private let provider = MoyaProvider<UserAPI>()

    func loadProfile(userId: Int) {
        provider.requestPublisher(.profile(userId: userId))
            .tryMap { try $0.map(User.self) }
            .receive(on: DispatchQueue.main)
            .sink(
                receiveCompletion: { [weak self] completion in
                    if case .failure(let error) = completion {
                        self?.error = error.localizedDescription
                    }
                },
                receiveValue: { [weak self] user in
                    self?.user = user
                }
            )
            .store(in: &cancellables)
    }
}
RxSwift 集成
import RxSwift

provider.rx.request(.searchRepos(query: "swift"))
    .map(RepoSearchResult.self)
    .observe(on: MainScheduler.instance)
    .subscribe(
        onSuccess: { result in print(result.items) },
        onFailure: { error in print(error) }
    )
    .disposed(by: disposeBag)

深入层(源码视角)

MoyaProvider 的请求流程
provider.request(.login(...))
    ↓
MoyaProvider.requestNormal
    ↓
调用所有 Plugin.prepare(修改 URLRequest)
    ↓
调用 Plugin.willSend(发送前通知)
    ↓
Alamofire.request(真正发网络请求)
    ↓
收到响应
    ↓
调用 Plugin.didReceive(响应后通知)
    ↓
回调 completion handler

Moya 本质是 Alamofire 的装饰器(Decorator),所有的实际网络操作都委托给 Alamofire,Moya 只负责协议声明、Plugin 拦截、Stub 切换。

Task 枚举的设计

Task 枚举涵盖了所有常见请求类型:

public enum Task {
    case requestPlain                              // 无 body
    case requestData(_ data: Data)                 // 原始 Data
    case requestParameters(parameters:encoding:)   // URL 参数 or JSON body
    case uploadMultipart(_ data: [MultipartFormData]) // 文件上传
    case downloadDestination(_ destination: DownloadDestination) // 文件下载
    case uploadCompositeMultipart(_, urlParameters:)  // 混合上传
    // ...
}

这种穷举枚举设计确保了所有请求形式都有类型安全的表达方式。


四、实战演示

场景:统一网络层封装(生产级模板)

// 1. 定义 API Target
enum NewsAPI {
    case topHeadlines(country: String, page: Int)
    case article(id: String)
}

extension NewsAPI: TargetType {
    var baseURL: URL { URL(string: "https://newsapi.org/v2")! }
    var path: String {
        switch self {
        case .topHeadlines: return "/top-headlines"
        case .article(let id): return "/articles/\(id)"
        }
    }
    var method: Moya.Method { .get }
    var task: Task {
        switch self {
        case .topHeadlines(let country, let page):
            return .requestParameters(
                parameters: ["country": country, "page": page, "pageSize": 20],
                encoding: URLEncoding.queryString
            )
        case .article: return .requestPlain
        }
    }
    var headers: [String: String]? { nil }
    var sampleData: Data { Data() }
}

// 2. Service 层封装(屏蔽 Moya 细节)
final class NewsService {
    private let provider = MoyaProvider<NewsAPI>(plugins: [
        TokenPlugin(),
        NetworkLoggerPlugin()
    ])

    func fetchHeadlines(country: String, page: Int) async throws -> [Article] {
        return try await withCheckedThrowingContinuation { cont in
            provider.request(.topHeadlines(country: country, page: page)) { result in
                switch result {
                case .success(let response):
                    do {
                        let articles = try response.map([Article].self, atKeyPath: "articles")
                        cont.resume(returning: articles)
                    } catch {
                        cont.resume(throwing: error)
                    }
                case .failure(let error):
                    cont.resume(throwing: error)
                }
            }
        }
    }
}

// 3. 业务层调用
let service = NewsService()
Task {
    let articles = try await service.fetchHeadlines(country: "cn", page: 1)
}

五、源码亮点

进阶层

TargetType 作为抽象屏障

Moya 的 MoyaProvider<Target: TargetType> 是泛型类型,每种 API 有独立的 Provider 实例。这意味着:

  • 不同 API 服务(UserAPI / ProductAPI / OrderAPI)完全隔离
  • 每个 Provider 可以配置不同的 Plugin(如不同的 Token 策略)
  • 测试时替换 Provider 无需修改任何业务代码

深入层:网络层的 SOLID 原则

Moya 的设计完美体现了 SOLID 原则:

原则 体现
Single Responsibility TargetType 只描述接口声明,Provider 只负责执行
Open/Closed 新增 API 只需新增枚举 case,不修改 Provider
Liskov Substitution StubbingProvider 可无缝替换真实 Provider
Interface Segregation Plugin 协议的每个方法都是可选实现
Dependency Inversion 业务代码依赖 TargetType 协议,而非具体 URL 字符串

六、踩坑记录

问题 1:sampleData 返回空 Data 导致测试解析失败

  • 原因:使用 StubbingProvider 但忘记实现 sampleData
  • 解决:为每个需要测试的 case 提供合法 JSON 的 sampleData

问题 2:Plugin.prepare 中修改 headers 无效

  • 原因URLRequest 是值类型,必须先 copy 再修改
  • 解决
    func prepare(_ request: URLRequest, target: TargetType) -> URLRequest {
        var req = request  // ← copy 一份
        req.setValue("Bearer xxx", forHTTPHeaderField: "Authorization")
        return req         // ← 返回修改后的副本
    }
    

问题 3:Moya 请求不在主线程回调

  • 原因:默认 callbackQueue.main,但某些版本或配置下可能改变
  • 解决:UI 更新前显式 DispatchQueue.main.async { ... } 或使用 .receive(on: MainScheduler.instance)

问题 4:多个 API 服务需要不同 baseURL

  • 原因:Moya 一个 TargetType 对应一个 baseURL
  • 解决:拆分为多个 enum(UserAPI, ProductAPI),各自独立 Provider

问题 5:文件上传进度无法监听

  • 原因:回调方式无进度回调
  • 解决
    provider.request(.uploadAvatar(data: imageData)) { result in ... }
    // 上面不支持进度,改用:
    provider.requestWithProgress(.uploadAvatar(data: imageData)) { progress in
        print("上传进度:", progress.progress)
    }
    

七、延伸思考

同类方案对比

方案 类型安全 测试友好 学习成本 OC 支持 推荐场景
URLSession ❌ 字符串 需 mock 超简单场景
Alamofire ⚠️ 需封装 需封装 中型 App
Moya ✅ 枚举 ✅ 内置 中大型 Swift App
Apollo(GraphQL) ✅ 代码生成 ⚠️ GraphQL API

推荐使用场景

  • ✅ API 接口较多(20+)的中大型 App
  • ✅ 团队协作,需要统一的 API 文档化
  • ✅ 需要完整的单元测试覆盖网络层
  • ✅ 已经在使用 Alamofire 想升级架构

不推荐场景

  • ❌ API 极少(3 个以内)→ 直接 Alamofire 更简单
  • ❌ OC 项目 → Moya 纯 Swift,考虑 AFNetworking
  • ❌ 追求最小依赖体积 → URLSession 直接封装

八、参考资源


九、本期互动

小作业

用 Moya 封装一个天气 API 服务层:定义 WeatherAPI enum,包含"当前天气"和"5天预报"两个 case,实现 TargetType,并编写一个 TokenPlugin 注入 API Key,最后用 StubbingProvider 为两个接口各写一个单元测试。评论区分享你的 TargetType 实现。

思考题

Moya 的 TargetType 强制将一个服务的所有 API 放在同一个 enum 里。当 API 接口很多时(50+),这个大 enum 会变得难以维护。你会如何设计拆分方案?能否在不改变使用方代码的前提下实现"API 分模块管理"?

读者征集

下一期我们将深入 SwiftyJSON(JSON 解析利器)。你在处理复杂嵌套 JSON 时用过哪些方案(SwiftyJSON / Codable / ObjectMapper / 手动解析)?在 OC 项目中你是如何处理 JSON 的?欢迎评论区分享。


📅 本系列每周五晚更新 ✅ 第11期:DGCharts · ✅ 第12期:Hero · ✅ 第13期:Realm · ➡️ 第14期:Moya · ○ 第15期:SwiftyJSON

【AFNetworking】OC 时代网络请求事实标准,Alamofire 的前身

作者 探索者dx
2026年4月14日 00:39

【AFNetworking】OC 时代网络请求事实标准,Alamofire 的前身

iOS三方库精读 · 第 9 期


一、一句话介绍

AFNetworking 是 iOS / macOS 平台上最早也是最成功的 Objective-C HTTP 网络库,它让网络请求从繁琐的 NSURLConnection / NSURLSession API 变得简洁优雅,成为 OC 时代的事实标准。

属性
GitHub Stars 33k+
最新版本 4.0.0
License MIT
支持平台 iOS 9+ / macOS 10.10+ / watchOS / tvOS
维护状态 维护模式(Maintenance Mode)

二、为什么选择它

原生痛点(2011 年的 iOS 世界)

在没有 AFNetworking 之前,iOS 开发者不得不面对:

  • NSURLConnection 样板代码:每次请求都要实现 delegate 回调,代码分散难以维护
  • JSON 解析手动处理:iOS 5 之前没有 NSJSONSerialization,需要第三方库
  • 图片加载无缓存:网络图片每次都要重新下载,没有内存/磁盘缓存
  • 网络状态监听复杂:Reachability API 繁琐,代码量大
  • HTTPS 配置困难:自签名证书、SSL Pinning 需要深入了解安全 API

AFNetworking 核心优势

  1. 一行代码发起请求:GET / POST / PUT / DELETE 统一 API,无需 delegate
  2. 自动序列化:JSON / XML / Plist / Image 响应自动解析
  3. UIImageView CategorysetImageWithURL: 一行搞定网络图片加载
  4. Reachability 内建:实时监听网络状态,断网自动提示
  5. SSL Pinning 支持:证书校验一行配置,安全合规
  6. Block 回调:告别分散的 delegate,代码逻辑更清晰

原生 API vs AFNetworking

场景 原生 NSURLSession AFNetworking
GET 请求 10+ 行代码 + delegate [manager GET:success:failure:]
JSON 解析 NSJSONSerialization 手动调用 自动解析为 NSDictionary / NSArray
图片加载 需自行实现缓存 setImageWithURL: 一行
网络监听 Reachability 复杂 API setReachabilityStatusChangeBlock:
文件上传 构造 multipart 请求繁琐 POST:constructingBodyWithBlock:

三、核心功能速览

基础层 概念解释、环境配置、基础用法

环境配置

CocoaPods(OC 项目首选)

pod 'AFNetworking', '~> 4.0'

Swift Package Manager

// Package.swift
dependencies: [
    .package(url: "https://github.com/AFNetworking/AFNetworking.git", from: "4.0.0")
]

Swift 项目桥接

// {ProjectName}-Bridging-Header.h
#import <AFNetworking/AFNetworking.h>
#import "UIImageView+AFNetworking.h"

基础 GET 请求

// AFNetworking 4.x (OC)
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];

[manager GET:@"https://jsonplaceholder.typicode.com/users"
  parameters:nil
    progress:nil
     success:^(NSURLSessionDataTask *task, id responseObject) {
         NSLog(@"成功: %@", responseObject);
     }
     failure:^(NSURLSessionDataTask *task, NSError *error) {
         NSLog(@"失败: %@", error.localizedDescription);
     }];

进阶层 最佳实践、性能优化、线程安全

POST 请求带参数

NSDictionary *params = @{
    @"name": @"张三",
    @"email": @"zhangsan@example.com",
    @"age": @28
};

[manager POST:@"https://api.example.com/users"
   parameters:params
     progress:nil
      success:^(NSURLSessionDataTask *task, id responseObject) {
          NSLog(@"创建成功: %@", responseObject);
      }
      failure:^(NSURLSessionDataTask *task, NSError *error) {
          NSLog(@"创建失败: %@", error);
      }];

文件上传

NSData *imageData = UIImageJPEGRepresentation(image, 0.8);

[manager POST:@"https://api.example.com/upload"
   parameters:nil
    constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileData:imageData
                                    name:@"file"
                                fileName:@"photo.jpg"
                               mimeType:@"image/jpeg"];
    }
     progress:^(NSProgress *progress) {
         NSLog(@"上传进度: %.0f%%", progress.fractionCompleted * 100);
     }
      success:^(NSURLSessionDataTask *task, id responseObject) {
          NSLog(@"上传成功");
      }
      failure:^(NSURLSessionDataTask *task, NSError *error) {
          NSLog(@"上传失败: %@", error);
      }];

图片加载(UIImageView Category)

#import "UIImageView+AFNetworking.h"

// 基础用法
[imageView setImageWithURL:[NSURL URLWithString:@"https://example.com/avatar.jpg"]];

// 带占位图
[imageView setImageWithURL:[NSURL URLWithString:@"https://example.com/avatar.jpg"]
         placeholderImage:[UIImage imageNamed:@"default_avatar"]];

// 取消加载(cell 复用场景)
[imageView cancelImageDownloadTask];

网络状态监听

[[AFNetworkReachabilityManager sharedManager] setReachabilityStatusChangeBlock:
    ^(AFNetworkReachabilityStatus status) {
        switch (status) {
            case AFNetworkReachabilityStatusNotReachable:
                NSLog(@"无网络连接");
                break;
            case AFNetworkReachabilityStatusReachableViaWiFi:
                NSLog(@"WiFi 连接");
                break;
            case AFNetworkReachabilityStatusReachableViaWWAN:
                NSLog(@"蜂窝网络");
                break;
            default:
                break;
        }
    }];

[[AFNetworkReachabilityManager sharedManager] startMonitoring];

HTTPS 证书校验

manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
manager.securityPolicy.allowInvalidCertificates = YES;  // 允许自签名
manager.securityPolicy.validatesDomainName = YES;

深入层 源码解析、设计思想、扩展定制

核心类关系

AFURLSessionManager (核心)
    ├── AFHTTPSessionManager (HTTP 封装)
    │       ├── requestSerializer (请求序列化)
    │       └── responseSerializer (响应序列化)
    ├── AFSecurityPolicy (安全策略)
    └── AFNetworkReachabilityManager (网络监听)

Category 扩展:
    UIImageView+AFNetworking (图片加载)
    UIProgressView+AFNetworking (进度条)
    AFNetworkActivityIndicatorManager (状态栏网络指示器)

Session Manager 核心设计

// AFHTTPSessionManager 继承 AFURLSessionManager
// 职责分离:网络层、请求构建、响应解析各自独立

@interface AFHTTPSessionManager : AFURLSessionManager
@property (nonatomic, strong) AFHTTPRequestSerializer <AFURLRequestSerialization> *requestSerializer;
@property (nonatomic, strong) AFHTTPResponseSerializer <AFURLResponseSerialization> *responseSerializer;
@end

// 序列化协议设计
@protocol AFURLRequestSerialization <NSObject>
- (NSURLRequest *)requestBySerializingRequest:(NSURLRequest *)request
                                withParameters:(id)parameters
                                         error:(NSError * __autoreleasing *)error;
@end

四、实战演示

场景:完整的网络请求封装

// APIClient.h - 统一网络请求封装
#import <AFNetworking/AFNetworking.h>

@interface APIClient : AFHTTPSessionManager
+ (instancetype)sharedClient;

// 业务方法
- (void)fetchUsers:(void(^)(NSArray *users, NSError *error))completion;
- (void)createUser:(NSDictionary *)params
        completion:(void(^)(NSDictionary *user, NSError *error))completion;
- (void)uploadAvatar:(UIImage *)image
           completion:(void(^)(NSString *url, NSError *error))completion;
@end

// APIClient.m
@implementation APIClient

+ (instancetype)sharedClient {
    static APIClient *instance;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        NSURL *baseURL = [NSURL URLWithString:@"https://api.example.com/"];
        instance = [[self alloc] initWithBaseURL:baseURL];
        
        // 配置请求/响应序列化
        instance.requestSerializer = [AFJSONRequestSerializer serializer];
        instance.responseSerializer = [AFJSONResponseSerializer serializer];
        instance.requestSerializer.timeoutInterval = 30;
        
        // 添加通用请求头
        [instance.requestSerializer setValue:@"application/json"
                             forHTTPHeaderField:@"Content-Type"];
        
        // HTTPS 配置
        instance.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];
    });
    return instance;
}

- (void)fetchUsers:(void(^)(NSArray *users, NSError *error))completion {
    [self GET:@"users"
      parameters:nil
        progress:nil
         success:^(NSURLSessionDataTask *task, id responseObject) {
             completion(responseObject, nil);
         }
         failure:^(NSURLSessionDataTask *task, NSError *error) {
             completion(nil, error);
         }];
}

- (void)createUser:(NSDictionary *)params
        completion:(void(^)(NSDictionary *user, NSError *error))completion {
    [self POST:@"users"
   parameters:params
     progress:nil
      success:^(NSURLSessionDataTask *task, id responseObject) {
          completion(responseObject, nil);
      }
      failure:^(NSURLSessionDataTask *task, NSError *error) {
          completion(nil, error);
      }];
}

- (void)uploadAvatar:(UIImage *)image
           completion:(void(^)(NSString *url, NSError *error))completion {
    NSData *data = UIImageJPEGRepresentation(image, 0.8);
    
    [self POST:@"upload"
   parameters:nil
    constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
        [formData appendPartWithFileData:data
                                    name:@"avatar"
                                fileName:@"avatar.jpg"
                               mimeType:@"image/jpeg"];
    }
     progress:nil
      success:^(NSURLSessionDataTask *task, id responseObject) {
          NSString *url = responseObject[@"url"];
          completion(url, nil);
      }
      failure:^(NSURLSessionDataTask *task, NSError *error) {
          completion(nil, error);
      }];
}

@end

五、源码亮点

进阶层 值得借鉴的用法

序列化器组合模式

// 请求序列化器可插拔
manager.requestSerializer = [AFJSONRequestSerializer serializer];   // JSON
manager.requestSerializer = [AFPropertyListRequestSerializer serializer];  // Plist

// 响应序列化器可插拔
manager.responseSerializer = [AFJSONResponseSerializer serializer];  // JSON
manager.responseSerializer = [AFXMLParserResponseSerializer serializer];  // XML
manager.responseSerializer = [AFImageResponseSerializer serializer];  // Image

Block 回调替代 Delegate

// 传统 delegate 分散在多个方法
// AFNetworking 用 block 聚合逻辑
[manager GET:url
  parameters:params
    progress:^(NSProgress *progress) {
        // 进度回调
    }
     success:^(NSURLSessionDataTask *task, id responseObject) {
        // 成功回调
    }
     failure:^(NSURLSessionDataTask *task, NSError *error) {
        // 失败回调
    }];

深入层 设计思想解析

NSURLSession 封装架构

// AFURLSessionManager 核心方法
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
                            completionHandler:(void (^)(NSURLResponse *, id, NSError *))handler {
    // 1. 创建 task
    NSURLSessionDataTask *task = [self.session dataTaskWithRequest:request];
    
    // 2. 存储 task delegate(用于回调)
    self.mutableTaskDelegatesKeyedByTaskIdentifier[@(task.taskIdentifier)] = delegate;
    
    // 3. 返回 task
    return task;
}

// task delegate 处理各种回调
- (void)URLSession:(NSURLSession *)session
          dataTask:(NSURLSessionDataTask *)dataTask
    didReceiveData:(NSData *)data {
    // 累积数据,最终一次性回调
    [mutableData appendData:data];
}

UIImageView Category 的缓存设计

// UIImageView+AFNetworking 内部使用 AFImageDownloader
// 自动实现内存缓存 + 磁盘缓存

@interface AFImageDownloader : NSObject
@property (nonatomic, strong) AFImageCache *imageCache;  // 内存缓存
@property (nonatomic, strong) NSURLCache *urlCache;       // 磁盘缓存
@end

// 缓存策略
- (void)downloadImageForURLRequest:(NSURLRequest *)request
                        completion:(void (^)(UIImage *, NSError *))completion {
    // 1. 先查内存缓存
    // 2. 再查磁盘缓存
    // 3. 最后发起网络请求
}

六、踩坑记录

问题 1:iOS 9+ ATS 限制

问题:iOS 9 默认要求 HTTPS,HTTP 请求被阻止。

原因:App Transport Security (ATS) 默认禁止明文 HTTP。

解决

<!-- Info.plist 添加例外 -->
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>

问题 2:Response Serializer 编码问题

问题:返回中文乱码或解析失败。

原因:服务器返回非 UTF-8 编码,AFJSONResponseSerializer 默认 UTF-8。

解决

AFHTTPResponseSerializer *serializer = [AFHTTPResponseSerializer serializer];
serializer.stringEncoding = NSUTF8StringEncoding;  // 或其他编码
manager.responseSerializer = serializer;

问题 3:Cell 复用图片错乱

问题:UITableView 快速滚动时,图片显示错误。

原因:Cell 复用时未取消之前的下载任务。

解决

- (void)prepareForReuse {
    [super prepareForReuse];
    [self.imageView cancelImageDownloadTask];  // 取消之前的下载
    self.imageView.image = nil;
}

问题 4:后台任务未处理

问题:App 进入后台后,下载任务中断。

原因:NSURLSession 默认不支持后台。

解决

// 使用后台 session configuration
NSURLSessionConfiguration *config = [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:@"com.example.background"];
AFHTTPSessionManager *manager = [[AFHTTPSessionManager alloc] initWithSessionConfiguration:config];

问题 5:Swift 混编桥接头文件缺失

问题:Swift 项目中使用 AFNetworking 报找不到模块。

原因:未配置 Objective-C Bridging Header。

解决

  1. 创建 {ProjectName}-Bridging-Header.h 文件
  2. 添加 #import <AFNetworking/AFNetworking.h>
  3. Build Settings → Swift Compiler - General → Objective-C Bridging Header 设置路径

七、延伸思考

AFNetworking vs Alamofire 横向对比

维度 AFNetworking Alamofire
语言 Objective-C Swift
首发年份 2011 2014
GitHub Stars 33k+ 41k+
最低版本 iOS 9.0 iOS 12.0
包体积 ~500KB ~300KB
维护状态 维护模式 活跃开发
Combine 集成 ❌ 不支持 ✅ DataRequest.publisher
async/await ❌ 不支持 ✅ async(...)
SwiftUI 支持 ❌ 不支持 需自行封装
学习曲线 中等 低(链式 API)
OC 兼容性 原生 需桥接

API 对照迁移表

AFNetworking (OC) Alamofire (Swift)
[manager GET:success:failure:] AF.request(url).response()
[manager POST:parameters:success:failure:] AF.request(url, method: .post, parameters: params)
[manager downloadTaskWithRequest:progress:destination:completion:] AF.download(url).downloadProgress()
[manager uploadTaskWithRequest:from:progress:completion:] AF.upload(multipartFormData:)
AFNetworkReachabilityManager NWPathMonitor (系统 API)
AFSecurityPolicy ServerTrustManager + PinnedCertificatesTrustEvaluator
UIImageView+AFNetworking AlamofireImage / Kingfisher

选型建议

选 AFNetworking:

  • 维护遗留 OC 项目,迁移成本高
  • 项目需要支持 iOS 9 ~ iOS 11
  • 团队 OC 技术栈成熟,Swift 经验少

选 Alamofire:

  • 新项目,Swift 为主要语言
  • 需要 Combine / async-await 集成
  • 想要更活跃的社区和持续更新
  • 最低支持 iOS 12+

渐进式迁移策略

  1. 共存阶段:OC 模块继续用 AFNetworking,新 Swift 模块用 Alamofire
  2. 抽象层:统一封装 NetworkService 协议,底层可替换
  3. 按模块迁移:优先迁移独立的业务模块,而非逐个请求修改
  4. 测试覆盖:迁移前后确保集成测试通过

八、参考资源

官方资源

相关文章

系列 Demo 仓库


本期互动

小作业

查看你现有的 OC 项目中 AFNetworking 的使用方式,尝试将一个简单的 GET 请求改写为 Alamofire 版本,对比代码量和可读性变化。

思考题

AFNetworking 从 1.x 到 4.x 经历了 NSURLConnection → NSURLSession 的底层迁移,如果你是核心开发者,你会如何设计 API 以保持向后兼容?

读者征集

你在从 AFNetworking 迁移到 Alamofire 的过程中遇到过哪些坑?欢迎评论区分享,优质回答会收录进下一期《踩坑记录》。


📅 本系列每周五晚更新 · 已学习:[✓ Alamofire] [✓ Kingfisher] [✓ Lottie] [✓ MarkdownUI] [✓ SDWebImage] [✓ SnapKit] [✓ ListDiff] [✓ RxSwift] [→ AFNetworking] [○ Charts]

iOS 26 适配 | 使用 `hidesSharedBackground` 保持导航栏按钮原有样式

作者 iceiceiceice
2026年3月27日 15:19

iOS 26 适配 | 使用 hidesSharedBackground 保持导航栏按钮原有样式

背景

iOS 26 引入了全新的液态玻璃(Liquid Glass)设计语言,导航栏按钮的默认视觉风格发生了较大变化——多个按钮会被合并在一个统一的玻璃背景块中展示。对于希望在 iOS 26 下保持 iOS 26 之前导航栏按钮样式的开发者来说,苹果提供了 hidesSharedBackground API,用于将共享背景拆分,让每个 item 拥有独立的 Liquid Glass 背景:

if (@available(iOS 26.0, *)) {
    item.hidesSharedBackground = YES;
}

启用后,每个 item 的玻璃背景块会被单独渲染,视觉上更接近旧版导航栏中按钮各自独立的呈现方式。但问题随之而来:系统会在每个玻璃背景块之间插入默认间距,开发者无法通过常规 API 将这个间距收紧为 0,导致多个按钮之间出现明显的视觉割裂感,与 iOS 26 之前的紧凑排列效果存在差异。

因此,仅设置 hidesSharedBackground = YES 还不够,还需要额外处理 PlatterView 的间距问题,才能真正还原旧版导航栏的按钮布局样式。


问题根因分析

在 iOS 26 中,每个 UIBarButtonItem 的 Liquid Glass 背景块由私有容器 _UINavigationBarPlatterView 承载。

UINavigationBar
  └── _UINavigationBarContentView
        ├── _UINavigationBarPlatterView   ← 左侧按钮容器(含独立玻璃背景)
        │     └── _UIButtonBarButton
        └── _UINavigationBarPlatterView   ← 右侧按钮容器(含独立玻璃背景)
              └── _UIButtonBarButton

每个 PlatterView 负责绘制该按钮的 Liquid Glass 背景块,同时也决定了按钮在导航栏中的排列位置。系统在计算这些容器的布局时,会在相邻 PlatterView 之间注入固定的默认间距,且这个间距:

  • 无法通过 UIBarButtonSystemItemFixedSpace 负间距消除(iOS 26 已失效)
  • 无法通过修改 customView 的约束影响
  • 无法通过 UINavigationBar 的公开布局 API 干预

解决方案

核心思路:在布局完成后,运行时递归查找所有 PlatterView 容器,强制重置其 x 坐标与 Leading 约束,将相邻玻璃背景块之间的间距收紧为 0,从而还原 iOS 26 之前导航栏按钮的紧凑排列效果。

完整代码

#pragma mark - iOS 26 PlatterView 间距修复

- (void)fixPlatterViewSpace {
    // 收集所有 PlatterView
    NSMutableArray<UIView *> *platterViews = [NSMutableArray array];
    [self collectPlatterViews:self result:platterViews];
    
    if (platterViews.count == 0) return;
    
    CGFloat navBarWidth = self.frame.size.width;
    CGFloat midX = navBarWidth / 2.0;
    
    // 按中心点分左右
    NSMutableArray *leftViews  = [NSMutableArray array];
    NSMutableArray *rightViews = [NSMutableArray array];
    
    for (UIView *v in platterViews) {
        CGFloat centerX = v.frame.origin.x + v.frame.size.width / 2.0;
        if (centerX < midX) {
            [leftViews addObject:v];
        } else {
            [rightViews addObject:v];
        }
    }
    
    // 左侧:按 x 升序,从 0 开始依次排列
    [leftViews sortUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
        return a.frame.origin.x > b.frame.origin.x
            ? NSOrderedDescending : NSOrderedAscending;
    }];
    CGFloat leftX = 0;
    for (UIView *v in leftViews) {
        [self fixPlatterView:v toX:leftX];
        leftX += v.frame.size.width;
    }
    
    // 右侧:按 x 降序,从右边缘 -5 开始向左排列
    [rightViews sortUsingComparator:^NSComparisonResult(UIView *a, UIView *b) {
        return a.frame.origin.x < b.frame.origin.x
            ? NSOrderedDescending : NSOrderedAscending;
    }];
    CGFloat rightX = navBarWidth - 5;
    for (UIView *v in rightViews) {
        rightX -= v.frame.size.width;
        [self fixPlatterView:v toX:rightX];
    }
}

- (void)collectPlatterViews:(UIView *)view result:(NSMutableArray *)result {
    for (UIView *subview in view.subviews) {
        if ([NSStringFromClass(subview.class) containsString:@"PlatterView"]) {
            [result addObject:subview];
        } else {
            [self collectPlatterViews:subview result:result];
        }
    }
}

- (void)fixPlatterView:(UIView *)platterView toX:(CGFloat)x {
    // 优先修改约束
    for (NSLayoutConstraint *constraint in platterView.superview.constraints) {
        if (constraint.firstItem == platterView &&
            constraint.firstAttribute == NSLayoutAttributeLeading) {
            constraint.constant = x;
        }
    }
    // frame 兜底
    CGRect frame = platterView.frame;
    frame.origin.x = x;
    platterView.frame = frame;
}

调用时机

该方法需要在UINavigationBar布局完成后调用,推荐在 layoutSubviews 末尾触发:

- (void)layoutSubviews {
    [super layoutSubviews];
    
    if (@available(iOS 26.0, *)) {
        [self fixPlatterViewSpace];
    }
}

逻辑拆解

1. 递归收集 PlatterView

[self collectPlatterViews:self result:platterViews];

使用类名字符串匹配 PlatterView,而非直接引用私有类,规避了编译报错。找到 PlatterView 后立即收集,不再递归其子视图,防止嵌套层级的重复收集。

2. 以中线划分左右语义区

CGFloat midX = navBarWidth / 2.0;

导航栏天然地以中线分隔 leftBarButtonItemsrightBarButtonItems 的语义区域,以此作为分组依据,保证左右按钮的 PlatterView 不会被错误归类。

3. 左侧从 x=0 紧密排列

leftX = 0
[BackButton]x = 0
[OtherButton]x = BackButton.width

从导航栏左侧起点开始,将各 PlatterView 依次紧贴排列,彻底消除相邻玻璃背景块之间的系统默认间距,还原旧版左侧按钮的紧凑布局。

4. 右侧从右边缘留 5pt 向左排列

rightX = navBarWidth - 5
[Button2] → rightX -= Button2.width
[Button1] → rightX -= Button1.width

保留 5pt 右侧安全边距,确保最右侧玻璃背景块不会贴边,同时各 PlatterView 之间零间距紧密排布,与旧版右侧按钮排列保持一致。

5. 约束修改 + frame 双保险

// 先改约束(正确路径)
constraint.constant = x;
// 再改 frame(兜底)
platterView.frame = frame;

优先走 Auto Layout 路径修改 Leading 约束保证一致性,frame 赋值作为兜底,确保在纯 frame 布局场景下同样生效。


注意事项

事项 说明
仅限 iOS 26+ @available(iOS 26.0, *) 包裹调用,避免影响低版本行为
调用时机 必须在 layoutSubviews 之后,frame 确定后才能正确分组
Safe Area 左侧从 x=0 起排,刘海屏 / Dynamic Island 下需结合 safeAreaInsets.left 调整起始偏移
私有类名风险 依赖类名包含 PlatterView 的字符串匹配,若苹果后续改名则需同步更新
约束冲突 当前仅修改 Leading 约束;若 PlatterView 同时存在 Trailing / Center 约束,可能引发冲突,需一并处理

小结

iOS 26 的 Liquid Glass 设计语言改变了导航栏按钮的默认视觉风格。对于需要在 iOS 26 下维持旧版导航栏样式的项目,完整的适配路径分为两步:第一步通过 hidesSharedBackground = YES 拆分共享玻璃背景,让每个 item 独立渲染;第二步通过运行时遍历 PlatterView 并强制重置间距,将按钮排列收紧为旧版的紧凑样式。两步缺一不可。

isa 指针、元类、继承链

作者 泉木
2026年3月17日 09:58

一、isa 不只是一个指针

在 64 位设备上,指针只需要 36~40 位就能表示所有内存地址。苹果觉得剩下的位浪费了,于是把 isa 设计成了一个 union(联合体) ,把类指针和一堆标志位都塞进了这 64 位里。

这叫 Tagged Pointer / Non-pointer ISA 技术。


二、isa_t 的完整源码

// 文件:objc-private.h
union isa_t {
    isa_t() { }
    isa_t(uintptr_t value) : bits(value) { }

    uintptr_t bits;         // 原始的 64 位值

private:
    Class cls;              // 类指针(只在 non-pointer isa 关闭时使用)

public:
#if defined(ISA_BITFIELD)
    struct {
        ISA_BITFIELD;       // 展开后是一堆位域定义
    };
    ...
};

ISA_BITFIELD 展开(ARM64,iOS 真机)

// 这是 ARM64 的位域定义
uintptr_t nonpointer        : 1;   // bit 0
uintptr_t has_assoc         : 1;   // bit 1
uintptr_t has_cxx_dtor      : 1;   // bit 2
uintptr_t shiftcls          : 33;  // bit 3~35  ← 类指针在这里!
uintptr_t magic             : 6;   // bit 36~41
uintptr_t weakly_referenced : 1;   // bit 42
uintptr_t unused            : 1;   // bit 43
uintptr_t has_sidetable_rc  : 1;   // bit 44
uintptr_t extra_rc          : 19;  // bit 45~63

三、每一位的含义逐个解释

bit 0:nonpointer

uintptr_t nonpointer : 1;

含义: 这个 isa 是不是 "non-pointer isa"(优化过的 isa)。

  • 0:纯指针,整个 64 位就是类地址(老设备/某些特殊情况)
  • 1:non-pointer isa,64 位里藏了很多信息

现代 iOS 设备全是 1


bit 1:has_assoc

uintptr_t has_assoc : 1;

含义: 这个对象是否有关联对象(Associated Object)。

关联对象就是你用 objc_setAssociatedObject 给对象动态绑定的数据。

为什么需要这一位?

  • 对象 dealloc 时,runtime 需要清理关联对象
  • 用这一位做快速判断:has_assoc == 0 → 跳过关联对象清理,直接释放,更快

bit 2:has_cxx_dtor

uintptr_t has_cxx_dtor : 1;

含义: 这个类(或它的父类)是否有 C++ 析构函数,或者 OC 的 .cxx_destruct 方法。

.cxx_destruct 是编译器自动生成的方法,用来清理带有 __strong 修饰的成员变量(ARC 下自动 release)。

为什么需要这一位?

  • 对象 dealloc 时,如果没有需要清理的 C++ 对象,就跳过 .cxx_destruct 调用
  • 优化释放速度

bit 3~35:shiftcls(33位)

uintptr_t shiftcls : 33;

含义: 这 33 位才是真正的类指针(右移 3 位存储,取的时候左移 3 位还原)。

为什么只用 33 位?因为 ARM64 的内存对齐保证类地址的低 3 位永远是 0,可以省掉。

如何取出类指针?

// runtime 内部的取法
Class getClass() const {
    return (Class)(shiftcls << 3);  // 左移3位还原真实地址
}

bit 36~41:magic(6位)

uintptr_t magic : 6;

含义: 固定的魔数,值是 0b011010(十进制 26)。

用途: 调试用。当你看到一个 isa,如果 magic 值不对,说明这个对象已经被释放或内存被踩了(野指针)。Xcode 和 runtime 的断言会检查这个值。


bit 42:weakly_referenced

uintptr_t weakly_referenced : 1;

含义: 这个对象是否被弱引用__weak 指针)指向过。

为什么需要这一位?

  • 对象 dealloc 时,如果有弱引用指向它,需要去 SideTable(全局散列表)里把那些弱引用都清零(避免 dangling pointer)
  • 用这一位快速判断:weakly_referenced == 0 → 跳过 SideTable 查找,直接释放

bit 43:unused

uintptr_t unused : 1;

含义: 目前未使用,预留位。


bit 44:has_sidetable_rc

uintptr_t has_sidetable_rc : 1;

含义: 引用计数是否溢出到了 SideTable。

正常情况下,引用计数存在 isa 的 extra_rc 里(19位,最大能存 2^19 - 1 = 524287)。如果引用计数超过了这个值,has_sidetable_rc = 1,多出来的部分存在全局的 SideTable 里。


bit 45~63:extra_rc(19位)

uintptr_t extra_rc : 19;

含义: 存储对象的引用计数 - 1

为什么是减 1?因为对象存活时引用计数至少为 1,存 0 代表计数是 1,节省一点空间。

实际的引用计数 = extra_rc + 1(如果 has_sidetable_rc == 0)


四、如何取出 isa 里的类指针(实际代码)

// objc-object.h
inline Class objc_object::getIsa() {
    if (fastpath(!isTaggedPointer())) {
        return ISA();
    }
    // ... TaggedPointer 的特殊处理
}

inline Class objc_object::ISA(bool authenticated) {
    ASSERT(!isTaggedPointer()); 
#if SUPPORT_INDEXED_ISA
    // 某些架构用索引
    ...
#else
    // ARM64 主路径:取 shiftcls 位,左移3位还原地址
    return (Class)(isa.bits & ISA_MASK);
#endif
}

其中 ISA_MASK 在 ARM64 是 0x0000000ffffffff8ULL,作用就是取 bit 3~35。


五、元类(Metaclass)是什么?

这是 OC 最难理解的概念之一,但其实逻辑非常自洽。

问题的由来

在 OC 里,"一切皆对象"——包括类本身也是对象。

[NSString class]  // 这返回的是一个对象
[NSString stringWithString:@"hello"]  // 这是给"类对象"发消息

既然类也是对象,那类对象的 isa 指向哪里?

答案就是:元类(metaclass)

元类的定义

元类是"类的类"。它存储的是类方法+ 方法),就像普通类存储实例方法(- 方法)一样。

对比:类 vs 元类

普通类(Class) 元类(Metaclass)
本质 objc_class 结构体 也是 objc_class 结构体
方法列表里存的 实例方法(- 类方法(+
isa 指向 元类 根元类(NSObject 的元类)
superclass 指向 父类 父类的元类

六、完整的 isa + 继承链图

这是 OC 里最经典的一张图,一定要理解它:

                 isa                  isa               isa
实例对象(inst) --------→ 类(MyClass) --------→ 元类(Meta-MyClass) ──→ 根元类
                                                                          │
              superclass              superclass             superclass    │ isa(自指)
         MyClass ───────→ NSObject     Meta-MyClass ──────→ Meta-NSObject─┘
                               │                                  │
                               │ superclass = nil                 │ superclass
                               ↓                                  ↓
                             (nil)                             NSObject(不是元类!)

用文字描述:

  1. 实例对象.isaMyClass(类)
  2. MyClass.isaMeta-MyClass(元类)
  3. Meta-MyClass.isaMeta-NSObject(根元类)
  4. Meta-NSObject.isaMeta-NSObject自指!根元类的 isa 指向自己

继承链:

  1. MyClass.superclassNSObject
  2. NSObject.superclassnil
  3. Meta-MyClass.superclassMeta-NSObject(元类也有继承链)
  4. Meta-NSObject.superclassNSObject元类继承链的终点是 NSObject 类,不是 nil!

七、为什么元类的继承链终点是 NSObject?

这个设计让你可以在任何类方法里调用 NSObject 的实例方法(比如 respondsToSelector:)。

// 这为什么能工作?
[MyClass respondsToSelector:@selector(doSomething)];

+respondsToSelector: 是 NSObject 的实例方法(- 方法),存在 NSObject 类里。
当你给 MyClass 发这个消息,runtime 查找路径:

Meta-MyClass(没有)
    → Meta-NSObject(没有)
        → NSObject(在这找到了!)

因为 Meta-NSObject.superclass = NSObject,所以元类链最终能访问到 NSObject 的实例方法。优雅!


八、TaggedPointer:特殊的对象

不是所有"对象"都是真正的对象(有 isa 的结构体)。

什么是 TaggedPointer?

对于一些小值对象(比如 NSNumberNSDate、小字符串),苹果直接把值编码进指针本身,不分配堆内存。

NSNumber *num = @42;
// 在 64 位下,这个指针可能长这样:
// 0xb000000000000162  (不是真实的堆地址!)
// 最高位 1 = TaggedPointer 标志
// 低位存了 42 这个值

判断是否是 TaggedPointer

static inline bool _objc_isTaggedPointer(const void * _Nullable ptr) {
    return ((uintptr_t)ptr & _OBJC_TAG_MASK) == _OBJC_TAG_MASK;
}
// ARM64: _OBJC_TAG_MASK = (1UL<<63),最高位为1就是 TaggedPointer

TaggedPointer 的好处

  • 不需要堆分配:直接在指针里存值,alloc 时不走 malloc
  • 不需要引用计数:也不需要 release,直接丢弃
  • 更快:少了内存分配和释放的开销

九、SideTable:引用计数和弱引用的大本营

当 isa 的 extra_rc 不够用,或者有弱引用时,数据存在 SideTable 里。

struct SideTable {
    spinlock_t slock;           // 自旋锁,保证线程安全
    RefcountMap refcnts;        // 引用计数表(散列表)
    weak_table_t weak_table;    // 弱引用表
};

全局有 8 个(或 64 个)SideTable,通过对象地址取模来分配,减少锁竞争。

weak_table_t 弱引用表

struct weak_table_t {
    weak_entry_t *weak_entries;  // 弱引用条目数组
    size_t num_entries;
    ...
};

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;  // 被指向的对象
    // 指向该对象的所有 __weak 指针地址的集合
    union {
        struct { weak_referrer_t *referrers; ... };
        struct { weak_referrer_t inline_referrers[WEAK_INLINE_COUNT]; };
    };
};

__weak 置零的过程

对象 dealloc
    ↓
检查 isa.weakly_referenced
    ↓(== 1)
去 SideTable 找 weak_entry_t
    ↓
遍历所有指向该对象的 __weak 指针
    ↓
全部置 nil
    ↓
从 weak_table 删除该条目

这就是为什么 __weak 指针在对象释放后自动变成 nil,而不会变成野指针——runtime 帮你清零了。


十、小结

概念 本质 存在哪里
isa 64位 union,含类指针+引用计数+标志位 每个对象的第一个字段
元类 存类方法的 objc_class 全局静态区
TaggedPointer 值直接编码进指针,无堆对象 栈/寄存器
extra_rc 引用计数(-1)的快速存储 isa 的高19位
SideTable 溢出引用计数 + 弱引用表 全局散列表

下一篇:延伸问题 Q&A——消息发送、方法查找、Swizzle、dealloc 全流程等

objc_class 结构体逐行解析

作者 泉木
2026年3月16日 20:05

前言

objc_class 开始,是因为它是整个 Runtime 的基础数据结构。Runtime 管的事很多——消息发送、方法查找、内存管理、Category 加载……但这些行为最终都要落在"类长什么样"上面。搞清楚 objc_class,后面的东西才能接得上。

一、源码全貌(先看完整结构)

下面是从 Apple 开源的 objc4 里提取的核心结构体,我做了适度精简,保留所有关键字段。

建议先整体扫一遍,有个印象,后面逐个解释。

// ============================================================
// 文件:objc-runtime-new.h(objc4-818.2)
// 源码地址:https://opensource.apple.com/source/objc4/
// ============================================================

// -------------------- 1. objc_object --------------------
// 所有 OC 对象的基类,只有一个字段:isa
struct objc_object {
private:
    isa_t isa;  // 64位,包含类指针+引用计数+标志位

public:
    Class ISA(bool authenticated = false);
    Class getIsa();
    // ... 省略其他方法
};


// -------------------- 2. objc_class --------------------
// 这就是"类"的底层结构,继承自 objc_object
struct objc_class : objc_object {
    // 注意:isa 字段继承自 objc_object,这里不重复写
    
    Class superclass;           // 父类指针
    cache_t cache;              // 方法缓存(哈希表)
    class_data_bits_t bits;     // 指向 class_rw_t 的指针+标志位

    // 取出真正的数据
    class_rw_t *data() const {
        return bits.data();
    }
    // ... 省略其他方法
};


// -------------------- 3. class_data_bits_t --------------------
// 这是 objc_class.bits 的类型,用来存储指向 class_rw_t 的指针 + 几个标志位
struct class_data_bits_t {
private:
    uintptr_t bits;   // 就是一个 64 位整数,低位藏标志位,高位存指针

public:
    // 用掩码取出真正的 class_rw_t 指针
    class_rw_t *data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }

    // 各种标志位的读取方法
    bool isSwiftLegacy() const {
        return getBit(FAST_IS_SWIFT_LEGACY);
    }
    bool isSwiftStable() const {
        return getBit(FAST_IS_SWIFT_STABLE);
    }
    // ... 其他方法
};

// ARM64 下的掩码和标志位定义:
// FAST_DATA_MASK      = 0x00007ffffffffff8UL  (取 bit 3~46,即真正的指针)
// FAST_IS_SWIFT_LEGACY = 1 << 0  (bit 0: 是否是旧版 Swift 类)
// FAST_IS_SWIFT_STABLE = 1 << 1  (bit 1: 是否是新版 Swift 类)
// FAST_HAS_DEFAULT_RR  = 1 << 2  (bit 2: 是否有默认的 retain/release)


// -------------------- 4. cache_t --------------------
// 方法缓存,加速方法查找
struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;  // 桶数组地址
    union {
        struct {
            explicit_atomic<mask_t>    _maybeMask;   // 桶数量-1(用于哈希取模)
            uint16_t                   _flags;
            uint16_t                   _occupied;    // 已使用的桶数
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
public:
    // ... 省略查找、插入方法
};

// 单个缓存桶
struct bucket_t {
private:
    explicit_atomic<SEL> _sel;       // 方法名(选择子)
    explicit_atomic<uintptr_t> _imp; // 函数指针(方法实现地址)
};


// -------------------- 5. class_rw_t --------------------
// 运行时可读写数据(Category 方法会合并到这里)
struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
    uint16_t index;

    explicit_atomic<uintptr_t> ro_or_rw_ext;  // 指向 class_ro_t 或扩展数据

    Class firstSubclass;       // 第一个子类
    Class nextSiblingClass;    // 兄弟类(形成链表)

    // 获取方法/属性/协议列表
    const method_array_t methods() const;
    const property_array_t properties() const;
    const protocol_array_t protocols() const;

    // 获取只读数据
    const class_ro_t *ro() const;
};


// -------------------- 6. class_ro_t --------------------
// 编译期只读数据(源码里写死的方法、变量、属性)
struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;      // 实例变量起始偏移
    uint32_t instanceSize;       // sizeof(实例),对象占多少字节

    const uint8_t * ivarLayout;  // 强引用 ivar 的内存布局

    const char * name;           // 类名字符串,如 "NSString"
    
    WrappedPtr<method_list_t, method_list_t::Ptrauth> baseMethods;  // 方法列表
    protocol_list_t * baseProtocols;     // 协议列表
    const ivar_list_t * ivars;           // 实例变量列表
    
    const uint8_t * weakIvarLayout;      // 弱引用 ivar 的内存布局
    property_list_t *baseProperties;     // 属性列表
};


// -------------------- 7. method_t --------------------
// 单个方法的描述
struct method_t {
    SEL name;              // 方法名(选择子),本质是 const char *
    const char *types;     // 类型编码,如 "v16@0:8"
    IMP imp;               // 函数指针(真正的代码地址)
};


// -------------------- 8. ivar_t --------------------
// 单个实例变量的描述
struct ivar_t {
    int32_t *offset;       // 偏移量指针(Non-Fragile ABI 用)
    const char *name;      // 变量名,如 "_name"
    const char *type;      // 类型编码,如 "@"NSString""
    uint32_t alignment_raw;// 对齐方式
    uint32_t size;         // 占多少字节
};


// -------------------- 9. isa_t --------------------
// isa 的真正定义(union,64位里塞了很多信息)
union isa_t {
    uintptr_t bits;        // 原始64位值

    // ARM64 位域展开(iOS 真机):
    struct {
        uintptr_t nonpointer        : 1;   // bit 0:  是否是优化过的 isa
        uintptr_t has_assoc         : 1;   // bit 1:  有关联对象?
        uintptr_t has_cxx_dtor      : 1;   // bit 2:  有 C++ 析构?
        uintptr_t shiftcls          : 33;  // bit 3-35:  类指针(右移3位存储)
        uintptr_t magic             : 6;   // bit 36-41: 固定值 0x1a,调试用
        uintptr_t weakly_referenced : 1;   // bit 42: 被弱引用?
        uintptr_t unused            : 1;   // bit 43: 未使用
        uintptr_t has_sidetable_rc  : 1;   // bit 44: 引用计数溢出到 SideTable?
        uintptr_t extra_rc          : 19;  // bit 45-63: 引用计数-1
    };
};

二、结构关系图

objc_class(一个类在内存里的样子)
┌─────────────────────────────────────┐
│  isa (继承自 objc_object)           │ ← isa_t union,64位
├─────────────────────────────────────┤
│  superclass                         │ ← 指向父类的 objc_class
├─────────────────────────────────────┤
│  cache                              │ ← cache_t 结构体
│    └── bucket_t[] 数组              │     每个桶存 { SEL, IMP }
├─────────────────────────────────────┤
│  bits                               │ ← class_data_bits_t(指针+标志位)
│    └── data() ───────────────────────────→ class_rw_t(运行时可写)
│                                     │        ├── methods()
│                                     │        ├── properties()
│                                     │        ├── protocols()
│                                     │        └── ro() ────────→ class_ro_t(只读)
│                                     │                             ├── name
│                                     │                             ├── baseMethods
│                                     │                             │     └── method_t[]
│                                     │                             ├── ivars
│                                     │                             │     └── ivar_t[]
│                                     │                             └── baseProperties
└─────────────────────────────────────┘

三、逐结构体解析

接下来按源码出现的顺序,逐个讲解每个结构体、每个字段的含义。


3.1 objc_object —— 所有对象的祖宗

struct objc_object {
private:
    isa_t isa;
};

这是什么?

这是 OC 里所有对象的底层表示。不管是 NSStringUIView、还是你自定义的 MyClass 实例,底层都是 objc_object

字段解析

字段 类型 含义
isa isa_t "is a" 的缩写,标识"这个对象是什么类型"。是一个 64 位的 union,里面藏了类指针 + 引用计数 + 各种标志位。isa_t 的详细结构会在第二篇展开讲解。

为什么只有一个字段?

因为 objc_object最小公共祖先。每个对象只需要知道"我是什么类型"(isa),其他的成员变量由具体的类定义,紧跟在 isa 后面存储。

内存布局示意

一个 MyClass 实例的内存:
┌────────────────┐ ← 对象起始地址
│     isa        │   8 字节(objc_object 的字段)
├────────────────┤
│    _name       │   8 字节(MyClass 自己的 ivar)
├────────────────┤
│    _age        │   4 字节(MyClass 自己的 ivar)
└────────────────┘

3.2 objc_class —— 类的完整定义

struct objc_class : objc_object {
    Class superclass;
    cache_t cache;
    class_data_bits_t bits;

    class_rw_t *data() const {
        return bits.data();
    }
};

这是什么?

这是 OC 里的底层表示。每个 @interface MyClass 在运行时都对应一个 objc_class 结构体实例。

注意它继承自 objc_object,所以"类也是对象"——类对象有自己的 isa(指向元类)。

字段逐个解析

字段 类型 含义
isa isa_t(继承来的) 类对象的 isa 指向它的元类(metaclass)。isa_t 的详细结构见第二篇。
superclass Class 父类指针。Classobjc_class * 的 typedef,即指向另一个 objc_class 的指针。NSObject 的 superclass 是 nil。;
cache cache_t 方法缓存,哈希表结构。最近调用的方法会缓存在这里,加速后续调用。
bits class_data_bits_t 一个 64 位整数,低 3 位是标志位,高位是 class_rw_t 指针

Class 是什么类型?

// objc.h
typedef struct objc_class *Class;

Class 就是 objc_class * 的别名,一个指向类对象的指针。你代码里写的所有 Class 都只是这个指针,没有额外结构:

Class cls = [MyClass class];       // 拿到 MyClass 的 objc_class * 指针
Class superCls = [cls superclass]; // 拿到父类的 objc_class * 指针

同理,id 也是:

typedef struct objc_object *id;    // id = objc_object *,指向任意实例对象

superclass 有什么用?

实现继承。当在当前类找不到方法时,runtime 会沿着 superclass 链往上找。

调用 [myObj doSomething]
    ↓
在 MyClass 的方法列表里找
    ↓ 找不到
通过 superclass 到 NSObject 里找
    ↓ 还找不到
触发消息转发

3.3 class_data_bits_t —— 指针 + 标志位的混合体

struct class_data_bits_t {
private:
    uintptr_t bits;   // 64 位整数

public:
    class_rw_t *data() const {
        return (class_rw_t *)(bits & FAST_DATA_MASK);
    }
};

这是什么?

就是 objc_class.bits 的类型。它不是简单的指针,而是把 class_rw_t 指针几个标志位 打包进同一个 64 位整数里。

为什么能这样做?

因为 class_rw_t 在内存里是 8 字节对齐的,所以它的地址的低 3 位永远是 000。苹果就把这 3 位拿来存标志位,不浪费。

64 位的布局

class_data_bits_t.bits(64位)

 63                                3  2  1  0
┌────────────────────────────────┬──┬──┬──┐
│     class_rw_t 指针 (bit 3~63) │ 210│
└────────────────────────────────┴──┴──┴──┘
                                   │  │  │
                                   │  │  └─ FAST_IS_SWIFT_LEGACY (是旧版Swift类?)
                                   │  └──── FAST_IS_SWIFT_STABLE (是新版Swift类?)
                                   └─────── FAST_HAS_DEFAULT_RR  (有默认retain/release?)

取指针的掩码

// ARM64
#define FAST_DATA_MASK 0x00007ffffffffff8UL

// 二进制:...11111111111111111111111111111111111111000
// 作用:与运算后,低 3 位清零,剩下的就是真正的 class_rw_t 地址

data() 方法做了什么?

class_rw_t *data() const {
    return (class_rw_t *)(bits & FAST_DATA_MASK);
    // bits & 掩码 → 把低 3 位标志位清掉 → 得到纯净的 class_rw_t 指针
}

一句话总结

class_data_bits_tisa_t 的设计思路一样——充分利用内存对齐带来的空闲位,一个 64 位整数里塞多种信息,省内存


3.4 cache_t —— 方法缓存

struct cache_t {
private:
    explicit_atomic<uintptr_t> _bucketsAndMaybeMask;
    union {
        struct {
            explicit_atomic<mask_t> _maybeMask;
            uint16_t _flags;
            uint16_t _occupied;
        };
        explicit_atomic<preopt_cache_t *> _originalPreoptCache;
    };
};

struct bucket_t {
private:
    explicit_atomic<SEL> _sel;
    explicit_atomic<uintptr_t> _imp;
};

为什么需要缓存?

每次调用方法都去 class_rw_t 的方法列表里遍历查找,太慢了。cache_t 是一个哈希表,把最近调用过的方法缓存起来。

字段解析

字段 含义
_bucketsAndMaybeMask 哈希桶数组的起始地址
_maybeMask 桶数量 - 1,用于哈希取模(hash & mask
_occupied 当前已使用的桶数量

bucket_t 是什么?

单个缓存条目,存储 SEL(方法名)和 IMP(函数指针)的映射。

字段 类型 含义
_sel SEL 方法选择子(方法名),如 @selector(viewDidLoad)
_imp uintptr_t 方法实现的函数地址

查找流程

[obj doSomething]
    ↓
计算 @selector(doSomething) 的哈希值
    ↓
hash & _maybeMask → 得到桶的索引
    ↓
取出 bucket_t,比较 _sel 是否等于 @selector(doSomething)
    ↓
相等 → 直接调用 _imp,结束(命中缓存,极快)
不相等 → 去 class_rw_t 里慢速查找

缓存什么时候会失效?

  • 调用 method_exchangeImplementations(Method Swizzle)后
  • 动态添加方法后
  • 类第一次加载时

失效时 runtime 会调用 flushCaches() 清空缓存。


3.5 class_rw_t —— 运行时可读写数据

struct class_rw_t {
    uint32_t flags;
    uint16_t witness;
    uint16_t index;

    explicit_atomic<uintptr_t> ro_or_rw_ext;

    Class firstSubclass;
    Class nextSiblingClass;

    const method_array_t methods() const;
    const property_array_t properties() const;
    const protocol_array_t protocols() const;
    const class_ro_t *ro() const;
};

这是什么?

rw = read-write(可读写)。这里存放运行时可以修改的数据,比如 Category 添加的方法会合并到这里。

字段解析

字段 含义
flags 各种标志位(是否已初始化、是否有 C++ 构造函数等)
ro_or_rw_ext 指向 class_ro_t(只读数据),或扩展数据
firstSubclass 指向第一个子类,形成子类链表
nextSiblingClass 指向下一个兄弟类(同一个父类的其他子类)

获取方法/属性/协议

const method_array_t methods() const;     // 返回方法列表(含 Category 方法)
const property_array_t properties() const; // 返回属性列表
const protocol_array_t protocols() const;  // 返回协议列表

这些方法返回的是合并后的列表——源码里写的 + Category 加进来的。

ro() 方法

返回 class_ro_t 指针,取出编译期确定的只读数据。


3.6 class_ro_t —— 编译期只读数据

struct class_ro_t {
    uint32_t flags;
    uint32_t instanceStart;
    uint32_t instanceSize;

    const uint8_t * ivarLayout;
    const char * name;
    
    WrappedPtr<method_list_t, ...> baseMethods;
    protocol_list_t * baseProtocols;
    const ivar_list_t * ivars;
    
    const uint8_t * weakIvarLayout;
    property_list_t *baseProperties;
};

这是什么?

ro = read-only(只读)。这里存放编译时就确定的数据,运行时不能修改。

字段逐个解析

字段 类型 含义
flags uint32_t 标志位
instanceStart uint32_t 实例变量在对象内存中的起始偏移(通常是 8,跳过 isa)
instanceSize uint32_t 一个实例对象占多少字节(sizeof
ivarLayout const uint8_t * 描述哪些 ivar 是强引用(ARC 用)
name const char * 类名字符串,如 "UIViewController"
baseMethods method_list_t * 源码里定义的方法列表(不含 Category)
baseProtocols protocol_list_t * 源码里遵循的协议列表
ivars ivar_list_t * 实例变量列表
weakIvarLayout const uint8_t * 描述哪些 ivar 是弱引用
baseProperties property_list_t * 源码里定义的属性列表

class_ro_t vs class_rw_t 对比

class_ro_t class_rw_t
全称 read-only read-write
什么时候确定 编译期(写进 Mach-O 二进制文件) 运行时(启动时构造)
能修改吗 ❌ 不能 ✅ 能
存什么 源码里写死的方法、属性、变量 动态添加的方法、Category 合并的方法

为什么要分两层?

因为 Category 是运行时加载的。编译期不知道会有哪些 Category,所以:

  1. 编译期:把源码里写的方法存进 class_ro_t
  2. 运行时:遍历所有 Category,把它们的方法合并class_rw_t

查找方法时,先查 class_rw_t(含 Category),它内部会访问 class_ro_t


3.7 method_t —— 单个方法

struct method_t {
    SEL name;
    const char *types;
    IMP imp;
};

字段解析

字段 类型 含义 例子
name SEL 方法选择子(方法名) @selector(viewDidLoad)
types const char * 类型编码(返回值+参数的类型) "v16@0:8"
imp IMP 函数指针,指向方法的真正实现 0x100001234(代码段地址)

SEL 是什么?

typedef struct objc_selector *SEL;

本质是一个唯一化的 C 字符串。同名方法在整个程序里 SEL 值相同(指针相等),所以比较方法名只需要比较指针,极快。

SEL sel1 = @selector(doSomething);
SEL sel2 = @selector(doSomething);
// sel1 == sel2(指针相等,不是字符串比较)

IMP 是什么?

typedef void (*IMP)(id, SEL, ...);

函数指针,前两个参数固定是:

  • id self:消息接收者
  • SEL _cmd:方法选择子

这解释了为什么 OC 方法里能直接用 self_cmd——它们是函数的隐藏参数。

// 你写的:
- (void)doSomething {
    NSLog(@"%@", self);
}

// 编译器眼里的:
void doSomething(id self, SEL _cmd) {
    NSLog(@"%@", self);
}

types 字符串怎么读?

- (NSString *)nameWithPrefix:(NSString *)prefix 为例,types 是 @24@0:8@16

@    → 返回值是 id(对象)
24   → 所有参数总共占 24 字节
@    → 第1个参数是 id(self)
0    → 从第 0 字节开始
:    → 第2个参数是 SEL(_cmd)
8    → 从第 8 字节开始
@    → 第3个参数是 id(prefix)
16   → 从第 16 字节开始

这套编码叫 Type Encoding,runtime 靠它做方法签名校验。


3.8 ivar_t —— 单个实例变量

struct ivar_t {
    int32_t *offset;
    const char *name;
    const char *type;
    uint32_t alignment_raw;
    uint32_t size;
};

字段解析

字段 类型 含义 例子
offset int32_t * 偏移量的指针(不是值!) 指向存储偏移量的内存
name const char * 变量名 "_name"
type const char * 类型编码 "@"NSString""
alignment_raw uint32_t 内存对齐方式 通常是 3(2^3 = 8 字节对齐)
size uint32_t 占多少字节 指针占 8 字节

为什么 offset 是指针而不是值?

这是 Non-Fragile ABI(非脆弱 ABI)的设计。

假设父类 NSObject 有 8 字节的 isa,子类 MyClass_name 变量在 offset 8。

如果 Apple 在新系统里给 NSObject 加了一个成员变量(变成 16 字节),按老 ABI,MyClass_name 还在 offset 8,就会和 NSObject 新增的变量重叠——程序崩溃。

Non-Fragile ABI 的解决方案:

  1. offset 是指针,不是值
  2. App 启动时,runtime 检查父类大小是否变化
  3. 如果变化了,自动调整所有子类 ivar 的 offset 值
  4. 子类不需要重新编译
旧系统:NSObject 8字节,MyClass._name 在 offset 8
    ↓ Apple 升级系统
新系统:NSObject 16字节
    ↓ runtime 自动修正
MyClass._name 的 offset 从 8 改成 16

访问 ivar 的过程

// 伪代码
id value = *(id *)((char *)obj + *ivar->offset);
// 1. 取出 offset 指针指向的偏移值
// 2. 对象地址 + 偏移值 = ivar 的内存地址
// 3. 解引用得到 ivar 的值

3.9 isa_t —— 64 位里藏了很多东西

union isa_t {
    uintptr_t bits;

    struct {
        uintptr_t nonpointer        : 1;
        uintptr_t has_assoc         : 1;
        uintptr_t has_cxx_dtor      : 1;
        uintptr_t shiftcls          : 33;
        uintptr_t magic             : 6;
        uintptr_t weakly_referenced : 1;
        uintptr_t unused            : 1;
        uintptr_t has_sidetable_rc  : 1;
        uintptr_t extra_rc          : 19;
    };
};

为什么不直接存指针?

在 64 位系统上,指针只需要约 40 位就能表示所有内存地址。剩下的位"浪费"了,苹果就把引用计数和各种标志位塞进去,省内存。

这叫 Non-pointer ISA(优化过的 isa)。

每一位的含义

位域 位数 含义
nonpointer 1 是否是优化过的 isa(现代设备都是 1)
has_assoc 1 对象是否有关联对象(objc_setAssociatedObject
has_cxx_dtor 1 是否有 C++ 析构函数或 ARC 的 .cxx_destruct
shiftcls 33 类指针(右移 3 位存储,取时左移还原)
magic 6 固定值 0x1a,调试用(值不对说明内存被踩了)
weakly_referenced 1 是否被 __weak 指针指向过
unused 1 未使用,预留
has_sidetable_rc 1 引用计数是否溢出到 SideTable
extra_rc 19 存储引用计数 - 1(最大 2^19 - 1 = 524287)

如何取出类指针?

Class cls = (Class)(isa.bits & ISA_MASK);
// ISA_MASK = 0x0000000ffffffff8ULL
// 掩码取出 bit 3~35,然后隐含左移还原

四、完整内存布局示意

把所有结构体串起来,一个类在内存里长这样:

objc_class 实例(代表 MyClass 这个类)
┌─────────────────────────────────────────────────────┐
│  isa (64位 isa_t)                                   │ → 指向 Meta-MyClass(元类)
├─────────────────────────────────────────────────────┤
│  superclass (8字节)                                 │ → 指向 NSObject
├─────────────────────────────────────────────────────┤
│  cache (cache_t)                                    │
│    _bucketsAndMaybeMask → [ bucket_t, bucket_t... ] │ 每个桶: { SEL, IMP }
│    _maybeMask = N-1                                 │
│    _occupied = 已用桶数                              │
├─────────────────────────────────────────────────────┤
│  bits (class_data_bits_t)                           │ ← 低3位是标志位,高位是指针
│    data() ──────────────────────────────────────────│──→ class_rw_t
│                                                     │      ├── methods()   → [method_t, ...]
│                                                     │      ├── properties()→ [property_t, ...]
│                                                     │      ├── protocols() → [protocol_t, ...]
│                                                     │      └── ro() ───────→ class_ro_t
│                                                     │              ├── name = "MyClass"
│                                                     │              ├── instanceSize = 24
│                                                     │              ├── baseMethods
│                                                     │              │     ├── method_t { SEL, types, IMP }
│                                                     │              │     └── method_t { ... }
│                                                     │              └── ivars
│                                                     │                    ├── ivar_t { offset*, "_name", "@", 3, 8 }
│                                                     │                    └── ivar_t { offset*, "_age",  "i", 2, 4 }
└─────────────────────────────────────────────────────┘

五、小结

结构体 可否运行时修改 存放什么
objc_class 不直接改 类的容器,持有 superclass/cache/bits
isa_t 部分可改(引用计数位) 类指针 + 引用计数 + 标志位,全塞在 64 位里
class_data_bits_t 不直接改 class_rw_t 指针 + 3 个标志位,又一个"指针+标志"混合体
cache_t 是(每次调用方法后更新) 最近调用的方法 SEL → IMP 映射
class_rw_t 运行时合并后的方法、属性、协议
class_ro_t 编译期确定的方法、变量、属性,写死在二进制里
method_t IMP 可以换(Swizzle) 一个方法的名字、类型编码、实现地址
ivar_t offset 可改(Non-Fragile ABI) 一个实例变量的名字、类型、偏移量

下一篇:isa 指针深度解析、元类体系、完整继承链图

❌
❌