iOS 开发中的 Runtime

由浅入深,从基本概念到源码解析,带你全面理解 Objective-C 运行时机制


一、什么是 Runtime?

1.1 从「面向对象」说起

在 C++ 这类静态语言中,方法的调用在编译期就确定了,编译器会把方法调用翻译成确定的函数地址。而 Objective-C 是一门「消息型」语言,方法调用在编译期只是生成了「发消息」的代码,真正要调用哪个方法,要到运行期才能确定。

这种「推迟到运行时再决定」的能力,就是 Runtime 的精髓。

1.2 Runtime 是什么?

Runtime 是 Objective-C 的运行时系统,是一套用 C 和汇编实现的底层库。它负责:

  • 类与对象的创建、布局
  • 方法的查找、派发(Message Dispatch)
  • 消息传递机制(Message Passing)
  • 动态添加/修改类、方法、属性
  • 方法交换(Method Swizzling)
  • KVO、KVC、Block 等特性的底层支撑

可以理解为:Runtime 是 Objective-C 的「操作系统」,没有它,OC 就无法运行。

1.3 两种版本

版本 说明 适用场景
Legacy Runtime 较早版本 32 位 macOS、老设备
Modern Runtime 当前主流 64 位 macOS、iOS、模拟器

Modern Runtime 支持:非 fragile instance variables、属性自动合成、Objective-C 2.0 语法等。


二、核心概念

2.1 对象(Object)与类(Class)

在 C 中,结构体就是「数据 + 布局」。在 OC 中,对象 本质上是一个指向结构体的指针,结构体第一个成员是指向 类对象(Class) 的指针 isa

1
2
3
4
5
6
7
8
9
10
11
12
13
// 简化理解
struct objc_object {
Class isa; // 指向所属类
};

struct objc_class {
Class isa; // 类对象的 isa 指向元类(metaclass)
Class superclass; // 父类
// ... 方法列表、属性列表等
};

typedef struct objc_object *id;
typedef struct objc_class *Class;
  • 实例对象isa → 类对象
  • 类对象isa → 元类(metaclass)
  • 元类isa → 根元类,superclass → 父类的元类

2.2 消息传递(Message Passing)

OC 中「调用方法」实际上是「发消息」:

1
2
3
[person sayHello];
// 等价于
objc_msgSend(person, @selector(sayHello));

objc_msgSend 是 Runtime 提供的 C 函数,流程大致为:

  1. 检查 receiver 是否为 nil(若为 nil,直接返回,不崩溃)
  2. receiverisa 指向的类中查找方法
  3. 若未找到,沿 superclass 链向上查找
  4. 找到后跳转执行实现(IMP)
  5. 若最终未找到,进入「消息转发」流程

2.3 SEL、IMP、Method

类型 说明 示例
SEL 方法选择器,方法名的唯一标识 @selector(sayHello)
IMP 函数指针,方法的实际实现 void (*)(id, SEL, ...)
Method 方法结构体,包含 SEL 和 IMP struct objc_method
1
2
3
4
5
6
7
typedef struct objc_method *Method;

struct objc_method {
SEL method_name;
char *method_types;
IMP method_imp;
};

三、消息查找与转发

3.1 方法查找流程(快速查找 + 慢速查找)

1
2
3
4
5
6
7
8
9
10
objc_msgSend(receiver, sel)

├─ 1. 检查 receiver 是否为 nil

├─ 2. 从类/父类缓存中查找 (objc_cache) —— 快速路径

└─ 3. 缓存未命中 → 调用 lookUpImpOrForward 慢速查找
├─ 在当前类 method_list 中查找
├─ 沿 superclass 链向上查找
└─ 若仍未找到 → 进入动态方法解析

3.2 动态方法解析(Dynamic Method Resolution)

在找不到方法时,Runtime 会先给类一次「补救」机会:

1
2
3
4
5
6
7
8
9
10
11
+ (BOOL)resolveInstanceMethod:(SEL)sel {
if (sel == @selector(dynamicMethod)) {
class_addMethod([self class], sel, (IMP)dynamicMethodIMP, "v@:");
return YES;
}
return [super resolveInstanceMethod:sel];
}

void dynamicMethodIMP(id self, SEL _cmd) {
NSLog(@"动态添加的方法被调用了");
}

3.3 消息转发(Message Forwarding)

若动态解析也返回 NO,则进入转发流程:

  1. Fast Forwarding- (id)forwardingTargetForSelector:(SEL)aSelector

    • 返回一个能响应该 Selector 的对象,消息将转给该对象
    • 不修改方法签名,性能较好
  2. Normal Forwarding- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector + - (void)forwardInvocation:(NSInvocation *)anInvocation

    • 返回方法签名,再在 forwardInvocation: 中处理
    • 可灵活重定向、修改参数、多对象分发等
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Fast Forwarding
- (id)forwardingTargetForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return self.backupObject; // 转给其他对象处理
}
return [super forwardingTargetForSelector:aSelector];
}

// Normal Forwarding
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
if (aSelector == @selector(sayHello)) {
return [NSMethodSignature signatureWithObjCTypes:"v@:"];
}
return [super methodSignatureForSelector:aSelector];
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
if (anInvocation.selector == @selector(sayHello)) {
[anInvocation invokeWithTarget:self.backupObject];
} else {
[super forwardInvocation:anInvocation];
}
}

3.4 流程小结

1
2
3
4
5
6
7
8
9
10
11
12
13
查找 IMP

├─ 1. 缓存命中 → 直接调用

├─ 2. 当前类及父类 method_list 中找到 → 写入缓存并调用

├─ 3. 未找到 → resolveInstanceMethod / resolveClassMethod

├─ 4. 解析失败 → forwardingTargetForSelector

├─ 5. 返回 nil → methodSignatureForSelector + forwardInvocation

└─ 6. 仍无法处理 → doesNotRecognizeSelector: crash

四、Runtime 源码结构

