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
),则