iOS 开发的深度面试往往围绕运行时机制、内存管理、多线程、视图渲染、架构设计等核心领域展开。本文将系统梳理这些领域的高频问题,并提供清晰、完整、可直接用于面试的答案,帮助开发者构建扎实的知识体系。
一、Runtime 核心原理
Runtime(运行时)是 OC 的灵魂,负责对象的创建、方法调用、消息转发等底层操作。其开源源码可参考苹果官方的objc4仓库。
1. Runtime 内存模型(isa、对象、类、metaclass)
OC 的内存模型以isa 指针 为核心,串联起实例对象、类对象、元类(metaclass) 三层结构,每层对应不同的结构体,存储不同信息。
(1)核心结构关系
plaintext
rust
实例对象(Instance)-> 类对象(Class)-> 元类(Metaclass)-> 根元类(Root Metaclass)
-
实例对象(Instance) :存储成员变量(ivar) 的值,其
isa指针指向对应的类对象 。结构体简化:
objc
arduinostruct Instance { Class isa; // 指向类对象 // 成员变量的值(如NSString *name; int age;) }; -
类对象(Class) :存储实例方法(-method)、属性(property)、协议(protocol) ,其
isa指针指向元类 ,同时包含指向父类的superclass指针。结构体核心依赖
class_data_bits_t,内部通过data()方法获取class_rw_t(可读写数据):objc
arduinostruct objc_class { Class isa; // 指向元类 Class superclass; // 指向父类 class_data_bits_t bits; // 存储类的核心数据 }; -
元类(Metaclass) :存储类方法(+method) ,其
isa指针指向根元类 (如NSObject的元类),superclass指向父类的元类。元类的本质是 "类的类"------ 因为类对象也是 OC 对象(可调用
+method),需要元类来管理其方法。 -
根元类(Root Metaclass) :所有元类的最终父类(如
NSObject的元类),其isa指针指向自身 ,superclass指向根类(如NSObject)。
(2)isa 指针的作用
-
本质是
Class类型的指针,用于定位对象的 "所属类" :- 实例对象的
isa→ 类对象(确定实例能调用哪些实例方法); - 类对象的
isa→ 元类(确定类能调用哪些类方法)。
- 实例对象的
-
64 位系统中,isa 指针通过位掩码 存储额外信息(如对象是否在堆上、引用计数等),需通过
ISA_MASK提取真实的类地址。
2. 为什么要设计 metaclass?
核心目的是解决 "类方法的存储归属" 问题:
- OC 中,实例方法的调用依赖实例对象的
isa找到类对象,类对象存储实例方法列表; - 类方法(如
+alloc)的调用者是 "类对象",而类对象本身也是 OC 对象(可被isa指向),因此需要一个专门的 "类"(元类)来存储类方法列表。 - 若没有 metaclass,类方法将无处存储,导致
[NSObject alloc]这类调用无法实现。
3. class_copyIvarList & class_copyPropertyList 区别
两者均用于获取类的成员信息,但针对的对象和返回内容完全不同,核心区别如下:
| 对比维度 | class_copyIvarList | class_copyPropertyList |
|---|---|---|
| 获取的内容 | 成员变量(ivar) | 属性(property) |
| 本质区别 | 编译时定义的 "底层变量"(如_name) |
封装后的 "属性"(含 setter/getter) |
| 是否包含合成变量 | 是(如@property合成的_name) |
是(直接返回属性本身) |
| 访问权限 | 可获取私有 ivar(如类内部定义的int _age) |
仅获取属性(私有 property 也可获取) |
| 返回类型 | Ivar *(成员变量指针数组) | objc_property_t *(属性指针数组) |
示例 :
若类定义为@interface Person : NSObject { int _weight; } @property (nonatomic, copy) NSString *name; @end,则:
class_copyIvarList返回_weight和_name(合成的 ivar);class_copyPropertyList仅返回name(属性)。
4. class_rw_t 和 class_ro_t 的区别
两者均是类对象的核心数据结构,存储方法、属性、协议等信息,但核心区别在于读写权限和初始化时机:
| 对比维度 | class_rw_t(Read-Write) | class_ro_t(Read-Only) |
|---|---|---|
| 读写权限 | 可读写(运行时可修改) | 只读(编译时确定,不可修改) |
| 初始化时机 | 运行时(类第一次被使用时初始化) | 编译时(编译器生成,存储在 Mach-O 的__DATA段) |
| 存储内容 | 包含class_ro_t的指针 + 运行时添加的方法 / 属性 / 协议(如 Category 的内容) |
编译时确定的 "固定信息":初始方法列表、属性列表、协议列表、成员变量信息 |
| 核心作用 | 支持动态添加内容(如 Category、Method Swizzle) | 存储类的 "静态基础信息",确保编译后不可篡改 |
关系 :class_rw_t内部有一个const class_ro_t *ro指针,指向类的只读基础数据;运行时动态添加的内容(如 Category 的方法)会直接存入class_rw_t。
5. Category 加载流程 & 方法优先级
Category(分类)是 OC 中动态扩展类功能的核心机制,其加载和方法调用有严格的顺序规则。
(1)Category 加载流程(运行时阶段)
-
编译时 :编译器将 Category 编译为
category_t结构体,存储分类的方法列表、属性列表、协议列表 ,以及所属的类名。
category_t结构体简化:objc
arduinostruct category_t { const char *name; // 所属类名 classref_t cls; // 所属类(运行时绑定) struct method_list_t *instance_methods; // 实例方法 struct method_list_t *class_methods; // 类方法 struct protocol_list_t *protocols; // 协议 struct property_list_t *properties; // 属性 }; -
运行时(map_images 阶段) :
- dyld(动态链接器)加载完所有类和分类后,调用
_objc_init初始化 Runtime; - Runtime 通过
_processCatlist遍历所有category_t,将分类的方法、属性、协议合并到所属类的class_rw_t中 (实例方法合并到类的instance_methods,类方法合并到元类的class_methods)。
- dyld(动态链接器)加载完所有类和分类后,调用
-
合并规则 :
分类的方法会插入到类原有方法列表的前面(而非替换),因此分类方法会 "覆盖" 类的同名方法(实际是优先调用)。
(2)Category 的 load 方法加载顺序
+load方法是 Category 中特殊的方法,不遵循消息转发机制,由 Runtime 直接调用,顺序规则如下:
- 类的 load 先于分类的 load :先调用所有类的
+load(父类 → 子类),再调用所有分类的+load; - 同类分类的 load 按编译顺序 :Xcode 编译时,后添加到项目的分类,其
+load先被调用(可通过 "Build Phases → Compile Sources" 调整顺序); - 不同类分类的 load 按类的加载顺序 :依赖类的加载顺序(如 A 类依赖 B 类,则 B 类的分类
+load先调用)。
(3)Category 同名方法的调用顺序
当多个分类(或类与分类)有同名方法时,调用顺序遵循 "后编译的分类优先":
-
分类方法覆盖类的同名方法(因分类方法在方法列表前面);
-
多个分类的同名方法,后编译的分类方法先被调用(编译顺序可通过 Xcode 调整);
-
父类分类的方法优先级低于子类的分类(因子类的类加载晚于父类)。
注意 :分类无法覆盖+load和+initialize方法(+initialize遵循消息转发,会先调用父类的)。
6. Category & Extension 区别 + 能否给 NSObject 添加 Extension?
(1)核心区别
| 对比维度 | Category(分类) | Extension(扩展) |
|---|---|---|
| 能否添加成员变量 | 不能(仅能添加方法、属性、协议,属性不会自动合成 ivar,需手动关联) | 能(可添加私有成员变量、方法、属性) |
| 可见性 | 公开(需在.h 中声明,或匿名分类在.m 中) | 私有(仅在定义的.m 文件中可见) |
| 编译时机 | 运行时合并到类中 | 编译时作为类的一部分(与类同时编译) |
| 是否需要实现 | 可单独实现(.m 文件) | 必须在类的.m 文件中实现(否则编译报错) |
| 核心用途 | 扩展已有类的功能(如给 UIView 加分类) | 给类添加私有成员(如在.m 中隐藏细节) |
(2)能否给 NSObject 添加 Extension?
不能直接添加,原因如下:
-
Extension 是类的 "一部分",必须在类的定义文件(.m)中声明和实现;
-
NSObject 是系统类,开发者无法修改其.m 文件,因此无法直接为其添加 Extension;
-
若强行在自己的文件中声明
@interface NSObject () { int _myVar; } @end,编译时会报错("Category is not allowed on 'NSObject'")。
替代方案:若需给 NSObject 添加私有成员,可通过 "匿名分类 + 关联对象" 实现,或自定义 NSObject 的子类。
7. 消息转发机制 + 与其他语言对比
OC 的方法调用本质是 "发送消息"(objc_msgSend),当消息无法被接收者处理时,会触发消息转发机制,避免崩溃。
(1)消息转发三阶段(完整流程)
在进入转发前,会先进行方法查找:
-
从接收者的类的缓存(cache_t) 中查找方法(快速查找);
-
缓存未命中,从类的
class_rw_t的方法列表中查找,若未找到则递归查找父类(直到根类NSObject); -
若所有父类均未找到,进入动态方法解析 → 快速转发 → 慢速转发三阶段。
具体转发流程:
-
动态方法解析(Resolve) :
- 调用
+resolveInstanceMethod:(实例方法)或+resolveClassMethod:(类方法),允许开发者动态添加方法实现; - 示例:若
[person run]未实现,可在resolveInstanceMethod中用class_addMethod添加run的实现; - 若返回
YES,则重新发起消息查找;若返回NO,进入下一阶段。
- 调用
-
快速转发(Fast Forwarding) :
- 调用
-forwardingTargetForSelector:,允许开发者将消息转发给其他对象("替身"); - 示例:返回
self.otherObject,则消息会转发给otherObject处理; - 若返回非
nil,则消息转发给该对象;若返回nil,进入下一阶段。
- 调用
-
慢速转发(Slow Forwarding) :
- 调用
-methodSignatureForSelector:,获取方法签名(返回值类型、参数类型); - 若返回
nil,则触发崩溃(unrecognized selector sent to instance); - 若返回有效签名,调用
-forwardInvocation:,开发者可在该方法中自定义消息处理逻辑(如转发给多个对象、记录日志)。
- 调用
(2)与其他语言(如 Java)的消息机制对比
| 对比维度 | OC(消息转发) | Java(方法调用) |
|---|---|---|
| 绑定时机 | 运行时绑定(动态) | 编译时绑定(静态,除非用反射) |
| 方法不存在的处理 | 触发消息转发,可自定义处理(避免崩溃) | 编译时报错(若未声明)或运行时抛NoSuchMethodError |
| 灵活性 | 高(支持动态添加方法、转发消息) | 低(需提前声明方法,反射仅能绕过编译检查) |
| 性能 | 略低(运行时查找和转发有开销) | 高(编译时确定方法地址) |
| 崩溃风险 | 可通过转发避免崩溃 | 无法避免(除非用 try-catch 捕获异常) |
8. 方法调用前的准备:消息查找流程
在 "动态解析→消息转发" 之前,Runtime 会先执行消息查找流程(分为快速查找和慢速查找),这是方法调用的核心前置步骤:
-
快速查找(缓存查找) :
- 调用
objc_msgSend时,先从接收者的类的cache_t(缓存)中查找方法; cache_t是哈希表,key 为SEL(方法选择器),value 为IMP(方法实现指针);- 若找到
IMP,直接跳转到实现执行;若未找到,进入慢速查找。
- 调用
-
慢速查找(方法列表查找) :
- 从类的
class_rw_t的method_list中遍历查找SEL(按方法列表顺序); - 若未找到,递归查找父类的
method_list(直到根类NSObject); - 若找到,将
SEL和IMP存入当前类的cache_t(缓存,供下次快速查找),然后执行IMP; - 若所有父类均未找到,进入动态方法解析和消息转发。
- 从类的
9. IMP、SEL、Method 的区别和使用场景
三者是 Runtime 中描述 "方法" 的核心概念,关系为:Method包含SEL和IMP,SEL是方法标识,IMP是方法实现地址。
| 概念 | 定义 | 本质 | 核心作用 | 使用场景 |
|---|---|---|---|---|
| SEL | 方法选择器(typedef const struct objc_selector *SEL;) |
字符串(方法名的哈希值) | 唯一标识一个方法(如@selector(run)) |
方法调用(objc_msgSend(person, @selector(run)))、判断方法是否存在([person respondsToSelector:@selector(run)]) |
| IMP | 方法实现指针(typedef id (*IMP)(id, SEL, ...);) |
函数指针 | 指向方法的具体实现代码 | 直接调用方法(跳过消息查找,如IMP imp = [person methodForSelector:@selector(run)]; imp(person, @selector(run));)、Method Swizzle |
| Method | 方法结构体(typedef struct objc_method *Method;) |
包含 SEL、IMP、方法签名 | 封装方法的完整信息 | 获取方法详情(如method_getName(method)获取 SEL、method_getImplementation(method)获取 IMP)、动态添加方法(class_addMethod) |
关系示例 :
Method method = class_getInstanceMethod([Person class], @selector(run));
SEL sel = method_getName(method); // 获取 SEL
IMP imp = method_getImplementation(method); // 获取 IMP
10. load、initialize 方法的区别(含继承关系)
+load和+initialize是类初始化时的两个特殊方法,但触发时机、调用逻辑、继承行为完全不同。
(1)核心区别
| 对比维度 | +load 方法 | +initialize 方法 |
|---|---|---|
| 触发时机 | 类 / 分类被加载到内存时(dyld 阶段) | 类第一次接收消息时(如[Person alloc]) |
| 调用方式 | Runtime 直接调用(不经过 objc_msgSend) | 经过消息转发(objc_msgSend) |
| 是否自动调用父类 | 是(父类 load 先于子类 load) | 否(仅当子类未实现时,才调用父类) |
| 分类是否覆盖 | 否(类和分类的 load 都会调用) | 是(分类的 initialize 会覆盖类的) |
| 调用次数 | 仅一次(类加载时) | 仅一次(类第一次使用时) |
| 线程安全 | 是(Runtime 加锁,串行调用) | 否(需手动加锁,避免多线程调用) |
(2)继承关系中的区别
-
+load :
父类的
+load先于子类的+load调用,且所有类的 load 调用完后,才调用分类的 load 。示例:
NSObject→Person(子类) →Person+Category1→Person+Category2(按编译顺序)。 -
+initialize :
仅当子类未实现
+initialize时,才会调用父类的+initialize(因消息转发会先查找子类,子类未实现则找父类)。示例:
objc
less@interface Father : NSObject @end @implementation Father + (void)initialize { NSLog(@"Father initialize"); } @end @interface Son : Father @end @implementation Son // 未实现initialize @end // 调用 [Son alloc] 时,会先调用 Father 的 initialize(因Son未实现)若子类实现了
+initialize,则仅调用子类的,父类的不会被调用(除非父类单独被使用)。
11. 消息转发机制的优劣
(1)优点
- 灵活性高:允许动态添加方法、转发消息,适配复杂场景(如 "多继承" 模拟、解耦);
- 容错性强 :可捕获 "未实现的方法",避免崩溃(如在
forwardInvocation中记录日志或返回默认值); - 支持 AOP(面向切面编程) :通过转发机制在方法调用前后插入逻辑(如埋点、权限校验)。
(2)缺点
- 性能开销:消息查找(缓存→方法列表→父类)+ 转发(三阶段)会增加运行时开销,频繁触发会影响性能;
- 调试难度大:方法调用链路长,崩溃时的调用栈可能不完整(如转发后崩溃,难以定位原始调用者);
- 可读性差:动态转发逻辑隐藏在底层,代码维护成本高(如新人难以理解 "为什么未实现的方法能执行")。
二、内存管理
iOS 内存管理的核心是引用计数 ,Runtime 通过SideTable、autoreleasepool等机制实现自动管理,ARC 则进一步简化了开发者的操作。
1. weak 的实现原理 + SideTable 结构
weak是 OC 中用于避免循环引用的弱引用机制,其核心是通过SideTable管理弱引用表,确保对象释放时自动将weak指针置为nil。
(1)SideTable 结构
SideTable是 Runtime 中的全局哈希表,每个对象的引用计数和弱引用均由SideTable管理,结构简化如下:
objc
arduino
struct SideTable {
spinlock_t slock; // 自旋锁(保证线程安全)
RefcountMap refcnts; // 引用计数表(key:对象指针,value:引用计数)
weak_table_t weak_table; // 弱引用表(存储所有指向该对象的weak指针)
};
- spinlock_t:轻量级锁,适用于短时间持有(如修改引用计数时),避免线程竞争;
- RefcountMap :
std::unordered_map<DisguisedPtr<objc_object>, size_t>,存储对象的引用计数; - weak_table_t :弱引用表,结构为
std::unordered_map<DisguisedPtr<objc_object>, weak_entry_t>,weak_entry_t内部存储所有指向该对象的weak指针数组。
(2)weak 实现原理
-
weak 指针赋值时 (如
__weak Person *weakP = person;):- Runtime 通过
objc_storeWeak(&weakP, person)将weakP添加到person对应的SideTable的weak_table中; - 若
person为nil,则直接将weakP置为nil(不操作SideTable)。
- Runtime 通过
-
对象释放时 (
dealloc阶段):- 调用
objc_clear_deallocating,从SideTable中找到该对象的weak_entry_t; - 遍历
weak_entry_t中的所有weak指针,将其置为nil; - 从
weak_table中删除该weak_entry_t,并清空引用计数表中的条目。
- 调用
-
核心优势 :
weak指针不会增加对象的引用计数,且对象释放时自动置为nil,避免野指针访问。
2. 关联对象的应用 + 系统实现 + 内存管理
关联对象(Associated Object)是 Category 中 "间接添加成员变量" 的机制,通过 Runtime API 将对象与另一个对象关联。
(1)关联对象的应用
-
给 Category 添加 "成员变量" :Category 不能直接添加 ivar,但可通过关联对象存储数据;
-
解耦数据存储 :如给 UIView 关联一个
NSString *identifier,无需继承 UIView; -
临时存储上下文:如网络请求回调中,将请求参数与回调 block 关联。
示例:
objc
less
// 给UIView添加分类,关联identifier
@interface UIView (Identifier)
@property (nonatomic, copy) NSString *identifier;
@end
@implementation UIView (Identifier)
static const void *kIdentifierKey = &kIdentifierKey;
- (void)setIdentifier:(NSString *)identifier {
// 关联对象:key=kIdentifierKey,value=identifier,策略=OBJC_ASSOCIATION_COPY_NONATOMIC
objc_setAssociatedObject(self, kIdentifierKey, identifier, OBJC_ASSOCIATION_COPY_NONATOMIC);
}
- (NSString *)identifier {
return objc_getAssociatedObject(self, kIdentifierKey);
}
@end
(2)系统实现原理
关联对象的管理依赖 Runtime 内部的全局哈希表,核心结构如下:
-
AssociationsManager :单例管理器,持有
AssociationsHashMap,并通过自旋锁保证线程安全; -
AssociationsHashMap :
unordered_map<DisguisedPtr<objc_object>, ObjectAssociationMap>,key 是 "被关联的对象"(如 UIView 实例),value 是该对象的关联表; -
ObjectAssociationMap :
unordered_map<void *, ObjcAssociation>,key 是开发者定义的key(如kIdentifierKey),value 是ObjcAssociation(存储关联值和内存管理策略); -
ObjcAssociation :存储关联值(
id _value)和内存管理策略(objc_AssociationPolicy)。
操作流程:
objc_setAssociatedObject:通过AssociationsManager找到AssociationsHashMap,根据 "被关联对象" 找到ObjectAssociationMap,存入key和ObjcAssociation;objc_getAssociatedObject:反向查找,根据 "被关联对象" 和key获取ObjcAssociation中的_value;objc_removeAssociatedObjects:删除 "被关联对象" 对应的ObjectAssociationMap。
(3)关联对象的内存管理
关联对象的内存管理由objc_AssociationPolicy(关联策略)决定,策略对应 ARC 下的内存语义:
| 关联策略 | 内存语义(ARC) | 作用 |
|---|---|---|
| OBJC_ASSOCIATION_ASSIGN | assign | 弱引用,不 retain,对象释放后变为野指针 |
| OBJC_ASSOCIATION_RETAIN_NONATOMIC | strong(非原子) | retain 关联值,线程不安全 |
| OBJC_ASSOCIATION_COPY_NONATOMIC | copy(非原子) | copy 关联值,线程不安全 |
| OBJC_ASSOCIATION_RETAIN | strong(原子) | retain 关联值,线程安全 |
| OBJC_ASSOCIATION_COPY | copy(原子) | copy 关联值,线程安全 |
释放时机:
- 当 "被关联对象" 释放时(
dealloc),Runtime 会自动调用objc_removeAssociatedObjects,根据关联策略释放关联值(如retain策略会release关联值); - 也可手动调用
objc_removeAssociatedObjects移除所有关联值。
(4)关联对象如何实现 weak 属性
关联对象本身不支持weak策略(OBJC_ASSOCIATION_ASSIGN是assign,非weak),但可通过弱引用容器实现:
-
自定义一个
WeakContainer类,内部用__weak持有目标对象; -
将
WeakContainer实例作为关联值,策略设为OBJC_ASSOCIATION_RETAIN_NONATOMIC; -
访问时从
WeakContainer中获取__weak对象,实现弱引用效果。
示例:
objc
less
// 弱引用容器
@interface WeakContainer : NSObject
@property (nonatomic, weak) id value;
@end
@implementation WeakContainer
@end
// 关联时使用容器
- (void)setWeakValue:(id)weakValue {
WeakContainer *container = [WeakContainer new];
container.value = weakValue;
objc_setAssociatedObject(self, kWeakValueKey, container, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)weakValue {
WeakContainer *container = objc_getAssociatedObject(self, kWeakValueKey);
return container.value; // 弱引用,对象释放后为nil
}
3. Autoreleasepool 原理 + 数据结构
Autoreleasepool(自动释放池)是 iOS 中管理临时对象内存的机制,通过延迟释放对象,避免频繁调用release。
(1)核心原理
-
作用 :收集调用
autorelease的对象,在Autoreleasepool销毁时,对池内所有对象调用release; -
触发时机:
- 主线程:RunLoop 的每个循环周期结束时(如
kCFRunLoopBeforeWaiting),自动销毁并重建Autoreleasepool; - 子线程:需手动创建
@autoreleasepool {},否则对象可能无法及时释放; - 手动销毁:
@autoreleasepool {}代码块执行完毕时,池内对象被release。
- 主线程:RunLoop 的每个循环周期结束时(如
(2)数据结构
Autoreleasepool基于双向链表 实现,核心结构是AutoreleasePoolPage:
-
AutoreleasePoolPage:每个 Page 是 4096 字节(一页内存),结构简化如下:
objc
arduinoclass AutoreleasePoolPage { static const size_t SIZE = 4096; // 4KB AutoreleasePoolPage *next; // 下一个Page(链表节点) AutoreleasePoolPage *prev; // 上一个Page id *begin; // Page内存储对象的起始地址 id *end; // Page内存储对象的结束地址 id *top; // 当前存储对象的下一个位置(栈指针) pthread_t thread; // 所属线程(每个线程对应一个Page链表) }; -
Page 链表 :当一个 Page 装满(
top == end)时,创建新的 Page 并加入链表; -
POOL_BOUNDARY :哨兵对象,标记
Autoreleasepool的边界。@autoreleasepool {}会在开始时压入POOL_BOUNDARY,结束时从top向下遍历,直到遇到POOL_BOUNDARY,对中间所有对象调用release,并将top重置到POOL_BOUNDARY之后。
(3)操作流程
- 创建
Autoreleasepool:调用objc_autoreleasePoolPush(),压入POOL_BOUNDARY,返回其地址; - 对象调用
autorelease:调用objc_autorelease(),将对象指针存入当前 Page 的top位置,top自增;若当前 Page 满,创建新 Page 并继续存储; - 销毁
Autoreleasepool:调用objc_autoreleasePoolPop(POOL_BOUNDARY),从top向下遍历,对每个对象调用release,直到遇到POOL_BOUNDARY,并调整top指针。
4. ARC 实现原理 + 优化
ARC(Automatic Reference Counting)是编译器和 Runtime 协作的自动内存管理机制,核心是 "编译器自动插入引用计数操作代码"。
(1)ARC 实现原理
-
编译器层面:
-
分析代码中对象的生命周期,在合适的位置自动插入
retain、release、autorelease; -
例如:
objc
csharp// ARC代码 - (void)test { Person *p = [[Person alloc] init]; // 编译器自动插入 [p retain](实际alloc返回的对象引用计数为1,无需retain) } // 函数结束时,编译器自动插入 [p release] -
遵循 "谁持有,谁释放" 原则:局部变量出作用域时释放,成员变量在对象
dealloc时释放。
-
-
Runtime 层面:
- 提供
objc_retain、objc_release、objc_autorelease等 API,供编译器插入调用; - 通过
SideTable管理引用计数,确保线程安全; - 处理
weak指针(如对象释放时置为nil)。
- 提供
(2)ARC 对 retain & release 的优化
ARC 通过编译器和 Runtime 优化,减少不必要的retain/release操作,提升性能:
-
返回值优化(NRVO - Named Return Value Optimization) :
若函数返回局部对象,编译器直接将对象的所有权转移给调用者,避免插入
autorelease和retain;示例:
objc
ini- (Person *)createPerson { Person *p = [[Person alloc] init]; // 局部对象 return p; // ARC优化:直接返回p,无需autorelease } // 调用者:Person *p = [self createPerson]; 无需retain -
Toll-Free Bridging 优化 :
当 Core Foundation 对象(如
CFStringRef)与 OC 对象(如NSString)桥接时,ARC 自动管理引用计数,避免手动调用CFRetain/CFRelease;示例:
NSString *str = (__bridge_transfer NSString *)CFStringCreateWithCString(...);,__bridge_transfer让 ARC 接管 CF 对象的释放。 -
局部变量优化 :
若局部变量仅在当前作用域使用,且无外部引用,编译器会省略
retain/release(如循环内的临时对象)。 -
零成本异常处理 :
MRC 中,异常抛出时需手动处理
release;ARC 中,编译器通过@try/@finally自动插入release,且优化了异常处理的性能开销。
5. ARC 下的内存泄漏场景
ARC 虽自动管理内存,但仍存在以下常见泄漏场景:
-
循环引用:
- Block 与 self 循环引用 :
self持有 block,block 持有self(如self.block = ^{ [self doSomething]; };);
解决:用__weak typeof(self) weakSelf = self;打破循环。 - ** delegate 循环引用 **:若
delegate用strong修饰(如@property (nonatomic, strong) id<Delegate> delegate;),会导致委托方与被委托方循环引用;
解决:delegate用weak修饰。 - 容器与对象循环引用 :对象持有容器,容器存储对象(如
self.array = @[self];);
解决:用weak容器(如NSArray存储WeakContainer)。
- Block 与 self 循环引用 :
-
NSTimer 未 invalidate :
NSTimer会retain其target(如self.timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(tick) userInfo:nil repeats:YES];),若self持有timer,会形成循环引用;解决:在
dealloc或页面销毁时调用[self.timer invalidate]; self.timer = nil;。 -
AFN 请求未取消 :
AFN 的
NSURLSessionDataTask会retain其回调 block,若 block 持有self,且请求未取消,self会一直被持有;解决:页面销毁时调用
[task cancel];。 -
缓存未清理 :
全局缓存(如
NSCache、单例中的NSDictionary)存储大量对象,且未设置过期策略,导致对象无法释放;解决:设置缓存上限(如
cache.countLimit = 100;),或在内存警告时清理缓存。 -
子线程未退出 :
子线程中开启 RunLoop 且未手动停止(如
CFRunLoopRun();),导致子线程一直存活,持有其内部的对象;解决:调用
CFRunLoopStop(CFRunLoopGetCurrent());停止 RunLoop。
三、NSNotification 机制
NSNotificationCenter(通知中心)是 iOS 中跨模块通信的核心机制,基于 "发布 - 订阅" 模式实现。
1. 实现原理(结构设计、存储关系)
NSNotificationCenter的核心是通知表,存储 "通知名 - 观察者 - 处理方法" 的映射关系,结构设计如下:
(1)核心结构
- 通知表(_notificationTable) :
NSNotificationCenter内部维护一个哈希表,key 为**通知名(NSString ) * ,value 为NSMapTable(存储该通知名对应的所有观察者); - 观察者表(NSMapTable) :key 为观察者(id) ,value 为
NSMutableArray(存储该观察者订阅该通知的所有 "处理条目"); - 处理条目(_NotificationObserver) :每个条目包含
selector(处理方法)、object(通知发送者过滤条件)、queue(指定线程处理通知)、context(上下文)。
(2)name & observer & SEL 的关系
- 多对多关系:一个通知名(name)可被多个观察者(observer)订阅,一个观察者可订阅多个通知名;
- 过滤逻辑 :订阅时指定
object,则仅接收该object发送的通知;若object为nil,则接收所有发送者的该通知; - 处理逻辑 :当通知被发布时,
NSNotificationCenter根据通知名找到所有观察者,遍历处理条目,若object匹配,调用objc_msgSend(observer, selector, notification)。
2. 通知的发送是同步还是异步?
默认是同步的:
-
调用
postNotificationName:object:userInfo:时,NSNotificationCenter会在当前线程 中立即遍历所有匹配的观察者,同步调用其selector; -
若某个观察者的
selector执行耗时,会阻塞当前线程(包括主线程,导致 UI 卡顿)。
异步发送方式 :
需手动将通知发布逻辑放入异步队列,例如:
objc
objectivec
// 在子线程异步发布通知
dispatch_async(dispatch_get_global_queue(0, 0), ^{
[[NSNotificationCenter defaultCenter] postNotificationName:@"TestNotification" object:nil];
});
// 或在主线程异步处理通知(观察者侧)
[[NSNotificationCenter defaultCenter] addObserverForName:@"TestNotification" object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification * _Nonnull note) {
// 主线程异步处理
}];
3. NSNotificationCenter 接收与发送是否在同一线程?如何异步发送?
(1)线程一致性
- 默认情况 :接收通知的线程与发送通知的线程完全一致 ;
示例:在子线程 A 调用postNotification,则所有观察者的selector会在子线程 A 执行;在主线程调用,则在主线程执行。 - 例外情况 :若订阅时指定了
queue(如addObserverForName:object:queue:usingBlock:),则通知会在指定的queue对应的线程执行;
示例:指定queue:[NSOperationQueue mainQueue],则无论通知在哪个线程发布,都会在主线程执行 block。
(2)异步发送通知的两种方式
-
发布侧异步 :将
postNotification放入异步队列,让发布操作不阻塞当前线程;objc
lessdispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ [[NSNotificationCenter defaultCenter] postNotificationName:@"AsyncPost" object:nil]; }); -
接收侧异步:订阅时指定异步队列,让处理逻辑在后台线程执行;
objc
swift// 接收通知的block在全局队列执行(异步) [[NSNotificationCenter defaultCenter] addObserverForName:@"AsyncReceive" object:nil queue:[[NSOperationQueue alloc] init] usingBlock:^(NSNotification * _Nonnull note) { // 耗时处理(如解析数据) }];
4. NSNotificationQueue 是异步还是同步?在哪个线程响应?
NSNotificationQueue(通知队列)是对NSNotificationCenter的扩展,核心作用是延迟发送通知 和合并重复通知,其发送方式和线程规则如下:
(1)同步 / 异步特性
-
默认是异步的 :
NSNotificationQueue不会立即发送通知,而是将通知加入队列,在RunLoop 的特定模式下批量发送; -
支持三种发送模式(
NSPostingStyle):NSPostWhenIdle:RunLoop 空闲时发送(如无事件处理时);NSPostASAP:RunLoop 下一次循环时发送(尽快发送,但不阻塞当前 RunLoop);NSPostNow:立即发送(同步,等价于NSNotificationCenter的直接发布)。
(2)响应线程
- 与发布通知的线程一致 :
NSNotificationQueue的通知最终由NSNotificationCenter发送,因此响应线程与NSNotificationQueue所在的线程一致; - 示例:在主线程创建
NSNotificationQueue并添加通知,则通知会在主线程的 RunLoop 空闲时发送,观察者在主线程响应;在子线程创建,则在子线程响应。
5. NSNotificationQueue 和 RunLoop 的关系
NSNotificationQueue完全依赖RunLoop实现延迟发送,核心交互如下:
- 通知入队 :调用
enqueueNotification:postingStyle:coalesceMask:forModes:时,NSNotificationQueue将通知存储在内部队列,并注册一个RunLoop 观察者(CFRunLoopObserverRef) ; - RunLoop 触发 :当 RunLoop 进入指定的模式(如
NSDefaultRunLoopMode)且满足发送条件(如NSPostWhenIdle对应 RunLoop 空闲)时,RunLoop 观察者触发回调; - 批量发送 :
NSNotificationQueue从队列中取出所有符合条件的通知,调用NSNotificationCenter的postNotification批量发送; - 合并通知 :若设置了
coalesceMask(如NSNotificationCoalescingOnName),则