4.1 主要头文件

1
2
3
4
5
6
7
8
objc4 源码(可在 opensource.apple.com 获取):
├── objc-runtime.mm # 运行时初始化、类加载
├── objc-msg-x86_64.s # objc_msgSend 汇编实现(x86_64)
├── objc-msg-arm64.s # objc_msgSend 汇编实现(ARM64)
├── objc-class.mm # 类结构、方法列表
├── objc-object.mm # 对象、isa、关联对象
├── message.mm # 消息查找、转发
└── runtime.mm # 动态创建类、方法等

4.2 objc_object 与 isa

1
2
3
4
5
6
7
8
9
// objc-private.h (简化)
struct objc_object {
private:
isa_t isa; // 非指针时存储类地址;Tagged Pointer 优化时存更多信息
public:
Class getIsa();
void initIsa(Class cls);
// ...
};

4.3 objc_class 结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// objc-runtime-new.h (简化)
struct objc_class : objc_object {
Class superclass;
cache_t cache; // 方法缓存
class_data_bits_t bits; // 方法列表、属性、协议等

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

const method_array_t methods() const;
const property_array_t properties() const;
const protocol_array_t protocols() const;
};

4.4 方法缓存(cache_t)

为提高查找效率,每个类都有方法缓存。查找顺序:先查缓存,再查方法列表

1
2
3
4
5
6
7
8
9
10
11
// 简化理解
struct cache_t {
bucket_t *buckets; // 哈希表
mask_t mask; // 容量 - 1
mask_t occupied; // 已缓存数量
};

struct bucket_t {
SEL sel;
IMP imp;
};

五、常用 Runtime API

5.1 类与对象

1
2
3
4
5
6
7
8
9
10
11
// 获取类
Class cls = [MyObject class];
Class cls2 = object_getClass(obj);

// 创建实例
id obj = class_createInstance(cls, 0);

// 判断类型
BOOL isMeta = class_isMetaClass(cls);
BOOL isMember = [obj isMemberOfClass:[MyObject class]];
BOOL isKind = [obj isKindOfClass:[NSObject class]];

5.2 方法操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 获取方法
Method m = class_getInstanceMethod(cls, @selector(sayHello));

// 获取 SEL 和 IMP
SEL sel = method_getName(m);
IMP imp = method_getImplementation(m);

// 交换实现
Method m1 = class_getInstanceMethod(cls, @selector(methodA));
Method m2 = class_getInstanceMethod(cls, @selector(methodB));
method_exchangeImplementations(m1, m2);

// 添加方法
class_addMethod(cls, @selector(newMethod), (IMP)newMethodIMP, "v@:");

5.3 属性与成员变量

1
2
3
4
5
6
7
8
9
10
11
// 获取属性列表
unsigned int count;
objc_property_t *properties = class_copyPropertyList(cls, &count);

// 获取成员变量
Ivar *ivars = class_copyIvarList(cls, &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
const char *type = ivar_getTypeEncoding(ivar);
}

5.4 动态创建类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 创建新类
Class newClass = objc_allocateClassPair([NSObject class], "MyDynamicClass", 0);

// 添加实例变量(需在 allocate 之后、register 之前)
class_addIvar(newClass, "title", sizeof(NSString *), log2(sizeof(NSString *)), @encode(NSString *));

// 添加方法
class_addMethod(newClass, @selector(doSomething), (IMP)doSomethingIMP, "v@:");

// 注册类
objc_registerClassPair(newClass);

// 使用
id instance = [[newClass alloc] init];

六、Method Swizzling(方法交换)

6.1 基本原理

通过 method_exchangeImplementations 交换两个方法的 IMP,使调用 A 时实际执行 B 的实现。常用于:

  • 无侵入式 Hook 系统或第三方方法
  • 统计埋点、日志
  • 修复 Bug、兼容旧版本

6.2 基础写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 交换 viewDidLoad 实现
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIViewController class];
SEL originalSel = @selector(viewDidLoad);
SEL swizzledSel = @selector(swizzled_viewDidLoad);

Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);

method_exchangeImplementations(originalMethod, swizzledMethod);
});
}

- (void)swizzled_viewDidLoad {
NSLog(@"ViewController viewDidLoad 被调用");
[self swizzled_viewDidLoad]; // 交换后,这里实际调用原 viewDidLoad
}

6.3 安全写法:处理子类未实现的情况

若子类未重写 viewDidLoadclass_getInstanceMethod 会拿到父类的方法。直接交换会导致:父类方法被交换,影响所有子类。更安全的做法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class cls = [UIViewController class];
SEL originalSel = @selector(viewDidLoad);
SEL swizzledSel = @selector(swizzled_viewDidLoad);

Method originalMethod = class_getInstanceMethod(cls, originalSel);
Method swizzledMethod = class_getInstanceMethod(cls, swizzledSel);

BOOL didAdd = class_addMethod(cls, originalSel,
method_getImplementation(swizzledMethod),
method_getTypeEncoding(swizzledMethod));
if (didAdd) {
class_replaceMethod(cls, swizzledSel,
method_getImplementation(originalMethod),
method_getTypeEncoding(originalMethod));
} else {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
});
}

6.4 实际应用:全局统计页面访问

1
2
3
4
5
6
7
8
9
10
11
// UIViewController+PageTrack.m
+ (void)load {
[self swizzleInstanceMethod:@selector(viewDidAppear:)
withMethod:@selector(track_viewDidAppear:)];
}

- (void)track_viewDidAppear:(BOOL)animated {
[self track_viewDidAppear:animated];
// 统计逻辑
[Analytics trackPageView:NSStringFromClass([self class])];
}

七、关联对象(Associated Objects)

7.1 为什么需要关联对象?

分类(Category)不能添加实例变量,但我们有时需要给已有类「挂」一些额外数据。关联对象可以在不修改原类的情况下,为实例绑定键值对

7.2 API

1
2
3
4
5
6
7
8
// 设置
void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);

