- 内存管理基础概念
1.1 引用计数(Reference Counting)
Objective-C 使用引用计数(retain count)来管理对象生命周期:
- 每个对象内部维护一个引用计数器
- 当有持有者持有该对象时,引用计数 +1
- 当持有者放弃持有时,引用计数 -1
- 当引用计数降为 0 时,系统自动调用
dealloc,释放内存
对象A 引用计数 = 2
├── 变量 x 持有(retain)
└── 数组 arr 持有(retain)
变量 x = nil → 引用计数 = 1
arr 释放 → 引用计数 = 0 → dealloc 被调用 → 内存释放
1.2 堆 vs 栈
| 区域 | 存放内容 | 管理方式 |
|---|---|---|
| 栈(Stack) | 局部变量、基本类型(int/float/struct)、指针本身 | 自动,函数返回即释放 |
| 堆(Heap) | OC 对象(NSObject 子类) | 需要引用计数管理 |
重点:OC 对象总在堆上,指针在栈上。
NSObject *obj中,obj变量在栈上,它指向的对象在堆上。
1.3 对象的生命周期
alloc/new/copy/mutableCopy → 创建对象 → retain count = 1
retain → 引用计数 +1
release → 引用计数 -1
autorelease → 延迟 release(在 pool drain 时执行)
retain count == 0 → dealloc → 内存回收
2. MRC(手动引用计数)
MRC(Manual Reference Counting)是 ARC 出现之前 iOS 开发使用的内存管理模式,由程序员手动调用 retain / release / autorelease。
2.1 核心方法
| 方法 | 作用 | retain count 变化 |
|---|---|---|
alloc / new | 创建对象 | +1(初始为1) |
copy / mutableCopy | 复制对象 | +1(新对象初始为1) |
retain | 持有对象 | +1 |
release | 放弃持有 | -1 |
autorelease | 延迟 release | pool drain 时 -1 |
retainCount | 查询当前引用计数 | — |
dealloc | 引用计数为0时系统调用 | — |
2.2 MRC 黄金法则
谁创建,谁释放。谁持有,谁释放。
NSObject *obj = [[NSObject alloc] init]; // retain count = 1
[obj retain]; // retain count = 2
[obj release]; // retain count = 1
[obj release]; // retain count = 0 → dealloc
2.3 MRC 下的属性 setter 实现
// Person.h
@interface Person : NSObject {
NSString *_name;
}
@property (nonatomic, retain) NSString *name;
@end
// Person.m
@implementation Person
// MRC retain 属性的标准 setter
- (void)setName:(NSString *)name {
if (_name != name) { // 防止自赋值:如果 name 就是 _name,
// 先 release 会导致对象被销毁
[_name release]; // 释放旧值
_name = [name retain]; // 持有新值
}
}
- (NSString *)name {
return _name;
}
// dealloc:必须释放所有持有的对象
- (void)dealloc {
[_name release];
_name = nil;
[super dealloc]; // MRC 必须调用!编译器不会帮你加
}
@end
自赋值问题详解
// 假设 _name 和 name 指向同一个对象
// ❌ 错误写法1:没有判断自赋值
- (void)setName:(NSString *)name {
[_name release]; // 如果 name == _name,对象被销毁!
_name = [name retain]; // 访问已销毁对象 → 崩溃或数据错误
}
// ❌ 错误写法2:顺序颠倒
- (void)setName:(NSString *)name {
_name = [name retain]; // 先 retain 再 release 没问题(自赋值场景)
[_name release]; // 但这里 _name 已经是新值了,release 的是新值!
}
// ✅ 正确写法:先判断,再 release 旧,再 retain 新
- (void)setName:(NSString *)name {
if (_name != name) {
[_name release];
_name = [name retain];
}
}
2.4 copy 属性
@property (nonatomic, copy) NSString *title;
- (void)setTitle:(NSString *)title {
if (_title != title) {
[_title release];
_title = [title copy]; // copy 而非 retain,得到不可变副本
}
}
为什么 NSString 属性用 copy 而不是 retain?
调用者可能传入
NSMutableString。如果用 retain,外部修改 mutableString 会悄悄改变你的属性值,违反封装。用 copy 得到一个不可变副本,安全。
2.5 MRC 常见错误
过度释放(Over-release)
NSString *s = [[NSString alloc] initWithString:@"hello"]; // retain count = 1
[s release]; // = 0 → dealloc
[s release]; // ❌ 野指针访问,EXC_BAD_ACCESS 崩溃
内存泄漏(Leak)
NSString *s = [[NSString alloc] initWithString:@"hello"]; // retain count = 1
// 函数结束,忘记 release
// s 指针销毁,但对象 retain count 仍为 1,永远泄漏
悬空指针(Dangling Pointer)
NSString *a = [[NSString alloc] initWithString:@"hello"];
NSString *b = a; // b 没有 retain,只是复制了指针值
[a release]; // retain count = 0,对象被销毁
NSLog(@"%@", b); // ❌ b 是悬空指针,崩溃!
3. ARC(自动引用计数)
ARC(Automatic Reference Counting)由 Clang 编译器在编译期自动插入 retain/release/autorelease 调用,运行时行为与 MRC 完全一致。
3.1 ARC 的本质
ARC 不是 GC(垃圾回收) ,区别如下:
| 特性 | ARC | GC |
|---|---|---|
| 工作时机 | 编译期插入代码 | 运行时扫描 |
| 回收时机 | 引用计数为0立即回收 | GC 决定,不确定 |
| 性能影响 | 几乎无额外开销 | Stop-The-World 暂停 |
| 循环引用 | 需要 weak 手动解决 | GC 可处理 |
// 你写的 ARC 代码
- (void)example {
NSObject *obj = [[NSObject alloc] init];
NSLog(@"%@", obj);
}
// 编译器等价生成的 MRC 代码(伪代码)
- (void)example {
NSObject *obj = [[NSObject alloc] init]; // retain count = 1
NSLog(@"%@", obj);
[obj release]; // 编译器插入:函数结束前自动 release → count = 0 → dealloc
}
3.2 ARC 规则(面试必背)
- 禁止手动调用:
retain/release/autorelease/retainCount/[super dealloc] - 禁止使用:
NSAllocateObject/NSDeallocateObject - C struct 中不能直接放 OC 对象指针(用
__unsafe_unretained或改用 OC 容器) - *id 与 void 强转**需要
__bridge系列修饰符 - dealloc 可以重写,但不能调用
[super dealloc](编译器自动处理)
3.3 ARC 下的 dealloc
- (void)dealloc {
// ✅ 可以做:清理 C 资源、移除通知、停止 timer
[[NSNotificationCenter defaultCenter] removeObserver:self];
[_timer invalidate];
free(_cBuffer);
// ❌ 不能调用:[super dealloc](ARC 编译器自动插入)
// ❌ 不能调用:[_name release](ARC 自动管理 OC 对象)
}
3.4 ARC 与 MRC 混编
在同一工程中,可以对某些文件单独禁用 ARC:
- Xcode → Build Phases → Compile Sources
- 目标文件添加
-fno-objc-arc:该文件使用 MRC - 目标文件添加
-fobjc-arc:强制 ARC(用于第三方 MRC 库)
4. 引用类型
4.1 strong(强引用)
@property (nonatomic, strong) NSObject *obj;
// 等价于 MRC 的 retain
// 持有对象,引用计数 +1
// 只要有 strong 引用,对象不会被释放
// 默认修饰符(OC 对象不写修饰符时等价于 strong)
4.2 weak(弱引用)
@property (nonatomic, weak) id<SomeDelegate> delegate;
// 不持有对象,引用计数不变
// 对象释放后,weak 指针自动置 nil(Zeroing Weak Reference)
// 常用于:delegate、父对象引用、避免循环引用
weak 的实现原理:
Runtime 维护全局的 weak_table_t(哈希表),key = 对象地址,value = 指向该对象的所有 weak 指针集合。
对象 A 被释放(retain count = 0)
↓ 进入 dealloc 流程
↓ objc_object::clearDeallocating()
↓ 查询 weak_table,找到所有指向 A 的 weak 指针
↓ 将它们全部置为 nil
↓ 从 weak_table 中移除记录
↓ 内存释放
weak 的性能开销:
- 注册 weak 引用时:哈希表插入,O(1) 均摊
- 读取 weak 时:需要加锁(防止多线程并发),有一定开销
- 对象销毁时:遍历并置 nil,与 weak 引用数量成正比
4.3 unsafe_unretained
@property (nonatomic, unsafe_unretained) id delegate;
// 不持有对象,引用计数不变
// 对象释放后,指针【不会】自动置 nil → 悬空指针 → 访问崩溃
// 使用场景极少,仅在需要极致性能且能保证对象生命周期时使用
weak vs unsafe_unretained:
都是弱引用,区别在于对象释放后:
weak:指针自动置 nil,安全unsafe_unretained:指针变悬空指针,不安全
4.4 assign
@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGFloat price;
@property (nonatomic, assign) BOOL isLoading;
// 用于基本数据类型(int/float/BOOL/CGRect/CGSize 等)
// 对 OC 对象使用 assign = unsafe_unretained,对象释放后指针不置 nil
// ❌ 不要对 OC 对象使用 assign(除非清楚自己在做什么)
4.5 copy
@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) void (^completion)(void);
// 调用 copy 方法得到副本并持有副本(引用计数 +1)
// NSString:防止传入 NSMutableString 被外部修改
// Block:必须 copy,Block 默认在栈上,copy 后移到堆上
Block 为什么必须 copy?
// Block 有三种存储类型:
// __NSGlobalBlock__:无捕获外部变量,存在全局段
// __NSStackBlock__:捕获了外部变量,在 MRC 下默认在栈上
// __NSMallocBlock__:copy 后在堆上(ARC 下赋值给 strong/copy 属性时自动 copy)
// ⚠️ MRC 下危险
typedef void (^MyBlock)(void);
MyBlock getBlock() {
int x = 10;
return ^{ NSLog(@"%d", x); }; // 栈 Block,函数返回后栈帧销毁,Block 失效
}
// ✅ ARC 下,赋值给 strong 属性时编译器自动 copy 到堆
// 但明确写 copy 更清晰,是业界最佳实践
4.6 修饰符总结
| 修饰符 | retain count | 对象释放后指针 | 适用场景 |
|---|---|---|---|
strong | +1 | 继续持有 | 默认,拥有关系 |
weak | 不变 | 自动置 nil | delegate、避免循环引用 |
unsafe_unretained | 不变 | 悬空指针 | 极少使用 |
assign | 不变 | 基本类型无影响 / OC对象变悬空指针 | 基本数据类型 |
copy | +1(副本) | 继续持有副本 | NSString、Block |
5. 循环引用(Retain Cycle)
5.1 什么是循环引用
两个(或多个)对象互相持有,引用计数永远不为 0,内存永远无法释放。
A ──strong──> B
B ──strong──> A
A.retainCount >= 1(B 持有它)
B.retainCount >= 1(A 持有它)
→ 两者都不会被释放 → 内存泄漏
5.2 Delegate 循环引用
// ❌ ViewController strong 持有 View,View 也 strong 持有 VC
@interface MyView : UIView
@property (nonatomic, strong) id<MyViewDelegate> delegate; // ❌
@end
// ✅ delegate 用 weak 打破循环
@interface MyView : UIView
@property (nonatomic, weak) id<MyViewDelegate> delegate; // ✅
@end
5.3 Block 循环引用(最高频考点)
// ❌ 经典循环引用
@interface MyVC : UIViewController
@property (nonatomic, copy) void (^block)(void);
@end
@implementation MyVC
- (void)viewDidLoad {
self.block = ^{
NSLog(@"%@", self.title); // Block 捕获 self(strong 引用)
// self ──strong──> block(self.block 属性)
// block ──strong──> self(捕获变量)
// 循环引用!dealloc 永远不调用
};
}
@end
解法1:__weak(推荐)
__weak typeof(self) weakSelf = self;
self.block = ^{
// weakSelf 不持有 self,不增加引用计数
NSLog(@"%@", weakSelf.title);
};
解法2:weakSelf + strongSelf(推荐,多行访问时)
__weak typeof(self) weakSelf = self;
self.block = ^{
// 为什么要 strongSelf?
// weakSelf 是 weak 引用,可能在 block 执行中途被置为 nil
// 例如:第一行访问时非 nil,线程切换后 self 释放,第二行访问时变 nil
// strongSelf 在 block 执行期间强持有 self,确保整个 block 执行完
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // self 已经释放,提前退出
NSLog(@"%@", strongSelf.title);
[strongSelf doSomething];
}; // strongSelf 出作用域,release,block 执行完后 self 可以正常释放
解法3:__block(打破循环,MRC 时代遗留)
// ARC 下 __block 变量本身是 strong 的,不推荐用这个方式
__block MyVC *blockSelf = self; // 注意:这里仍然是强引用!
self.block = ^{
NSLog(@"%@", blockSelf.title);
blockSelf = nil; // 手动赋 nil 打破循环,但只在 block 执行后才能释放
};
// 问题:如果 block 从未执行,循环引用依然存在
5.4 NSTimer 循环引用
// ❌ 循环引用
@interface MyVC : UIViewController
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation MyVC
- (void)viewDidLoad {
// NSTimer 内部 strong retain target(self)
// self ──strong──> timer(self.timer 属性)
// timer ──strong──> self(target 参数)
// → dealloc 永远不调用!
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
}
@end
解法1:在合适时机主动 invalidate(最简单)
// viewWillDisappear 或 viewDidDisappear 中
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate]; // timer 释放对 self 的 retain
self.timer = nil;
}
解法2:iOS 10+ Block API + weakSelf
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer *t) {
[weakSelf tick]; // weakSelf 不持有 self
}];
// timer ──strong──> block,block ──weak──> self
// self 可以正常释放,dealloc 里 invalidate timer 即可
解法3:NSProxy 中间代理
// WeakProxy:持有一个 weak 引用指向真正的 target
// timer ──strong──> proxy ──weak──> self
// self 释放后,proxy 收到消息,直接让 timer invalidate
@interface WeakProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:[WeakProxy proxyWithTarget:self]
selector:@selector(tick)
userInfo:nil
repeats:YES];
5.5 检测循环引用的工具
| 工具 | 使用场景 | 特点 |
|---|---|---|
| Instruments → Leaks | 运行时 | 自动检测泄漏,有 backtrace |
| Instruments → Allocations | 运行时 | 对比进入/退出页面前后对象数量 |
| Xcode Memory Graph | 调试时 | 可视化引用关系图,直观 |
| FBRetainCycleDetector | 测试/运行时 | Meta 开源,代码集成,自动报告 |
6. autorelease 与 AutoreleasePool
6.1 autorelease 是什么
autorelease 不是立即释放,而是把对象注册到当前 autorelease pool,等 pool drain 时统一 release。
// MRC
NSString *s = [[[NSString alloc] initWithString:@"hello"] autorelease];
// s 注册到 pool,函数结束不崩溃,等 pool drain 时才 release
// 常见返回 autorelease 对象的方法(非 alloc/new/copy/mutableCopy 开头)
NSString *s2 = [NSString stringWithFormat:@"hello %d", 1]; // autorelease
NSArray *arr = [NSArray arrayWithObject:@"a"]; // autorelease
6.2 命名约定(持有规则)
| 方法名前缀 | 返回对象持有状态 | 调用者责任 |
|---|---|---|
alloc / new / copy / mutableCopy | retain count = 1 | 调用者负责 release |
| 其他所有方法 | autorelease 对象 | 无需 release(pool 统一处理) |
6.3 AutoreleasePool 工作机制
@autoreleasepool {
// 进入:向当前线程的 pool 栈 push 一个新 pool
NSString *s = [NSString stringWithFormat:@"hello %d", 1];
// s 是 autorelease 对象,被注册到当前 pool
} // 出块:drain pool,对所有注册的对象发送 release
底层结构:
- AutoreleasePool 是一个双向链表,每个节点是
AutoreleasePoolPage(4096 bytes) - 每个 page 存储一批对象指针
push在 page 上标记一个POOL_BOUNDARY(边界标记)pop从栈顶 release 所有对象,直到遇到边界
6.4 RunLoop 与 AutoreleasePool 的关系
主线程的 RunLoop 在每次循环:
- kCFRunLoopEntry(进入 RunLoop):创建 autorelease pool
- kCFRunLoopBeforeWaiting(即将休眠):drain 旧 pool,创建新 pool
- kCFRunLoopExit(退出 RunLoop):drain pool
这是为什么大多数代码不手动写 @autoreleasepool 也能正常运行的原因。
6.5 需要手动添加 @autoreleasepool 的场景
场景1:子线程
dispatch_async(dispatch_get_global_queue(0, 0), ^{
// ⚠️ 子线程没有默认 RunLoop,也没有 autorelease pool
// autorelease 对象会泄漏(直到线程结束才由系统清理)
@autoreleasepool {
for (int i = 0; i < 10000; i++) {
NSString *s = [NSString stringWithFormat:@"%d", i];
// 不加 pool:所有对象堆积到循环结束
// 加 pool:每次 pool 出块时 drain,内存峰值稳定
}
}
});
场景2:大量临时对象的循环
// 不加 pool:内存持续增长到循环结束
for (int i = 0; i < 100000; i++) {
@autoreleasepool {
UIImage *img = [UIImage imageNamed:[NSString stringWithFormat:@"img_%d", i]];
// 处理 img
} // 每次迭代后立即 drain,内存峰值极低
}
7. ARC 底层原理
7.1 编译器如何插入 retain/release
Clang 基于所有权语义静态分析代码流,在以下时机插入:
- 对象赋值给 strong 变量:插入
objc_retain - strong 变量超出作用域或被覆盖:插入
objc_release - 函数参数、返回值传递:插入必要的 retain/release
- 跨作用域的对象:通过
objc_retainAutoreleasedReturnValue等函数优化
// 你写的
- (void)foo {
NSObject *a = [[NSObject alloc] init]; // +1(alloc 自带)
NSObject *b = a; // b 是 strong 变量
a = nil;
[b doSomething];
}
// 编译器生成(伪代码)
- (void)foo {
NSObject *a = objc_msgSend(NSObject, @selector(alloc));
a = objc_msgSend(a, @selector(init)); // count = 1
NSObject *b = objc_retain(a); // b = a,+1,count = 2
objc_storeStrong(&a, nil); // a = nil,release 旧值,count = 1
objc_msgSend(b, @selector(doSomething));
objc_storeStrong(&b, nil); // b 出作用域,release,count = 0 → dealloc
}
7.2 ARC 使用的关键 C 函数
id objc_retain(id obj); // +1
void objc_release(id obj); // -1
id objc_autorelease(id obj); // 注册到 pool
// 优化版:跨函数传递时避免多余的 retain/release
id objc_retainAutoreleasedReturnValue(id obj);
id objc_autoreleaseReturnValue(id obj);
// weak 相关
id objc_initWeak(id *location, id obj);
id objc_loadWeakRetained(id *location);
void objc_destroyWeak(id *location);
void objc_storeWeak(id *location, id obj);
7.3 weak 变量的底层实现
// __weak NSObject *weakObj = obj;
// 展开后:
// 1. 赋值时
objc_initWeak(&weakObj, obj);
// → 在全局 weak_table 中注册 {obj地址 → &weakObj}
// 2. 读取 weakObj 时
NSObject *tmp = objc_loadWeakRetained(&weakObj);
// → 加锁,检查对象是否存在,存在则 retain,返回
// 使用 tmp...
objc_release(tmp);
// 3. weakObj 出作用域时
objc_destroyWeak(&weakObj);
// → 从 weak_table 移除注册
// 4. 对象 dealloc 时
// → clearDeallocating → 找到所有 weak 指针 → 全部置 nil
7.4 TaggedPointer(小对象优化)
对于 NSNumber、短字符串(≤7字节)、NSDate 等小对象,苹果使用 Tagged Pointer 技术:
- 指针的最低位(或最高位,ARM64)置 1 作为标识
- 值直接编码在指针本身,不在堆上分配内存
- retain/release 对 Tagged Pointer 是空操作(不需要引用计数)
- 内存效率极高,访问更快
NSNumber *n1 = @42; // Tagged Pointer,不在堆上
NSNumber *n2 = @(1e100); // 数值太大,是真正的堆对象
// 判断是否是 Tagged Pointer
BOOL isTagged = ((uintptr_t)n1 & 1) == 1; // 简化版,实际更复杂
7.5 SideTable 与引用计数存储
对象的引用计数不只存在一个地方:
isa 优化(Non-pointer isa): 在 64 位系统上,isa 指针不只是指向 Class,还内嵌了引用计数(低位部分)和其他标志位。
isa(64位)
bits[0] : nonpointer(=1 表示非原始指针,内嵌信息)
bits[1] : has_assoc(有关联对象)
bits[2] : has_cxx_dtor(有 C++ 析构或 ObjC dealloc)
bits[3-35] : class pointer(类指针)
bits[36-41]: extra_rc(引用计数-1,最多存 64-1 = 63)
bits[42] : has_sidetable_rc(引用计数溢出时,存入 SideTable)
...
当 extra_rc 溢出时,引用计数会溢出到全局的 SideTable(哈希表,key=对象地址,value=溢出引用计数)。
8. 常见内存问题与排查
8.1 内存泄漏(Memory Leak)
现象: 内存持续增长,进入/退出某个界面内存不回落
常见原因:
- 循环引用(最常见)
- delegate 使用 strong
- Block 捕获 self 未使用 weakSelf
- NSTimer 未 invalidate
- C 资源(malloc/CoreFoundation/CF对象)未手动释放
- 通知未 removeObserver(iOS 9 以下)
排查方法:
Xcode → Debug → Memory Graph Debugger
→ 查看是否有不应存在的对象
→ 点击对象看引用链,找到循环
8.2 野指针(Dangling Pointer / EXC_BAD_ACCESS)
现象: EXC_BAD_ACCESS,访问已释放内存
常见原因:
- MRC 下 over-release
- unsafe_unretained 指针指向已释放对象
- 多线程同时 release 同一对象
- C 指针使用后未置 NULL
排查方法:
Xcode Scheme → Diagnostics → Enable Zombie Objects
→ 对象被释放后变成 NSZombie,再次访问会输出类名
→ "message sent to deallocated instance 0x..."
8.3 内存峰值过高
现象: 短时间内内存暴涨,可能触发 Memory Warning 或被系统杀进程
常见原因:
- 大量临时 autorelease 对象未及时 drain
- 大图未及时释放
- 缓存无限增长
解决方案:
- 大循环中嵌套
@autoreleasepool - 使用
NSCache替代NSMutableDictionary做缓存(内存压力时自动清理) - 图片使用
imageWithContentsOfFile:替代imageNamed:(不缓存)
8.4 多线程内存问题
// ❌ 线程不安全的引用计数
// 两个线程同时 release 同一对象可能导致 double-free
// ✅ ARC 下使用 atomic(默认是非原子的)
@property (atomic, strong) NSObject *obj;
// ✅ 或者用 dispatch_queue 保护
@property (nonatomic, strong) dispatch_queue_t queue;
9. 高频面试题精析
Q1:ARC 和 MRC 的区别?
答:
- MRC 需要程序员手动调用 retain/release/autorelease 管理内存
- ARC 由 Clang 编译器在编译期自动插入这些调用,运行时机制完全一致
- ARC 不是 GC,底层依然是引用计数,不会产生 Stop-The-World
- ARC 不能解决循环引用,需要 weak 手动打破
Q2:weak 和 assign 的区别?
答:
- 对 OC 对象而言:weak 是弱引用,对象释放后自动置 nil;assign 不增加引用计数,对象释放后变悬空指针
- assign 通常用于基本数据类型(int/BOOL/CGFloat 等),这些类型不是对象,没有引用计数概念
- ❌ 不要用 assign 修饰 OC 对象,用 weak 代替
Q3:为什么 Block 属性用 copy?
答:
- Block 默认可能在栈上(捕获了外部变量的情况下),函数返回后栈帧销毁,Block 失效
- copy 后 Block 移到堆上,生命周期由引用计数管理,可以安全传递和存储
- ARC 下赋值给 strong 属性时编译器会自动 copy,但显式写 copy 表达意图更清晰
- Block 属性永远写 copy 是最佳实践
Q4:为什么 NSString 属性用 copy 而不是 strong?
答:
调用者可能传入 NSMutableString(它是 NSString 的子类):
- 用 strong/retain:只是持有同一个 mutableString,外部修改它会影响你的属性值
- 用 copy:得到一个不可变副本(NSString),外部修改原 mutableString 不影响你
注意:如果传入的本来就是 NSString,copy 操作是 O(1) 的(不可变对象 copy 返回 self),没有性能损失。
Q5:循环引用是什么?如何解决?
答:
两个或多个对象互相强引用,引用计数永远不为 0,无法释放,造成内存泄漏。
常见场景与解法:
- delegate:delegate 属性改为 weak
- Block 捕获 self:用
__weak typeof(self) weakSelf = self,多行访问再加__strong typeof(weakSelf) strongSelf = weakSelf - NSTimer:在合适时机 invalidate,或用 iOS 10+ block API + weakSelf,或用 NSProxy 中间层
Q6:autoreleasepool 什么时候用?
答:
- 子线程中:子线程没有默认 RunLoop,需要手动创建 autoreleasepool
- 大量临时对象的循环中:防止内存峰值过高,在循环体内嵌套 @autoreleasepool 让临时对象及时释放
- 命令行工具 / 单元测试:没有 RunLoop,需要手动管理
Q7:dealloc 里可以做什么?
答:
可以做的:
- 移除通知观察者(
removeObserver:self) - 停止 NSTimer(
[_timer invalidate]) - 释放 C 资源(
free()/fclose()等) - KVO 移除(
removeObserver:forKeyPath:)
不能做的(ARC):
[super dealloc](编译器自动处理)[_obj release](ARC 自动处理 OC 对象)
不要做的:
- 发送异步通知或执行耗时操作(dealloc 的线程不确定)
- 调用 self 的其他方法(可能访问已释放成员,危险)
Q8:__weak 修饰的变量为何能在对象释放后自动置 nil?
答:
Runtime 维护一张全局的 weak_table(哈希表):
- key:对象的内存地址
- value:所有指向该对象的 weak 变量地址集合
当对象引用计数降为 0,进入 dealloc 流程时,Runtime 调用 clearDeallocating:
- 在 weak_table 中找到该对象所有 weak 指针
- 将它们全部赋值为 nil
- 从 weak_table 中移除该对象的记录
所以每次读取 weak 变量时,Runtime 会加锁原子地读取,保证线程安全。
Q9:什么是 TaggedPointer?
答:
一种小对象优化技术。对于 NSNumber、短 NSString、NSDate 等数值较小的对象,苹果将值直接编码在指针中,不在堆上分配内存:
- 指针最高位(ARM64)= 1 标识这是 Tagged Pointer
- 其余位编码类型 + 值
- retain/release 是空操作
- 内存效率极高,无堆分配,无引用计数开销
Q10:如何在 ARC 中使用 CoreFoundation 对象(CFTypeRef)?
答:
CF 对象不受 ARC 管理,需要用 __bridge 系列修饰符在 OC 和 CF 之间转换:
// __bridge:不转移所有权,双方各管各的
NSString *str = (__bridge NSString *)cfStr; // CF 对象转 OC,CF 还负责 release
// __bridge_retained:OC → CF,OC 放弃所有权,CF 负责 CFRelease
CFStringRef cfStr = (__bridge_retained CFStringRef)ocStr;
// 使用后必须 CFRelease(cfStr)
// __bridge_transfer:CF → OC,CF 放弃所有权,ARC 接管
NSString *str = (__bridge_transfer NSString *)cfStr;
// 不需要 CFRelease,ARC 会 release