// 获取
id objc_getAssociatedObject(id object, const void *key);

// 移除
void objc_removeAssociatedObjects(id object); // 移除该对象所有关联,慎用

7.3 内存策略(policy)

Policy 对应属性修饰符 说明
OBJC_ASSOCIATION_ASSIGN assign 弱引用
OBJC_ASSOCIATION_RETAIN_NONATOMIC nonatomic, strong 强引用,非原子
OBJC_ASSOCIATION_RETAIN atomic, strong 强引用,原子
OBJC_ASSOCIATION_COPY_NONATOMIC nonatomic, copy 拷贝,非原子
OBJC_ASSOCIATION_COPY atomic, copy 拷贝,原子

7.4 示例:为 UIButton 绑定 Block

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// UIButton+Block.h
- (void)addAction:(void (^)(UIButton *sender))block forControlEvents:(UIControlEvents)events;

// UIButton+Block.m
#import <objc/runtime.h>

static const void *kButtonBlockKey = &kButtonBlockKey;

- (void)addAction:(void (^)(UIButton *))block forControlEvents:(UIControlEvents)events {
objc_setAssociatedObject(self, kButtonBlockKey, block, OBJC_ASSOCIATION_COPY_NONATOMIC);
[self addTarget:self action:@selector(block_buttonTapped:) forControlEvents:events];
}

- (void)block_buttonTapped:(UIButton *)sender {
void (^block)(UIButton *) = objc_getAssociatedObject(self, kButtonBlockKey);
if (block) block(sender);
}

八、实际项目中的应用案例

8.1 场景一:无侵入埋点

通过 Method Swizzling Hook viewDidAppear:viewDidDisappear:,自动统计页面停留时长,无需在每个 VC 里手动写埋点代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// PageAnalytics.m
- (void)tracked_viewDidAppear:(BOOL)animated {
[self tracked_viewDidAppear:animated];
self.pageEnterTime = CACurrentMediaTime();
[Tracking logEvent:@"page_enter" properties:@{@"page": NSStringFromClass([self class])}];
}

- (void)tracked_viewDidDisappear:(BOOL)animated {
NSTimeInterval duration = CACurrentMediaTime() - self.pageEnterTime;
[Tracking logEvent:@"page_leave" properties:@{
@"page": NSStringFromClass([self class]),
@"duration": @(duration)
}];
[self tracked_viewDidDisappear:animated];
}

8.2 场景二:防崩溃( unrecognized selector)

利用消息转发,在 forwardInvocation: 中统一处理未实现的方法调用,记录日志并优雅降级,避免 Crash。

1
2
3
4
5
6
7
8
9
10
11
12
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {
NSMethodSignature *sig = [super methodSignatureForSelector:aSelector];
if (!sig) {
sig = [NSMethodSignature signatureWithObjCTypes:"v@:"]; // 兜底签名
}
return sig;
}

- (void)forwardInvocation:(NSInvocation *)anInvocation {
[CrashGuard logUnrecognizedSelector:anInvocation.selector onObject:self];
// 可选择上报、降级处理等
}

8.3 场景三:字典转模型(JSON → Model)

遍历类的属性列表,根据属性名从字典中取值并赋值,实现自动 JSON 转 Model。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+ (instancetype)modelWithDictionary:(NSDictionary *)dict {
id model = [[self alloc] init];
unsigned int count;
objc_property_t *properties = class_copyPropertyList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
const char *name = property_getName(properties[i]);
NSString *key = [NSString stringWithUTF8String:name];
id value = dict[key];
if (value && ![value isKindOfClass:[NSNull class]]) {
[model setValue:value forKey:key];
}
}
free(properties);
return model;
}

8.4 场景四:KVO 防崩溃

KVO 常见崩溃:未配对 removeObserver、重复 remove、对象已释放。可通过 Hook addObserver:forKeyPath:options:context:removeObserver:forKeyPath:,用关联对象维护观察者链表,在 dealloc 时自动移除,实现「自释放」KVO。

8.5 场景五:调试时打印对象属性

利用 class_copyIvarListclass_copyPropertyList 遍历所有属性,valueForKey: 取值并拼接成字符串,方便调试时查看对象完整状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (NSString *)debugDescription {
NSMutableString *desc = [NSMutableString stringWithFormat:@"<%@: %p>\n", [self class], self];
unsigned int count;
Ivar *ivars = class_copyIvarList([self class], &count);
for (unsigned int i = 0; i < count; i++) {
Ivar ivar = ivars[i];
const char *name = ivar_getName(ivar);
NSString *key = [NSString stringWithUTF8String:name];
id value = [self valueForKey:key];
[desc appendFormat:@" %@ = %@\n", key, value];
}
free(ivars);
return desc;
}

九、注意事项与最佳实践

9.1 线程安全

  • objc_msgSend 等查找流程内部有锁,但动态修改类结构(如 class_addMethod)需注意线程安全
  • Method Swizzling 建议在 +load 中、单次执行完成,避免并发问题

9.2 Swizzling 陷阱

  • 只在 +load 中执行,且用 dispatch_once 保证只执行一次
  • 注意子类未实现父类方法时的交换范围
  • 避免多个库对同一方法重复 Swizzling,易产生难以排查的 Bug

9.3 性能考虑

  • Runtime 动态特性有开销,高频路径慎用
  • 方法缓存使大部分调用命中缓存,性能可接受
  • 关联对象查找为哈希查找,量不大时影响较小

9.4 Swift 与 Runtime

  • Swift 类继承自 NSObject 时,仍可使用 Runtime
  • 纯 Swift 类(不继承 NSObject)使用更静态的派发方式,Runtime 能力受限
  • 若需在 Swift 中使用 Runtime,需将类/方法标记为 @objcdynamic

十、总结

主题 要点
本质 OC 是消息型语言,方法调用 = 发消息,由 Runtime 在运行期决定实际执行的 IMP
查找 缓存 → 当前类 → 父类 → 动态解析 → 快速转发 → 完整转发 → 崩溃
Swizzling 交换 IMP,实现无侵入 Hook,注意子类与并发
关联对象 为实例动态绑定数据,弥补 Category 不能加实例变量的限制
应用 埋点、防崩溃、JSON 转模型、调试工具等

理解 Runtime,不仅有助于排查「消息转发」「KVO 崩溃」等问题,还能在需要时写出更灵活、可扩展的架构。建议结合 objc4 源码 和实际项目实践,逐步加深理解。

SDWebImage 源码

由浅入深,从基本概念到源码解析,带你全面掌握图片加载框架的设计与实现


一、什么是 SDWebImage?

1.1 为什么需要图片加载库?

在 iOS 开发中,展示网络图片是极其常见的需求。如果自己实现,需要处理:

  • 异步下载:不能阻塞主线程
  • 缓存策略:内存缓存 + 磁盘缓存,避免重复下载
  • 图片解码:在主线程解码大图会导致卡顿
  • 复用与取消:列表滑动时,旧请求应及时取消,避免错乱
  • 格式支持:JPEG、PNG、GIF、WebP 等

SDWebImage 将这些能力封装成一套成熟方案,被广泛应用于 App 中。

1.2 SDWebImage 简介

SDWebImage 是一个异步图片下载与缓存库,支持 iOS、macOS、watchOS、visionOS。核心特性包括:

特性 说明
异步下载 基于 NSURLSession,不阻塞主线程
内存 + 磁盘缓存 支持自定义缓存策略、过期时间
后台解码 避免主线程解码导致的卡顿
渐进式加载 支持 JPEG 等格式的渐进显示
动图支持 GIF、APNG、WebP 动画
缩略图解码 大图可只解码指定尺寸,节省内存
协议化设计 v5.x 起核心组件可插拔、可替换

1.3 版本演进要点

版本 主要变化
4.x Block 回调、FLAnimatedImageView
5.x 协议化:Loader、Cache、Coder 均可自定义;新增 View Indicator、Image Transform
5.6+ 完善协议体系,架构更清晰

二、核心架构与原理

2.1 整体数据流

一次完整的图片加载流程大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
用户调用 sd_setImageWithURL:


┌───────────────────────────────────────┐
│ UIView+WebCache (便捷入口) │
│ sd_internalSetImageWithURL:... │
└───────────────┬───────────────────────┘


┌───────────────────────────────────────┐
│ SDWebImageManager (调度中心) │
│ loadImageWithURL:options:... │
└───────────────┬───────────────────────┘

┌───────────┼───────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Cache │ │ Loader │ │ Coder │
│ 查找缓存 │ │ 下载图片 │ │ 解码图片 │
└─────────┘ └─────────┘ └─────────┘


显示到 UIImageView

2.2 协议化设计(v5.x 核心)

v5.x 将核心能力抽象为协议,实现可替换、可扩展:

协议 默认实现 职责
SDImageCache SDImageCache 内存 + 磁盘缓存
SDImageLoader SDWebImageDownloader 网络/本地图片加载
SDImageCoder SDImageCodersManager 图片编解码
SDWebImageCacheSerializer - 自定义缓存序列化
SDWebImageIndicator SDWebImageActivityIndicator 加载状态指示器

这种设计让开发者可以:

  • 替换默认下载器(如接入自研 CDN SDK)
  • 使用自定义缓存(如接入 YYCache、PINCache)
  • 支持新图片格式(实现 Coder 协议即可)

2.3 主线程检测的演进

SDWebImage 中使用 dispatch_main_async_safe 确保 UI 更新在主线程/主队列执行。v5.x 对主线程检测做了改进:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 旧版:用 [NSThread isMainThread] 判断
#define dispatch_main_async_safe(block)\
if ([NSThread isMainThread]) { block(); } else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

// 新版:用 dispatch_queue 标签判断
#define dispatch_main_async_safe(block)\
if (dispatch_queue_get_label(DISPATCH_CURRENT_QUEUE_LABEL) == \
dispatch_queue_get_label(dispatch_get_main_queue())) {\
block();\
} else {\
dispatch_async(dispatch_get_main_queue(), block);\
}

原因:主队列 ≠ 主线程。某些场景下,非主队列的任务可能在主线程执行,若依赖 isMainThread,在依赖「主队列」的框架(如 VektorKit)中会出现问题。主队列上的任务一定在主线程执行,反之则需用队列标签判断。


三、源码解析

3.1 入口:UIView + WebCache

所有 View 的图片设置最终汇聚到 sd_internalSetImageWithURL:...

1
2
3
4
5
6
7
- (void)sd_internalSetImageWithURL:(nullable NSURL *)url
placeholderImage:(nullable UIImage *)placeholder
options:(SDWebImageOptions)options
context:(nullable SDWebImageContext *)context
setImageBlock:(nullable SDSetImageBlock)setImageBlock
progress:(nullable SDImageLoaderProgressBlock)progressBlock
completed:(nullable SDInternalCompletionBlock)completedBlock;

核心步骤(精简):

  1. 取消旧任务sd_cancelImageLoadOperationWithKey:,避免同一 View 的多次请求冲突
  2. 设置占位图:立即显示 placeholder
  3. 调用 ManagerloadImageWithURL:options:context:progress:completed:
  4. 保存 Operation:将返回的 id<SDWebImageOperation> 存入 sd_operationDictionary,便于取消

sd_operationDictionary 使用 NSMapTable,key 强引用、value 弱引用,因为 Operation 由 Manager 的 runningOperations 持有,这里仅作取消用。

3.2 SDWebImageManager:调度中心

Manager 负责串联 Cache、Loader、Coder,核心逻辑在 loadImageWithURL:options:context:progress:completed:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 1. 生成缓存 Key
NSString *key = [self cacheKeyForURL:url context:context];

// 2. 先查缓存(可选跳过)
if (!(options & SDWebImageFromLoaderOnly)) {
[self callQueryCacheOperationForKey:key ... completed:^(UIImage *cachedImage, NSData *cachedData, SDImageCacheType cacheType) {
if (cachedImage) {
// 命中缓存,直接回调
completedBlock(cachedImage, cachedData, nil, cacheType, YES, url);
return;
}
// 3. 未命中,执行下载
[self callLoadOperationWithURL:url ... completed:^(UIImage *image, NSData *data, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
// 4. 下载完成,写入缓存并回调
if (image && (options & SDWebImageCacheMemoryOnly)) {
[self.imageCache storeImage:image forKey:key ...];
}
completedBlock(image, data, error, cacheType, finished, imageURL);
}];
}];
}

流程:查缓存 → 未命中则下载 → 下载完成写缓存 → 回调

3.3 SDWebImageDownloader:下载器

下载器负责发起网络请求,支持并发数、超时、Request/Response 修改等配置:

1
2
3
4
5
6
// 核心下载接口
- (nullable SDWebImageDownloadToken *)downloadImageWithURL:(nullable NSURL *)url
options:(SDWebImageDownloaderOptions)options
context:(nullable SDWebImageContext *)context
progress:(nullable SDWebImageDownloaderProgressBlock)progressBlock
completed:(nullable SDWebImageDownloaderCompletedBlock)completedBlock;

URL 复用:同一 URL 的多次请求会复用同一个 SDWebImageDownloaderOperation,通过 addHandlersForProgress:completed: 累积多个回调,下载完成后依次执行,避免重复下载。

1
2
3
4
5
6
7
8
9
10
NSOperation<SDWebImageDownloaderOperation> *operation = [self.URLOperations objectForKey:url];
if (operation) {
@synchronized (operation) {
downloadOperationCancelToken = [operation addHandlersForProgress:progressBlock completed:completedBlock];
}
} else {
operation = [self createDownloaderOperationWithUrl:url options:options context:context];
[self.URLOperations setObject:operation forKey:url];
// ...
}

可插拔扩展

  • SDWebImageDownloaderRequestModifier:修改 Request(如加 Header)
  • SDWebImageDownloaderResponseModifier:修改 Response(如校验 MIME-Type)
  • SDWebImageDownloaderDecryptor:解密(如 Base64)

3.4 SDImageCache:缓存

缓存采用内存 + 磁盘二层结构:

1
2
3
4
5
6
7
8
9
10
11
12
// 查询缓存
- (void)queryImageForKey:(NSString *)key
options:(SDImageCacheOptions)options
context:(nullable SDWebImageContext *)context
completion:(nullable SDImageCacheQueryCompletionBlock)completionBlock;

// 存储
- (void)storeImage:(UIImage *)image
imageData:(NSData *)imageData
forKey:(NSString *)key
cacheType:(SDImageCacheType)cacheType
completion:(nullable SDWebImageNoParamsBlock)completionBlock;
  • 内存缓存:基于 NSCache,受内存压力和系统策略自动回收
  • 磁盘缓存:默认使用 NSFileManager 存储到 Library/Caches/default,可配置自定义路径

缓存 Key 默认为 URL 的绝对字符串,可通过 SDWebImageContextcacheKeyFilter 自定义。

3.5 解码与变换

为避免主线程解码导致卡顿,SDWebImage 在后台队列解码:

1
2
3
// 解码在 SDImageIOCoder / SDImageGIFCoder 等中完成
// 通过 SDImageCoder 协议统一
- (UIImage *)decodedImageWithData:(NSData *)data options:(SDImageCoderOptions *)options;

v5.x 支持 Image Transform:下载后可对图片做缩放、旋转、圆角等处理,结果再缓存,避免重复计算。


四、示例

4.1 基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#import <SDWebImage/SDWebImage.h>

// 最简单的用法
[self.imageView sd_setImageWithURL:[NSURL URLWithString:@"https://example.com/photo.jpg"]];

// 带占位图和完成回调
[self.imageView sd_setImageWithURL:url
placeholderImage:[UIImage imageNamed:@"placeholder"]
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (error) {
NSLog(@"加载失败: %@", error);
} else {
NSLog(@"加载成功,来源: %@", cacheType == SDImageCacheTypeMemory ? @"内存" :
cacheType == SDImageCacheTypeDisk ? @"磁盘" : @"网络");
}
}];

4.2 进度与选项

1
2
3
4
5
6
7
[self.imageView sd_setImageWithURL:url
placeholderImage:placeholder
options:SDWebImageRetryFailed | SDWebImageProgressiveLoad
progress:^(NSInteger receivedSize, NSInteger expectedSize, NSURL * _Nullable targetURL) {
CGFloat progress = expectedSize > 0 ? (CGFloat)receivedSize / expectedSize : 0;
self.progressView.progress = progress;
} completed:nil];

常用 options

  • SDWebImageRetryFailed:失败后重试
  • SDWebImageProgressiveLoad:渐进式加载
  • SDWebImageRefreshCached:忽略缓存强制刷新
  • SDWebImageFromLoaderOnly:只从网络加载,不查缓存

4.3 预加载

1
2
3
4
5
6
7
SDWebImagePrefetcher *prefetcher = [SDWebImagePrefetcher sharedImagePrefetcher];
prefetcher.maxConcurrentPrefetches = 4;
[prefetcher prefetchURLs:imageURLs progress:^(NSUInteger no, NSUInteger total) {
NSLog(@"预加载进度: %lu/%lu", (unsigned long)no, (unsigned long)total);
} completed:^(NSUInteger finishedCount, NSUInteger skippedCount) {
NSLog(@"完成: %lu, 跳过: %lu", (unsigned long)finishedCount, (unsigned long)skippedCount);
}];

4.4 自定义缓存 Key

1
2
3
4
5
6
SDWebImageContext *context = @{
SDWebImageContextCacheKeyFilter: [SDWebImageCacheKeyFilter cacheKeyFilterWithBlock:^NSString * _Nullable(NSURL * _Nullable url) {
return [url.absoluteString stringByAppendingString:@"_suffix"]; // 自定义 key
}]
};
[self.imageView sd_setImageWithURL:url placeholderImage:nil context:context];

4.5 图片变换(圆角、缩放)

1
2
3
4
[self.imageView sd_setImageWithURL:url
placeholderImage:placeholder
options:0
context:@{SDWebImageContextImageTransformer: [SDImageRoundCornerTransformer transformerWithRadius:10 corners:UIRectCornerAllCorners borderWidth:1 borderColor:[UIColor whiteColor]]}];

五、实际项目中的应用案例

5.1 列表图片加载与复用

场景:TableView/CollectionView 中加载头像或商品图。

做法

  • 使用 sd_setImageWithURL: 即可,SDWebImage 会自动取消不可见 cell 的请求
  • 可选 SDWebImageAvoidAutoSetImage,在 completed 中手动设置,便于加入过渡动画
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
MyCell *cell = [tableView dequeueReusableCellWithIdentifier:@"MyCell"];
NSURL *avatarURL = self.avatars[indexPath.row];
[cell.avatarView sd_setImageWithURL:avatarURL
placeholderImage:[UIImage imageNamed:@"default_avatar"]
options:SDWebImageAvoidAutoSetImage
completed:^(UIImage * _Nullable image, NSError * _Nullable error, SDImageCacheType cacheType, NSURL * _Nullable imageURL) {
if (image) {
[UIView transitionWithView:cell.avatarView duration:0.2 options:UIViewAnimationOptionTransitionCrossDissolve animations:^{
cell.avatarView.image = image;
} completion:nil];
}
}];
return cell;
}

5.2 详情页大图预加载

场景:从列表进入详情时,希望大图尽快展示。

做法:在列表滑动到某条数据时,对详情大图 URL 做预加载:

1
2
3
4
5
6
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
NSString *detailImageURL = self.detailImageURLs[indexPath.row];
[[SDWebImagePrefetcher sharedImagePrefetcher] prefetchURLs:@[[NSURL URLWithString:detailImageURL]]];
// 再 push 到详情页
[self.navigationController pushViewController:detailVC animated:YES];
}

5.3 统一添加请求头(如 Token)

场景:图片 URL 需要鉴权 Header。

做法:实现 SDWebImageDownloaderRequestModifier

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@interface MyRequestModifier : NSObject <SDWebImageDownloaderRequestModifier>
@end

@implementation MyRequestModifier
- (NSURLRequest *)modifiedRequestWithRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutable = [request mutableCopy];
[mutable setValue:[MyAuthManager shared].token forHTTPHeaderField:@"Authorization"];
return [mutable copy];
}
@end

// 配置
SDWebImageDownloader *downloader = [SDWebImageManager sharedManager].imageLoader;
downloader.requestModifier = [[MyRequestModifier alloc] init];

5.4 加载状态指示器

场景:图片加载时显示 UIActivityIndicator。

1
2
self.imageView.sd_imageIndicator = SDWebImageActivityIndicator.grayIndicator;
[self.imageView sd_setImageWithURL:url];

或自定义实现 SDWebImageIndicator 协议,适配项目 UI 规范。

5.5 自定义缓存路径与策略

场景:头像与普通图片使用不同缓存目录和过期策略。

1
2
3
4
5
6
7
// 头像缓存:30 天
SDImageCache *avatarCache = [[SDImageCache alloc] initWithNamespace:@"avatar" diskCacheDirectory:[NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES).firstObject stringByAppendingPathComponent:@"AvatarCache"]]];
avatarCache.config.maxDiskAge = 30 * 24 * 60 * 60;

// 通过 Context 指定使用 avatarCache
SDWebImageContext *context = @{SDWebImageContextCustomCache: avatarCache};
[self.avatarView sd_setImageWithURL:avatarURL placeholderImage:nil context:context];

六、小结

SDWebImage 通过协议化设计清晰的职责划分,将图片加载、缓存、解码、展示等环节解耦,既易用又易扩展。掌握其架构与源码,有助于:

  • 合理选用 optionscontext,优化体验与性能
  • 在需要时自定义 Loader、Cache、Coder,适配业务
  • 理解异步加载、缓存、取消等通用模式,迁移到其他场景

建议结合官方 GitHubWiki 阅读源码,逐层从 Category → Manager → Loader/Cache 跟踪调用链,会有更深理解。

iOS 开发中的 ReactiveCocoa (RAC)

由浅入深,从基本概念到源码解析,带你全面掌握 Swift 响应式编程


一、什么是响应式编程?

1.1 从命令式到声明式

传统的命令式编程中,我们通过直接修改状态和调用方法来驱动程序执行:

1
2
3
4
5
6
7
8
9
// 命令式:监听文本框变化
textField.addTarget(self, action: #selector(textDidChange), for: .editingChanged)

func textDidChange() {
let text = textField.text ?? ""
if text.count >= 3 {
searchUsers(text)
}
}

响应式编程则将数据流视为核心概念,用声明式的方式描述「当数据变化时该做什么」:

1
2
3
4
5
6
7
// 响应式:声明数据流关系
textField.reactive.continuousTextValues
.filter { ($0?.count ?? 0) >= 3 }
.debounce(0.3, on: QueueScheduler.main)
.observeValues { [weak self] text in
self?.searchUsers(text ?? "")
}

1.2 ReactiveCocoa 与 ReactiveSwift

  • ReactiveSwift:纯 Swift 实现的响应式核心库,提供 Signal、SignalProducer 等基础类型
  • ReactiveCocoa:基于 ReactiveSwift,为 Cocoa/Cocoa Touch 提供 UI 绑定、扩展和便捷 API

两者关系可以理解为:ReactiveSwift 是引擎,ReactiveCocoa 是上层封装。

1.3 为什么选择 RAC?

  • 统一抽象:将 delegate、回调、通知、KVO、Target-Action 等统一为「事件流」
  • 可组合:通过 map、filter、combineLatest 等操作符组合数据流
  • 减少状态:用数据流替代分散的中间变量
  • 声明式:代码更贴近「业务意图」,易于阅读和维护

二、核心概念

2.1 事件 (Event)

Event 是事件流中的最小传输单元,类似一次性的直播流中的一帧:

1
2
3
4
5
6
public enum Event<Value, Error: Swift.Error> {
case value(Value) // 携带一个值
case failed(Error) // 失败(携带错误)
case completed // 正常完成
case interrupted // 被中断
}

一个典型的流:若干个 .value,最后以 .completed.failed 结束;.interrupted 表示订阅被取消。

2.2 观察者 (Observer)

Observer 负责向事件流发送事件:

1
2
3
4
5
6
let (signal, observer) = Signal<String, Never>.pipe()

// observer 用于发送事件
observer.send(value: "Hello")
observer.send(value: "World")
observer.sendCompleted()

2.3 可销毁对象 (Disposable)

订阅信号会返回 Disposable,用于取消订阅、释放资源:

1
2
3
4
5
6
let disposable = signalProducer.start { value in
print(value)
}

// 不再需要时取消订阅
disposable.dispose()

2.4 核心类型总览

类型 描述 类比
Signal 热信号,单向事件流,所有者控制发送 直播画面
SignalProducer 冷信号,延迟执行,每次 start 创建新流 点播视频
Property 可观察的「有值」盒子,永不失败 播放进度条
Action 串行执行、可启用/禁用的操作 自动售货机
Lifetime 观察的生命周期,用于自动取消 观看时间段

三、Signal 与 SignalProducer

3.1 Signal:热信号

  • 热信号:无论有没有观察者,事件都会持续发送
  • 所有者完全控制何时发送、发送什么
  • 观察者只能订阅,不能影响流的产生
1
2
3
4
5
6
7
8
9
10
// 创建 Signal 的方式:pipe
let (signal, observer) = Signal<String, Never>.pipe()

signal.observeValues { value in
print("收到: \(value)")
}

observer.send(value: "A") // 输出: 收到: A
observer.send(value: "B") // 输出: 收到: B
observer.sendCompleted()

3.2 SignalProducer:冷信号

  • 冷信号:只有在 start 时才真正执行
  • 每次 start 都会创建新的 Signal,重新跑一遍逻辑
  • 适合网络请求、文件读取等「按需执行」的场景
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let producer = SignalProducer<String, Never> { observer, _ in
print("开始执行") // 每次 start 都会打印
observer.send(value: "Hello")
observer.send(value: "World")
observer.sendCompleted()
}

let d1 = producer.startWithValues { print("观察者1: \($0)") }
// 输出: 开始执行
// 输出: 观察者1: Hello
// 输出: 观察者1: World

let d2 = producer.startWithValues { print("观察者2: \($0)") }
// 再次输出: 开始执行
// 输出: 观察者2: Hello
// 输出: 观察者2: World

3.3 热信号 vs 冷信号

特性 Signal (热) SignalProducer (冷)
执行时机 由发送者决定 start 时执行
多订阅 共享同一流 每个订阅独立执行
典型场景 按钮点击、通知 网络请求、文件读取

四、Property 与 Action

4.1 Property:可观察的盒子

Property 表示「始终有一个当前值」且「永不失败」的流,适合表示 UI 状态、配置等:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// MutableProperty:可读可写
let username = MutableProperty<String>("")

// 读取当前值
print(username.value)

// 监听变化
username.signal.observeValues { newValue in
print("用户名变为: \(newValue)")
}

// 更新值
username.value = "张三"
username.modify { $0 = "李四" }

4.2 Action:串行操作

Action 将「输入 → 输出」封装为可复用、可启用/禁用的操作,且保证串行执行(同一时间只处理一个请求):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
let searchAction = Action<String, [User], NSError> { keyword in
return searchAPI(keyword) // 返回 SignalProducer
}

// 绑定启用条件(例如:输入非空)
let enabledSearch = Action(state: viewModel.keyword, enabledIf: { $0.count > 0 }) { _, keyword in
return searchAPI(keyword)
}

// 执行
searchAction.apply("Swift").startWithResult { result in
switch result {
case .success(let users):
self.users = users
case .failure(let error):
self.showError(error)
}
}

五、常用操作符

5.1 转换类

map:转换每个值

1
2
3
signal
.map { $0.uppercased() }
.observeValues { print($0) }

filter:过滤值

1
2
3
signal
.filter { $0.count >= 3 }
.observeValues { print($0) }

reduce:聚合为单个值(在 completed 时发出)

1
2
3
signal
.reduce(0) { $0 + $1 }
.observeValues { print("总和: \($0)") }

5.2 组合类

combineLatest:合并多个流的最新值

1
2
3
4
let combined = Signal.combineLatest(usernameSignal, passwordSignal)
combined.observeValues { (user, pwd) in
loginButton.isEnabled = user.count > 0 && pwd.count >= 6
}

zip:按顺序一一配对

1
2
let zipped = Signal.zip(numbersSignal, lettersSignal)
// (1,A), (2,B), (3,C)...

5.3 扁平化 (flatten)

merge:内层多个流的值按到达顺序全部输出

concat:按顺序消费内层流,前一个完成后才订阅下一个

latest:只保留「最新」的内层流,常用于搜索联想(新关键词来时取消旧请求)

1
2
3
4
5
6
7
searchKeywordSignal
.flatMap(.latest) { keyword in
searchAPI(keyword) // 新关键词会取消上一次请求
}
.observeValues { users in
self.updateUI(users)
}

5.4 错误处理

1
2
3
4
producer
.flatMapError { _ in SignalProducer(value: []) } // 失败时返回空数组
.retry(upTo: 3) // 失败重试 3 次
.mapError { CustomError.wrapped($0) } // 转换错误类型

六、源码与实现原理

6.1 Signal 的核心结构

Signal 通过闭包保存「如何向观察者推送事件」的逻辑,内部维护观察者列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
// Signal 的核心:Generator 闭包
// 当有观察者订阅时,闭包被调用,传入 Observer
public init(_ generator: (Observer) -> Disposable?)

// pipe 创建方式
public static func pipe() -> (Signal, Signal.Observer) {
var observer: Signal.Observer!
let signal = Signal { innerObserver in
observer = innerObserver
return nil
}
return (signal, observer)
}

observer.send(value:) 被调用时,所有已注册的观察者都会收到该值。

6.2 SignalProducer 的延迟执行

SignalProducer 保存的是「如何创建 Signal」的闭包,而不是 Signal 本身:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 每次 start 时:
// 1. 创建一个新的 Signal(通过 pipe 或类似机制)
// 2. 执行 generator 闭包,将 observer 传入
// 3. 闭包内的逻辑开始执行,向 observer 发送事件
public func start(_ observer: Observer) -> Disposable {
let (signal, pipeObserver) = Signal.pipe()
let disposable = CompositeDisposable()
disposable += signal.observe(observer)
disposable += self.startWithSignal { signal, innerDisposable in
pipeObserver.observe(signal)
disposable += innerDisposable
}
return disposable
}

因此每次 start 都会触发一次完整的「创建 + 执行」过程。

6.3 操作符的链式调用

mapfilter 等操作符本质是创建新的 Signal/SignalProducer,内部订阅上游并转换后传给下游:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// map 的简化逻辑
extension Signal {
public func map<U>(_ transform: @escaping (Value) -> U) -> Signal<U, Error> {
return Signal { observer in
return self.observe { event in
switch event {
case let .value(value):
observer.send(value: transform(value))
case .completed:
observer.sendCompleted()
case let .failed(error):
observer.send(error: error)
case .interrupted:
observer.sendInterrupted()
}
}
}
}
}

七、实战示例

7.1 登录表单校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class LoginViewModel {
let username = MutableProperty("")
let password = MutableProperty("")

var canLogin: Property<Bool> {
return Property(
initial: false,
then: Signal.combineLatest(username.signal, password.signal)
.map { user, pwd in
user.count >= 3 && pwd.count >= 6
}
)
}

let loginAction: Action<(String, String), User, Error>

init() {
loginAction = Action(enabledIf: canLogin) { [weak self] _, input in
return loginAPI(username: input.0, password: input.1)
}
}
}

// ViewController 中绑定
viewModel.canLogin.signal
.observeValues { [weak loginButton] canLogin in
loginButton?.isEnabled = canLogin
}

loginButton.reactive.pressed = CocoaAction(viewModel.loginAction) { _ in
(viewModel.username.value, viewModel.password.value)
}

7.2 搜索联想(防抖 + 取消旧请求)

1
2
3
4
5
6
7
8
9
10
11
12
13
searchTextField.reactive.continuousTextValues
.filter { ($0 ?? "").count >= 2 }
.debounce(0.3, on: QueueScheduler.main)
.flatMap(.latest) { [weak self] keyword -> SignalProducer<[User], Error> in
guard let keyword = keyword, !keyword.isEmpty else {
return .init(value: [])
}
return self?.searchAPI(keyword) ?? .empty
}
.observe(on: UIScheduler())
.startWithValues { [weak self] users in
self?.updateSearchResults(users)
}

7.3 多数据源合并展示

1
2
3
4
5
6
7
8
9
10
11
12
13
let localUsers = loadLocalUsers()   // SignalProducer<[User], Never>
let remoteUsers = fetchRemoteUsers() // SignalProducer<[User], Error>

SignalProducer.combineLatest(
localUsers.flatMapError { _ in .init(value: []) },
remoteUsers.flatMapError { _ in .init(value: []) }
)
.map { local, remote in
mergeAndDeduplicate(local: local, remote: remote)
}
.startWithValues { [weak self] users in
self?.tableView.reload(with: users)
}

八、ReactiveCocoa 的 Cocoa 扩展

ReactiveCocoa 为 UIKit 提供了 reactive 命名空间下的扩展:

1
2
3
4
5
6
7
8
9
10
11
12
13
// UIControl 事件
button.reactive.controlEvents(.touchUpInside)
.observeValues { _ in print("点击") }

// UITextField 文本
textField.reactive.continuousTextValues
.observeValues { print($0 ?? "") }

// 双向绑定(需要导入 ReactiveCocoa)
label.reactive.text <~ viewModel.title // 单向:ViewModel -> Label

// CocoaAction 绑定按钮
button.reactive.pressed = CocoaAction(viewModel.submitAction)

九、最佳实践与注意事项

  1. 避免循环引用:在闭包中使用 [weak self],在适当时机 dispose 订阅
  2. 主线程更新 UI:使用 .observe(on: UIScheduler()) 确保 UI 更新在主线程
  3. 合理选择热/冷信号:异步任务、网络请求用 SignalProducer;UI 事件、通知用 Signal
  4. 错误类型:尽量使用 Never 表示不会失败的流,便于组合
  5. 利用 Lifetime:在 ViewController/View 销毁时通过 Lifetime 自动取消订阅

十、总结

概念 要点
Event 事件流的传输单元:value / failed / completed / interrupted
Signal 热信号,由所有者控制,多订阅共享
SignalProducer 冷信号,延迟执行,每次 start 独立运行
Property 有当前值、不失败的流,适合状态
Action 串行、可启用/禁用的操作封装
操作符 map、filter、combineLatest、flatMap 等组合与转换流
实践 表单校验、搜索联想、多源合并、UI 绑定

掌握这些概念后,你就能用响应式思维重构业务逻辑,写出更清晰、更易维护的 Swift/iOS 代码。


参考资源