iOS 内存管理完整补充知识
从对象到类、从结构体到元类、从 C++ 到内存分布区、到手机硬件内存的完整知识线
目录
- [ARC 自动引用计数详细机制](#ARC 自动引用计数详细机制 "#1-arc-%E8%87%AA%E5%8A%A8%E5%BC%95%E7%94%A8%E8%AE%A1%E6%95%B0%E8%AF%A6%E7%BB%86%E6%9C%BA%E5%88%B6")
- 内存对齐与对象大小
- [Tagged Pointer 技术](#Tagged Pointer 技术 "#3-tagged-pointer-%E6%8A%80%E6%9C%AF")
- [Mach-O 文件结构与内存映射](#Mach-O 文件结构与内存映射 "#4-mach-o-%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84%E4%B8%8E%E5%86%85%E5%AD%98%E6%98%A0%E5%B0%84")
- [AutoreleasePool 与 RunLoop 关系](#AutoreleasePool 与 RunLoop 关系 "#5-autoreleasepool-%E4%B8%8E-runloop-%E5%85%B3%E7%B3%BB")
- 堆分配策略与内存碎片
- 栈基础与栈溢出
- 类/元类查找链与方法缓存
- [OC vs C++ 内存模型差异](#OC vs C++ 内存模型差异 "#9-oc-vs-c-%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B%E5%B7%AE%E5%BC%82")
- 虚拟内存与物理内存映射
- [Weak 表实现与性能](#Weak 表实现与性能 "#11-weak-%E8%A1%A8%E5%AE%9E%E7%8E%B0%E4%B8%8E%E6%80%A7%E8%83%BD")
0. 引用计数基础概念(小白必读)
0.1 什么是引用计数?
引用计数 = 记录"有多少个地方在使用这个对象"的数字
生活化比喻:图书馆借书
想象一下图书馆的书:
diff
一本书(对象):
- 被借出时:借书人数 = 1
- 又有人借:借书人数 = 2
- 有人还书:借书人数 = 1
- 所有人还完:借书人数 = 0 → 书可以放回仓库(对象被释放)
OC 对象也是一样:
- 对象被创建:引用计数 = 1
- 有人强引用它:引用计数 +1
- 有人不再引用:引用计数 -1
- 引用计数 = 0:对象被释放(内存回收)
0.2 "引用计数加1"是什么意思?
"引用计数加1" = 又多了一个地方在使用这个对象
代码示例
swift
// 步骤 1:创建对象
NSObject *obj = [[NSObject alloc] init];
// 此时:obj 指向的对象,引用计数 = 1
// 意思:有 1 个地方在使用这个对象(就是 obj 这个变量)
// 步骤 2:另一个变量也指向这个对象
NSObject *obj2 = obj; // 强引用赋值
// 此时:obj 指向的对象,引用计数 = 2
// 意思:有 2 个地方在使用这个对象(obj 和 obj2)
// 步骤 3:obj 不再指向这个对象
obj = nil;
// 此时:obj 指向的对象,引用计数 = 1
// 意思:还有 1 个地方在使用(obj2 还在用)
// 步骤 4:obj2 也不再指向
obj2 = nil;
// 此时:引用计数 = 0
// 意思:没有地方在使用这个对象了 → 对象被释放!
0.3 "self 引用计数加1"具体指什么?
"self 引用计数加1" = 又多了一个地方在强引用 self 这个对象
示例 1:普通赋值
less
@interface ViewController : UIViewController
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self 的引用计数 = 1(假设只有系统在引用它)
// 创建一个强引用
ViewController *anotherRef = self; // 强引用赋值
// 此时:self 的引用计数 = 2
// 意思:有 2 个地方在强引用 self(系统 + anotherRef)
// anotherRef 不再引用
anotherRef = nil;
// 此时:self 的引用计数 = 1(恢复)
}
@end
示例 2:Block 捕获 self(关键!)
objectivec
- (void)viewDidLoad {
[super viewDidLoad];
// self 的引用计数 = 1
// ❌ 情况 A:block 直接捕获 self
self.block = ^{
[self doSomething]; // block 强引用 self
};
// 此时:self 的引用计数 = 2
// 原因:self 强引用 block,block 强引用 self
// 形成循环:self → block → self(循环引用!)
// ✅ 情况 B:block 捕获 weakSelf
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // block 弱引用 self(不增加引用计数)
};
// 此时:self 的引用计数 = 1(没有增加!)
// 原因:weakSelf 是弱引用,不会让引用计数 +1
}
示例 3:Weak-Strong Dance 中的引用计数变化
objectivec
- (void)viewDidLoad {
[super viewDidLoad];
// 初始状态:self 引用计数 = 1
__weak typeof(self) weakSelf = self;
// 此时:self 引用计数 = 1(weakSelf 不增加引用计数)
self.block = ^{
// block 被创建,捕获了 weakSelf(弱引用)
// 此时:self 引用计数 = 1(仍然没有增加)
// block 执行时:
__strong typeof(weakSelf) strongSelf = weakSelf;
// 此时:self 引用计数 = 2(strongSelf 强引用,+1)
// 意思:又多了一个地方在强引用 self(就是 strongSelf)
[strongSelf doSomething];
// block 执行完,strongSelf 作用域结束
// 此时:self 引用计数 = 1(strongSelf 释放,-1)
// 意思:strongSelf 不再引用 self,引用计数恢复
};
// 最终:self 引用计数 = 1(block 只弱引用 self,不增加引用计数)
}
0.4 引用计数的"加1"和"减1"是怎么实现的?
底层实现(简化理解)
objectivec
// 伪代码:引用计数的实现
struct NSObject {
int retainCount; // 引用计数(实际可能不在对象里,在 side table)
};
// retain(加1)
- (id)retain {
retainCount++; // 引用计数 +1
return self;
}
// release(减1)
- (void)release {
retainCount--; // 引用计数 -1
if (retainCount == 0) {
[self dealloc]; // 引用计数为 0,释放对象
}
}
ARC 自动插入 retain/release
ini
// 你写的代码
NSObject *obj = [[NSObject alloc] init];
NSObject *obj2 = obj;
// 编译器实际生成的代码(伪代码)
NSObject *obj = [[NSObject alloc] init]; // retainCount = 1
NSObject *obj2 = [obj retain]; // retainCount = 2(自动插入 retain)
// ... 使用 ...
[obj release]; // retainCount = 1(自动插入 release)
[obj2 release]; // retainCount = 0,对象释放
0.5 常见误区澄清
误区 1:指针变量本身不占引用计数
swift
NSObject *obj = [[NSObject alloc] init];
// obj 这个指针变量本身不占引用计数
// 引用计数是对象自己的属性,不是指针的属性
// 多个指针指向同一个对象
NSObject *obj1 = [[NSObject alloc] init]; // 对象引用计数 = 1
NSObject *obj2 = obj1; // 对象引用计数 = 2(不是 obj2 的引用计数)
NSObject *obj3 = obj1; // 对象引用计数 = 3(不是 obj3 的引用计数)
// 所有指针都指向同一个对象,所以这个对象的引用计数 = 3
误区 2:weak 引用不增加引用计数
objectivec
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
__weak NSObject *weakObj = obj; // 引用计数 = 1(没有增加!)
__strong NSObject *strongObj = obj; // 引用计数 = 2(增加了!)
// weak 引用不会让引用计数 +1
// 只有 strong 引用才会让引用计数 +1
误区 3:引用计数不是对象的"数量"
swift
// ❌ 错误理解:引用计数 = 对象的数量
NSObject *obj1 = [[NSObject alloc] init]; // 1 个对象,引用计数 = 1
NSObject *obj2 = [[NSObject alloc] init]; // 2 个对象,引用计数 = 1(每个对象都是 1)
// ✅ 正确理解:引用计数 = 指向这个对象的强引用数量
NSObject *obj = [[NSObject alloc] init]; // 1 个对象
NSObject *ref1 = obj; // 对象引用计数 = 2(2 个强引用指向它)
NSObject *ref2 = obj; // 对象引用计数 = 3(3 个强引用指向它)
0.6 面试一句话总结
"引用计数加1" = 又多了一个强引用指向这个对象,对象的引用计数数值 +1
关键点:
- 引用计数是对象的属性,不是指针的属性
- 只有 strong 引用才会让引用计数 +1
- weak 引用不会让引用计数 +1
- 引用计数 = 0 时,对象被释放
1. ARC 自动引用计数详细机制
1.1 ARC 在编译时做了什么?
ARC 不是运行时技术,而是编译时技术!
编译器会在编译阶段 自动插入 retain、release、autorelease 调用。
示例代码对比
MRC 时代(手动):
csharp
// MRC 代码
- (void)example {
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
[obj retain]; // 引用计数 = 2
[obj release]; // 引用计数 = 1
[obj release]; // 引用计数 = 0,对象被释放
}
ARC 时代(自动):
ini
// ARC 代码(你写的)
- (void)example {
NSObject *obj = [[NSObject alloc] init];
// 编译器自动在方法结束前插入 [obj release];
}
编译器转换后的伪代码:
csharp
// 编译器实际生成的代码
- (void)example {
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
// ... 你的代码 ...
[obj release]; // ← 编译器自动插入!
}
1.2 ARC 的 retain/release 插入规则
规则 1:赋值时自动 retain
ini
NSObject *obj1 = [[NSObject alloc] init]; // 引用计数 = 1
NSObject *obj2 = obj1; // obj2 强引用,引用计数 = 2
// 编译器自动插入:obj2 = [obj1 retain];
规则 2:变量作用域结束时自动 release
objectivec
- (void)example {
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
// ... 使用 obj ...
// 编译器在方法结束前自动插入:[obj release];
}
规则 3:属性赋值时自动管理
objectivec
@property (strong, nonatomic) NSObject *obj;
- (void)setObj:(NSObject *)obj {
if (_obj != obj) {
[_obj release]; // 编译器自动插入:释放旧值
_obj = [obj retain]; // 编译器自动插入:持有新值
}
}
1.3 什么是循环引用?(核心概念)
1.3.1 用生活例子理解"两个对象互相引用"
想象两个好朋友互相借钱:
arduino
小明 和 小红:
小明说:"我借了小红 100 元,小红必须还我,我才能还别人"
小红说:"我借了小明 100 元,小明必须还我,我才能还别人"
结果:两个人互相等待对方还钱,永远还不完!
这就是"互相引用"的问题。
在代码中:
css
对象 A 说:"我强引用了对象 B,B 必须存在,我才能存在"
对象 B 说:"我强引用了对象 A,A 必须存在,我才能存在"
结果:两个对象互相等待对方释放,永远释放不了!
这就是"循环引用"。
1.3.2 循环引用的图示
正常情况(没有循环):
css
对象 A(引用计数 = 1)
↑
│ 强引用
│
变量 a
对象 B(引用计数 = 1)
↑
│ 强引用
│
变量 b
结果:a = nil 时,A 被释放;b = nil 时,B 被释放 ✅
循环引用情况:
css
对象 A(引用计数 = 2)
↑ ↑
│ │
│ 强引用 │ 强引用(来自 B)
│ │
变量 a 对象 B(引用计数 = 2)
↑ ↑
│ │
│ 强引用 │ 强引用(来自 A)
│ │
变量 b 对象 A(引用计数 = 2)
↑
│
│(形成循环!)
│
对象 B(引用计数 = 2)
问题:
- 即使
a = nil和b = nil,A 和 B 的引用计数都还是 1(因为互相引用) - 引用计数永远不会变成 0
- 对象永远不会被释放 → 内存泄漏!
1.3.3 代码示例:两个对象互相引用
less
// 定义两个类
@interface PersonA : NSObject
@property (strong, nonatomic) PersonB *personB; // A 强引用 B
@end
@interface PersonB : NSObject
@property (strong, nonatomic) PersonA *personA; // B 强引用 A
@end
// 使用
PersonA *a = [[PersonA alloc] init]; // A 引用计数 = 1
PersonB *b = [[PersonB alloc] init]; // B 引用计数 = 1
a.personB = b; // B 引用计数 = 2(A 强引用 B)
b.personA = a; // A 引用计数 = 2(B 强引用 A)
// 此时:
// A 引用计数 = 2(变量 a + B.personA)
// B 引用计数 = 2(变量 b + A.personB)
a = nil; // A 引用计数 = 1(还有 B.personA 在引用)
b = nil; // B 引用计数 = 1(还有 A.personB 在引用)
// 问题:A 和 B 的引用计数都是 1,永远不会变成 0
// 结果:A 和 B 永远不会被释放 → 内存泄漏!
图示:
css
初始:
变量 a → PersonA(引用计数 = 1)
变量 b → PersonB(引用计数 = 1)
互相引用后:
变量 a → PersonA(引用计数 = 2)← PersonB.personA
↓ PersonA.personB
变量 b → PersonB(引用计数 = 2)← PersonA.personB
↑ PersonB.personA
│
└───────────┘(形成循环!)
a = nil, b = nil 后:
PersonA(引用计数 = 1)← PersonB.personA
↓ PersonA.personB
PersonB(引用计数 = 1)← PersonA.personB
↑ PersonB.personA
│
└───────────┘(循环还在,无法释放!)
1.3.4 如何打破循环引用?
方法:把其中一个强引用改成弱引用
less
// ✅ 正确:B 弱引用 A
@interface PersonA : NSObject
@property (strong, nonatomic) PersonB *personB; // A 强引用 B
@end
@interface PersonB : NSObject
@property (weak, nonatomic) PersonA *personA; // B 弱引用 A(关键!)
@end
// 使用
PersonA *a = [[PersonA alloc] init]; // A 引用计数 = 1
PersonB *b = [[PersonB alloc] init]; // B 引用计数 = 1
a.personB = b; // B 引用计数 = 2(A 强引用 B)
b.personA = a; // A 引用计数 = 1(B 弱引用 A,不增加引用计数)
// 此时:
// A 引用计数 = 1(只有变量 a)
// B 引用计数 = 2(变量 b + A.personB)
a = nil; // A 引用计数 = 0 → A 被释放!
// B.personA 自动变成 nil(weak 的特性)
b = nil; // B 引用计数 = 1(还有 A.personB?不对,A 已经释放了)
// 实际上,A 释放时,A.personB 也被释放
// 所以 B 引用计数 = 0 → B 被释放!
// 结果:两个对象都能正常释放 ✅
图示(打破循环后):
ini
变量 a → PersonA(引用计数 = 1)
↓ PersonA.personB(强引用)
变量 b → PersonB(引用计数 = 2)
↑ PersonB.personA(弱引用,不增加引用计数)
a = nil 后:
PersonA(引用计数 = 0)→ 被释放!
↓ PersonA.personB 也被释放
PersonB(引用计数 = 1)← 只有变量 b
↑ PersonB.personA = nil(自动置 nil)
b = nil 后:
PersonB(引用计数 = 0)→ 被释放!✅
1.3.5 循环引用的核心理解
循环引用 = 两个或多个对象互相强引用,形成闭环,导致都无法释放
关键点:
- 必须是"强引用" :weak 引用不会形成循环引用
- 必须是"互相" :A → B → A(闭环)
- 结果:引用计数永远不会变成 0,对象永远不会被释放
解决方法:
- 把循环中的至少一个强引用改成弱引用
- 或者手动断开循环(设置为 nil)
1.3.6 循环引用会导致什么?(重要!)
🚨 循环引用的后果
1. 内存泄漏(Memory Leak)
最直接的后果:对象永远不会被释放,占用内存越来越多
less
@interface ViewController : UIViewController
@property (copy, nonatomic) void (^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ❌ 循环引用
self.block = ^{
[self doSomething]; // block 强引用 self
};
// self 强引用 block,block 强引用 self → 循环引用
}
@end
// 使用场景:
ViewController *vc = [[ViewController alloc] init];
[self.navigationController pushViewController:vc animated:YES];
// 用户返回上一页
[self.navigationController popViewControllerAnimated:YES];
// 问题:
// vc 应该被释放,但因为循环引用,vc 无法释放
// 内存泄漏!vc 占用的内存永远不会回收
影响:
- 内存占用持续增长
- 长时间运行后可能导致内存不足
- 应用可能被系统杀死(OOM - Out of Memory)
2. dealloc 永远不会被调用
dealloc 方法不会被调用,清理代码不会执行
objectivec
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ❌ 循环引用
self.block = ^{
[self doSomething];
};
}
- (void)dealloc {
NSLog(@"ViewController 被释放"); // ❌ 永远不会打印!
// 清理代码不会执行
[[NSNotificationCenter defaultCenter] removeObserver:self];
[self.timer invalidate];
// 这些清理代码都不会执行!
}
@end
影响:
- 资源无法释放(通知观察者、定时器、网络请求等)
- 可能导致其他问题(通知重复接收、定时器继续运行等)
3. 通知观察者无法移除
objectivec
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 添加通知观察者
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"SomeNotification"
object:nil];
// ❌ 循环引用
self.block = ^{
[self doSomething];
};
}
- (void)dealloc {
// ❌ 永远不会执行!
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
// 问题:
// ViewController 无法释放
// 通知观察者无法移除
// 即使 ViewController 已经不在屏幕上,仍然会接收通知
// 可能导致崩溃或逻辑错误
4. 定时器无法停止
objectivec
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ❌ 循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self // timer 强引用 self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// self 强引用 timer,timer 强引用 self → 循环引用
}
- (void)dealloc {
// ❌ 永远不会执行!
[self.timer invalidate]; // 定时器无法停止
self.timer = nil;
}
// 问题:
// ViewController 无法释放
// 定时器继续运行,即使 ViewController 已经不在屏幕上
// 定时器回调可能访问已销毁的视图,导致崩溃
5. 网络请求回调可能继续执行
objectivec
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// ❌ 循环引用
[NetworkManager requestWithCompletion:^(NSData *data) {
[self handleResponse:data]; // block 强引用 self
}];
// 如果 NetworkManager 也强引用这个 block,可能形成循环引用
}
- (void)dealloc {
// ❌ 永远不会执行!
// 清理代码不会执行
}
// 问题:
// ViewController 无法释放
// 网络请求完成后,回调可能访问已销毁的视图
// 可能导致崩溃或逻辑错误
6. KVO 观察者无法移除
objectivec
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self.model addObserver:self
forKeyPath:@"value"
options:NSKeyValueObservingOptionNew
context:nil];
// ❌ 循环引用
self.block = ^{
[self doSomething];
};
}
- (void)dealloc {
// ❌ 永远不会执行!
[self.model removeObserver:self forKeyPath:@"value"];
}
// 问题:
// ViewController 无法释放
// KVO 观察者无法移除
// 如果 model 被释放,可能导致崩溃
📊 循环引用的影响总结
| 影响 | 说明 | 严重程度 |
|---|---|---|
| 内存泄漏 | 对象无法释放,内存持续增长 | ⚠️⚠️⚠️ 严重 |
| dealloc 不执行 | 清理代码不会执行 | ⚠️⚠️⚠️ 严重 |
| 通知无法移除 | 继续接收通知,可能导致崩溃 | ⚠️⚠️ 中等 |
| 定时器无法停止 | 定时器继续运行,可能访问已销毁对象 | ⚠️⚠️ 中等 |
| 网络回调继续执行 | 回调可能访问已销毁对象 | ⚠️⚠️ 中等 |
| KVO 无法移除 | 可能导致崩溃 | ⚠️⚠️ 中等 |
🔍 如何检测循环引用?
方法 1:检查 dealloc 是否被调用
objectivec
- (void)dealloc {
NSLog(@"✅ ViewController 被释放"); // 如果没打印,说明有循环引用
}
方法 2:使用 Instruments 的 Leaks 工具
- 打开 Xcode
- Product → Profile(或 Cmd + I)
- 选择 Leaks
- 运行应用,执行可能产生循环引用的操作
- 查看是否有内存泄漏
方法 3:使用 Xcode Memory Graph
- 运行应用
- 在 Debug Navigator 中点击 Memory Graph
- 查看对象是否正常释放
方法 4:使用 MLeaksFinder(第三方工具)
自动检测内存泄漏,在开发阶段就能发现问题。
✅ 如何避免循环引用?
- 使用 weak 引用:在 block、delegate、通知等场景使用 weak
- 及时断开引用:在不需要时手动设置为 nil
- 使用 weak-strong dance:在 block 中使用 weak-strong dance 模式
- 代码审查:定期检查代码,特别是 block、delegate、通知等场景
1.4 循环引用的典型场景
场景 1:self ↔ block
less
// ❌ 错误:循环引用
@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self 强引用 block
self.block = ^{
// block 强引用 self(捕获了 self)
[self doSomething]; // ← 形成循环引用!
};
}
@end
// ✅ 正确:使用 weak-strong dance
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf doSomething];
};
}
🔍 Weak-Strong Dance 详细解释:
第一步:__weak typeof(self) weakSelf = self;
objectivec
__weak typeof(self) weakSelf = self;
作用:
- 创建一个
weak指针指向self - 不增加
self的引用计数 - 如果
self被释放,weakSelf会自动变成nil
内存状态:
lua
self 的引用计数 = 1(假设只有这里引用)
weakSelf → 指向 self(但不增加引用计数)
为什么需要 weak?
- 如果 block 里直接用
self,block 会强引用self - 形成循环:
self→block→self(循环引用!) - 用
weakSelf后,block 只弱引用self,打破循环
第二步:在 block 内部使用 __strong typeof(weakSelf) strongSelf = weakSelf;
ini
self.block = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
// ...
};
作用:
- 在 block 执行时 ,把
weakSelf转成strongSelf(强引用) - 如果
weakSelf是nil,strongSelf也是nil - 如果
weakSelf不是nil,strongSelf会增加引用计数 ,保证执行期间self不会被释放
内存状态变化:
情况 A:block 执行时,self 还存在
ruby
执行前:
self 引用计数 = 1
weakSelf → self(弱引用)
执行时(进入 block):
strongSelf = weakSelf; // strongSelf 强引用 self
self 引用计数 = 2 ← 增加了!
执行中:
[self doSomething]; // 安全!self 不会被释放
执行后(block 结束):
strongSelf 作用域结束,自动 release
self 引用计数 = 1 ← 恢复
情况 B:block 执行时,self 已经被释放
ruby
执行前:
self 引用计数 = 0,已被释放
weakSelf = nil(自动置 nil)
执行时(进入 block):
strongSelf = weakSelf; // strongSelf = nil
if (!strongSelf) return; // 直接返回,不执行后续代码
第三步:if (!strongSelf) return;
kotlin
if (!strongSelf) return;
作用:
- 安全检查 :如果
self已经被释放,weakSelf是nil,strongSelf也是nil - 直接返回,避免后续代码访问已释放的对象
为什么需要这个检查?
- 虽然访问
nil对象在 OC 中是安全的(不会崩溃),但逻辑上不应该执行 - 提前返回,避免执行无意义的代码
第四步:使用 strongSelf 而不是 weakSelf
ini
[strongSelf doSomething]; // ✅ 正确
// [weakSelf doSomething]; // ⚠️ 理论上可以,但不推荐
为什么用 strongSelf?
关键原因:防止执行中途被释放
ini
// ❌ 危险:只用 weakSelf
self.block = ^{
__weak typeof(self) weakSelf = self;
if (!weakSelf) return;
// 假设 doSomething 执行时间很长
[weakSelf doSomething]; // 执行到一半...
// 如果此时 self 被释放了(其他强引用都断了)
// weakSelf 变成 nil,但代码还在执行!
[weakSelf doAnotherThing]; // 可能访问 nil
};
// ✅ 安全:使用 strongSelf
self.block = ^{
__weak typeof(self) weakSelf = self;
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
// strongSelf 强引用 self,保证整个 block 执行期间 self 不会被释放
[strongSelf doSomething]; // self 引用计数 = 2,安全
[strongSelf doAnotherThing]; // self 引用计数 = 2,安全
// block 结束,strongSelf 释放,self 引用计数 = 1
};
完整执行流程示例
objectivec
@interface ViewController ()
@property (nonatomic, copy) void (^block)(void);
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// 步骤 1:创建 weak 引用
__weak typeof(self) weakSelf = self;
// 此时:self 引用计数 = 1,weakSelf → self(弱引用)
// 步骤 2:创建 block(捕获 weakSelf,不是 self)
self.block = ^{
// 步骤 3:block 执行时,转为 strong 引用
__strong typeof(weakSelf) strongSelf = weakSelf;
// 步骤 4:安全检查
if (!strongSelf) {
NSLog(@"self 已被释放,不执行");
return;
}
// 步骤 5:使用 strongSelf(保证执行期间 self 不会被释放)
NSLog(@"执行任务,self 引用计数 = %lu", [strongSelf retainCount]);
[strongSelf doSomething];
// 步骤 6:block 结束,strongSelf 自动释放
// self 引用计数恢复
};
// 步骤 7:viewDidLoad 结束,但 block 还在(被 self.block 持有)
}
- (void)doSomething {
NSLog(@"执行任务");
}
- (void)dealloc {
NSLog(@"ViewController 被释放");
// 如果 block 还在,这里不会被调用(因为循环引用)
// 如果用了 weak-strong dance,这里会被调用
}
@end
常见问题解答
Q1:为什么不能直接用 weakSelf?
ini
// ❌ 不推荐
self.block = ^{
__weak typeof(self) weakSelf = self;
[weakSelf doSomething]; // 执行中途 self 可能被释放
};
答案: 虽然不会崩溃(OC 对 nil 消息安全),但执行中途 self 可能被释放,导致逻辑错误。
Q2:strongSelf 会不会又造成循环引用?为什么 block 里用了 strong 修饰,不也是强引用 self 吗?
答案:不会! 这是最关键的理解点!
关键理解:block 捕获的是什么?
重要:block 捕获的是 weakSelf(弱引用),不是 strongSelf!
objectivec
__weak typeof(self) weakSelf = self; // 步骤 1:创建 weak 引用
self.block = ^{
// 步骤 2:block 捕获的是 weakSelf(弱引用)
// block 内部结构(伪代码):
// struct Block {
// __weak typeof(self) weakSelf; // ← block 捕获的是这个!
// void (*invoke)(...);
// };
// 步骤 3:block 执行时,才创建 strongSelf(局部变量)
__strong typeof(weakSelf) strongSelf = weakSelf;
// strongSelf 是 block 执行时才创建的,不是 block 捕获的!
};
详细解释:为什么不会形成循环引用?
情况 A:如果 block 直接捕获 self(会形成循环引用)
python
// ❌ 错误:block 捕获 self(强引用)
self.block = ^{
[self doSomething]; // block 捕获 self(强引用)
};
// 内存关系:
// self → block(强引用)
// block → self(强引用,因为捕获了 self)
// 形成循环:self → block → self ❌
情况 B:block 捕获 weakSelf,执行时创建 strongSelf(不会形成循环引用)
objectivec
// ✅ 正确:block 捕获 weakSelf(弱引用)
__weak typeof(self) weakSelf = self;
self.block = ^{
// block 捕获的是 weakSelf(弱引用),不是 self!
// 所以:block → weakSelf(弱引用,不增加引用计数)
// strongSelf 是 block 执行时才创建的局部变量
__strong typeof(weakSelf) strongSelf = weakSelf;
// strongSelf 不是 block 捕获的,是执行时的临时变量
};
// 内存关系:
// self → block(强引用)
// block → weakSelf(弱引用,不增加引用计数)✅
// block 执行时:strongSelf → self(临时强引用,执行完就释放)✅
// 没有循环!✅
用图示理解
错误情况(会循环引用):
lua
self ──→ block ──→ self(强引用)
↑ │
└──────────────────┘(循环!)
正确情况(不会循环引用):
lua
self ──→ block ──→ weakSelf ──→ self(弱引用,不形成循环)
↑
└──────────────────────────────┘(没有循环!)
block 执行时:
self ──→ block ──→ weakSelf ──→ self(弱引用)
↑ ↑
│ │
└──────────────────────────────┘
│
strongSelf(临时强引用,执行完就释放)
关键点总结
-
block 捕获的是什么?
- block 捕获的是
weakSelf(弱引用),不是strongSelf - 所以 block 不强引用
self,不会形成循环
- block 捕获的是
-
strongSelf 是什么?
strongSelf是 block 执行时 才创建的局部变量- 不是 block 捕获的,是执行时的临时强引用
- block 执行完,
strongSelf就释放了
-
为什么不会形成循环?
- 循环引用的关键是:block 本身是否强引用 self
- 因为 block 捕获的是
weakSelf(弱引用),所以 block 不强引用 self strongSelf只是执行时的临时强引用,不会形成持久的循环
完整的内存关系图
python
创建阶段:
self(引用计数 = 1)
↓ 强引用
block(捕获 weakSelf,弱引用 self)
↓ 弱引用(不增加引用计数)
weakSelf → self(引用计数 = 1,没有增加)
执行阶段(block 被调用):
self(引用计数 = 1)
↓ 强引用
block
↓ 弱引用
weakSelf → self(引用计数 = 1)
↓
strongSelf(局部变量,强引用 self)
↓ 强引用(临时)
self(引用计数 = 2,临时增加)
执行结束:
strongSelf 释放 → self(引用计数 = 1,恢复)
block 仍然存在,但只弱引用 self(不形成循环)
答案:不会! 因为:
- block 捕获的是
weakSelf(弱引用),不是strongSelf strongSelf是局部变量,只在 block 执行期间存在- block 执行完,
strongSelf自动释放 - 不会形成持久的循环引用,因为 block 本身不强引用 self
Q3:什么时候 weakSelf 会变成 nil?
答案: 当 self 的所有强引用都断开时:
objectivec
// 场景:ViewController 被 pop 或 dismiss
[self.navigationController popViewControllerAnimated:YES];
// 此时如果 self 没有其他强引用,会被释放
// weakSelf 自动变成 nil
Q4:可以简化成这样吗?
ini
// ⚠️ 简化版(不推荐,但某些场景可用)
__weak typeof(self) weakSelf = self;
self.block = ^{
[weakSelf doSomething]; // 直接使用 weakSelf
};
答案:
- 简单场景可以 :如果
doSomething执行很快,且不涉及多步操作 - 复杂场景不行 :如果 block 执行时间长,或有多步操作,必须用
strongSelf保证执行期间对象不被释放
面试标准答案(一句话总结)
Weak-Strong Dance 的作用:
weakSelf:打破循环引用,让 block 不强持有selfstrongSelf:在 block 执行期间强持有self,防止执行中途被释放if (!strongSelf) return:安全检查,如果self已释放则提前返回
核心思想: 用弱引用打破循环,用临时强引用保证执行安全。
场景 2:NSTimer 循环引用
objectivec
// ❌ 错误:循环引用
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// self 强引用 timer
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:self // ← timer 强引用 self
selector:@selector(timerAction)
userInfo:nil
repeats:YES];
// 形成循环:self → timer → self
}
// ✅ 正确:使用中间对象或 block-based API
- (void)viewDidLoad {
[super viewDidLoad];
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return;
[strongSelf timerAction];
}];
}
- (void)dealloc {
[self.timer invalidate]; // 必须手动停止
self.timer = nil;
}
场景 3:通知观察者循环引用
objectivec
// ⚠️ iOS 9+ 后通知中心会弱引用观察者,但业务代码仍需注意
- (void)viewDidLoad {
[super viewDidLoad];
// 如果 self 强引用通知,通知回调里又用 self,可能形成循环
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(handleNotification:)
name:@"SomeNotification"
object:nil];
}
- (void)dealloc {
// 必须移除观察者
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
1.4 Weak 表的工作机制
Weak 表是什么?
Weak 表 = 一张全局的哈希表,记录所有 weak 指针
swift
// 伪代码:Weak 表的结构
struct WeakTable {
// key: 对象的地址
// value: 所有指向这个对象的 weak 指针数组
HashMap<对象地址, Array<weak指针地址>>;
};
Weak 指针的工作流程
objectivec
NSObject *obj = [[NSObject alloc] init]; // 引用计数 = 1
__weak NSObject *weakObj = obj; // 引用计数仍 = 1
// 步骤 1:weakObj 被注册到 Weak 表
// Weak 表记录:obj 的地址 → [weakObj 的地址]
obj = nil; // 引用计数 = 0,对象即将被释放
// 步骤 2:对象释放时,系统遍历 Weak 表
// 找到所有指向这个对象的 weak 指针
// 步骤 3:把所有 weak 指针置为 nil
// weakObj 现在 = nil(安全!)
面试常问:Weak 表如何实现?
答案要点:
- 全局哈希表:以对象地址为 key,存储所有指向它的 weak 指针
- 对象释放时:遍历 Weak 表,找到所有相关 weak 指针,置为 nil
- 性能优化:使用哈希表,查找是 O(1) 平均时间复杂度
2. 内存对齐与对象大小
2.1 什么是内存对齐?
内存对齐 = 数据在内存中的起始地址必须是某个数的倍数
对齐规则(64 位系统)
-
基本类型对齐:
char:1 字节对齐short:2 字节对齐int:4 字节对齐long/指针:8 字节对齐double:8 字节对齐
-
结构体对齐:
- 结构体整体大小必须是最大成员对齐值的倍数
- 结构体起始地址必须是最大成员对齐值的倍数
示例:结构体内存对齐
objectivec
struct Example {
char a; // 1 字节,偏移 0
// 填充 3 字节(padding)
int b; // 4 字节,偏移 4(必须是 4 的倍数)
char c; // 1 字节,偏移 8
// 填充 7 字节(padding)
double d; // 8 字节,偏移 16(必须是 8 的倍数)
};
// 总大小 = 24 字节(必须是 8 的倍数)
// 验证
NSLog(@"Size: %lu", sizeof(struct Example)); // 输出:24
2.2 OC 对象的内存对齐
对象内存布局
objectivec
@interface Person : NSObject {
@public
char _name; // 1 字节
int _age; // 4 字节
double _height; // 8 字节
}
@end
// 内存布局(64 位系统):
// [isa 指针: 8 字节] [padding: 0]
// [_name: 1 字节] [padding: 3 字节]
// [_age: 4 字节]
// [padding: 4 字节](为了 double 对齐)
// [_height: 8 字节]
// 总大小 = 8 + 4 + 4 + 8 = 24 字节(必须是 8 的倍数)
查看对象实际大小
scss
Person *p = [[Person alloc] init];
// 方法 1:实例大小(对齐后)
size_t instanceSize = class_getInstanceSize([Person class]);
NSLog(@"Instance size: %zu", instanceSize); // 输出:24
// 方法 2:实际分配大小(系统可能分配更多)
size_t mallocSize = malloc_size((__bridge const void *)p);
NSLog(@"Malloc size: %zu", mallocSize); // 可能输出:32(系统额外分配)
2.3 编译器如何插入 Padding?
objectivec
@interface Example : NSObject {
char a; // 偏移 8(isa 后),大小 1
// 编译器插入 padding: 3 字节
int b; // 偏移 12,大小 4
char c; // 偏移 16,大小 1
// 编译器插入 padding: 7 字节(为了 double 对齐)
double d; // 偏移 24,大小 8
}
@end
// 编译器优化:调整成员顺序可以减少 padding
@interface OptimizedExample : NSObject {
double d; // 偏移 8,大小 8(最大对齐值)
int b; // 偏移 16,大小 4
char a; // 偏移 20,大小 1
char c; // 偏移 21,大小 1
// padding: 6 字节(为了整体 8 字节对齐)
}
@end
// 优化后总大小可能更小!
2.4 面试常问点
Q:为什么需要内存对齐?
答案要点:
- CPU 读取效率:未对齐的数据可能需要多次内存访问
- 硬件要求:某些 CPU 架构要求数据必须对齐,否则崩溃
- 缓存行优化:对齐的数据更容易放入 CPU 缓存行
3. Tagged Pointer 技术
3.1 什么是 Tagged Pointer?
Tagged Pointer = 把小数据直接编码进指针里,不占用堆内存
传统方式 vs Tagged Pointer
objectivec
// 传统方式(64 位系统)
NSNumber *num1 = @(42);
// 内存布局:
// 指针变量(栈上,8 字节)→ 指向堆上的 NSNumber 对象(至少 16 字节)
// 总占用:8 + 16 = 24 字节
// Tagged Pointer 方式
NSNumber *num2 = @(42);
// 内存布局:
// 指针变量(栈上,8 字节),但指针里直接存了 42 的值!
// 总占用:8 字节(节省 16 字节!)
3.2 Tagged Pointer 的识别
objectivec
NSNumber *num1 = @(42);
NSNumber *num2 = @(1000000); // 大数字
// 判断是否是 Tagged Pointer
NSLog(@"num1 is Tagged: %d", _objc_isTaggedPointer((__bridge void *)num1));
// 输出:1(是 Tagged Pointer)
NSLog(@"num2 is Tagged: %d", _objc_isTaggedPointer((__bridge void *)num2));
// 输出:0(不是,因为数字太大)
3.3 哪些对象支持 Tagged Pointer?
- NSNumber:小整数(通常 < 2^60)
- NSDate:时间戳在某个范围内
- NSString:短字符串(通常 < 7 个字符,ASCII)
- NSIndexPath:某些 iOS 版本
示例:NSString 的 Tagged Pointer
objectivec
NSString *str1 = @"abc"; // Tagged Pointer
NSString *str2 = @"abcdefghijkl"; // 普通对象(堆上)
// 验证
NSLog(@"str1 pointer: %p", str1); // 指针值看起来很奇怪(有 tag 位)
NSLog(@"str2 pointer: %p", str2); // 正常的堆地址
// 查看实际内容
NSLog(@"str1: %@", str1); // 正常输出
NSLog(@"str2: %@", str2); // 正常输出
3.4 Tagged Pointer 的优势
- 节省内存:不需要堆分配
- 提高性能:不需要引用计数管理
- 减少碎片:不占用堆空间
3.5 面试常问点
Q:Tagged Pointer 如何工作?
答案要点:
- 利用指针的未使用位:64 位指针只用 48 位,剩余位用来存 tag 和数据
- 特殊标记位:最低位通常是 1,表示这是 Tagged Pointer
- 类型编码:用几个位表示类型(NSNumber/NSString/NSDate 等)
- 数据编码:剩余位存实际数据
4. Mach-O 文件结构与内存映射
4.1 Mach-O 是什么?
Mach-O = macOS/iOS 的可执行文件格式
类似于:
- Windows:
.exe(PE 格式) - Linux:ELF 格式
- macOS/iOS:
.app(Mach-O 格式)
4.2 Mach-O 的基本结构
bash
Mach-O 文件
├── Header(文件头)
│ ├── 魔数(标识文件类型)
│ ├── CPU 架构(arm64/x86_64)
│ └── 加载命令数量
│
├── Load Commands(加载命令)
│ ├── 代码段位置
│ ├── 数据段位置
│ └── 动态库依赖
│
└── Data(数据区)
├── __TEXT(代码段)
│ ├── 可执行代码
│ └── 常量字符串
│
└── __DATA(数据段)
├── 全局变量
├── 静态变量
└── 类元数据
4.3 主要段(Segment)详解
__TEXT 段(代码段)
特点:只读(Read-Only)、可执行(Executable)
objectivec
// 这些内容在 __TEXT 段:
// 1. 可执行代码
- (void)example {
NSLog(@"Hello"); // 这行代码编译后的机器指令在 __TEXT 段
}
// 2. 常量字符串
NSString *str = @"Hello"; // @"Hello" 在 __TEXT 段
// 3. 常量数据
const int kValue = 100; // 在 __TEXT 段
__DATA 段(数据段)
特点:可读写(Read-Write)
objectivec
// 这些内容在 __DATA 段:
// 1. 全局变量
int globalVar = 10; // 在 __DATA 段
// 2. 静态变量
static int staticVar = 20; // 在 __DATA 段
// 3. 类元数据(运行时注册)
@interface MyClass : NSObject
@end
// MyClass 的类对象信息在 __DATA 段
4.4 类对象在 Mach-O 中的位置
less
@interface Person : NSObject
@end
// 编译后,Person 类的信息存储在:
// 1. __TEXT 段:方法实现(机器码)
// 2. __DATA 段:类对象结构
// - isa 指针
// - superclass 指针
// - 方法列表指针
// - 属性列表指针
// - 协议列表指针
4.5 静态库 vs 动态库的内存映射
静态库(.a 文件)
arduino
// 静态库的代码被直接链接进主可执行文件
// 内存映射:
// 主可执行文件的 __TEXT 段包含静态库的代码
// 主可执行文件的 __DATA 段包含静态库的数据
动态库(.dylib / .framework)
arduino
// 动态库由 dyld(动态链接器)在运行时加载
// 内存映射:
// 1. dyld 读取动态库的 Mach-O 文件
// 2. 将 __TEXT 段映射到内存(只读、可执行)
// 3. 将 __DATA 段映射到内存(可读写)
// 4. 每个进程共享同一份 __TEXT 段(节省内存)
// 5. 每个进程有独立的 __DATA 段副本
4.6 面试常问点
Q:类对象在哪里?
答案要点:
- 编译时:类信息写在 Mach-O 的 __DATA 段
- 运行时:dyld 加载 Mach-O,将类信息注册到 runtime
- 内存位置:类对象在进程的虚拟地址空间中(具体地址由 ASLR 随机化)
5. AutoreleasePool 与 RunLoop 关系
5.1 AutoreleasePool 是什么?
AutoreleasePool = 延迟释放池,让对象"晚一点"释放
ini
// 传统 release(立即释放)
NSObject *obj = [[NSObject alloc] init];
[obj release]; // 立即释放,引用计数 = 0
// Autorelease(延迟释放)
NSObject *obj = [[NSObject alloc] init];
[obj autorelease]; // 加入自动释放池,等池子结束时才 release
5.2 AutoreleasePool 的结构
scss
// AutoreleasePool 是一个栈结构
@autoreleasepool {
// Pool 1(外层)
@autoreleasepool {
// Pool 2(内层)
NSObject *obj = [[NSObject alloc] init];
// obj 被加入 Pool 2
}
// Pool 2 结束,obj 被释放
}
// Pool 1 结束
5.3 RunLoop 与 AutoreleasePool 的关系
主线程的隐式 AutoreleasePool
scss
// 主线程的 RunLoop 结构(简化)
void mainRunLoop() {
while (appIsRunning) {
@autoreleasepool { // ← 系统自动创建
// 处理事件
handleEvents();
// 处理定时器
handleTimers();
// 处理 Source
handleSources();
}
// 池子结束,释放所有 autorelease 的对象
}
}
关键点:
- 主线程的每个 RunLoop 周期都有一个隐式的
@autoreleasepool - 当 RunLoop 进入休眠或结束一个周期时,池子会 drain(释放所有对象)
子线程没有隐式 AutoreleasePool
ini
// ❌ 错误:子线程大量创建对象
dispatch_async(dispatch_get_global_queue(0, 0), ^{
for (int i = 0; i < 10000; i++) {
NSObject *obj = [[NSObject alloc] init];
// obj 被 autorelease,但没有池子,会积压!
}
});
// ✅ 正确:手动创建 AutoreleasePool
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@autoreleasepool {
for (int i = 0; i < 10000; i++) {
NSObject *obj = [[NSObject alloc] init];
// obj 在池子结束时释放
}
}
// 或者更细粒度:
for (int i = 0; i < 10000; i++) {
@autoreleasepool {
NSObject *obj = [[NSObject alloc] init];
// 每次循环结束就释放
}
}
});
5.4 什么时候需要手动创建 AutoreleasePool?
场景 1:子线程大量创建对象
scss
dispatch_async(dispatch_get_global_queue(0, 0), ^{
@autoreleasepool {
// 大量临时对象
for (int i = 0; i < 100000; i++) {
NSString *str = [NSString stringWithFormat:@"%d", i];
// 使用 str...
}
}
// 池子结束,所有临时对象立即释放,降低峰值内存
});
场景 2:大循环中创建临时对象
objectivec
// ❌ 不好:所有临时对象积压到外层池子
for (int i = 0; i < 10000; i++) {
NSMutableArray *arr = [NSMutableArray array]; // autorelease
// 使用 arr...
}
// ✅ 好:每次循环结束就释放
for (int i = 0; i < 10000; i++) {
@autoreleasepool {
NSMutableArray *arr = [NSMutableArray array];
// 使用 arr...
}
// arr 立即释放
}
5.5 面试常问点
Q:为什么子线程需要手动创建 AutoreleasePool?
答案要点:
- 主线程:RunLoop 自动创建和销毁 AutoreleasePool
- 子线程:没有 RunLoop(或 RunLoop 不活跃),没有隐式池子
- 后果:autorelease 的对象会积压,直到线程结束才释放,导致内存峰值过高
- 解决 :手动创建
@autoreleasepool,及时释放临时对象
6. 堆分配策略与内存碎片
6.1 堆内存分配器(malloc)
iOS 使用 jemalloc 或类似的分配器管理堆内存。
分配策略(简化)
markdown
堆内存分配器
├── Tiny 区(< 16 字节)
│ └── 快速分配,固定大小块
│
├── Small 区(16 字节 ~ 几 KB)
│ └── 按大小分类的块池
│
└── Large 区(> 几 KB)
└── 直接 mmap 分配
6.2 内存碎片问题
什么是内存碎片?
scss
// 场景:频繁分配和释放不同大小的对象
// 1. 分配 100 字节
void *p1 = malloc(100);
// 2. 分配 200 字节
void *p2 = malloc(200);
// 3. 释放 p1(100 字节的空洞)
free(p1);
// 4. 现在想分配 150 字节
void *p3 = malloc(150);
// 问题:p1 的空洞只有 100 字节,不够!
// 只能从其他地方分配,导致碎片
如何减少碎片?
策略 1:对象池(Object Pool)
ini
// 复用对象,而不是频繁创建和销毁
@interface ObjectPool : NSObject
+ (instancetype)sharedPool;
- (id)getObject;
- (void)returnObject:(id)obj;
@end
// 使用
ObjectPool *pool = [ObjectPool sharedPool];
MyObject *obj = [pool getObject];
// 使用 obj...
[pool returnObject:obj]; // 归还,而不是释放
策略 2:批量分配
arduino
// 一次性分配大块内存,自己管理
void *buffer = malloc(1024 * 1024); // 1MB
// 自己在这 1MB 里分配小对象
// 减少系统 malloc 调用次数
6.3 面试常问点
Q:如何优化内存分配性能?
答案要点:
- 对象池:复用对象,减少分配/释放次数
- 批量分配:一次性分配大块内存,自己管理
- 避免频繁小对象分配:合并小对象,或使用结构体
- 使用 AutoreleasePool:及时释放临时对象,降低峰值
7. 栈基础与栈溢出
7.1 栈的基本概念
栈 = 函数调用的"工作区"
csharp
void functionA() {
int a = 10; // 在栈上
functionB();
}
void functionB() {
int b = 20; // 在栈上
functionC();
}
void functionC() {
int c = 30; // 在栈上
}
// 调用栈(从下往上):
// [functionA 的栈帧: a = 10]
// [functionB 的栈帧: b = 20]
// [functionC 的栈帧: c = 30] ← 栈顶
7.2 iOS 线程栈大小
objectivec
// 主线程栈大小:通常 1MB
// 子线程栈大小:通常 512KB(可配置)
// 创建自定义栈大小的线程
NSThread *thread = [[NSThread alloc] initWithTarget:self
selector:@selector(threadMain)
object:nil];
thread.stackSize = 1024 * 1024; // 1MB
[thread start];
7.3 栈溢出的常见原因
原因 1:无限递归
csharp
// ❌ 错误:无限递归
- (void)recursive {
int localVar[1000]; // 大局部变量
[self recursive]; // 无限递归,栈帧不断增长
// 最终:栈溢出(Stack Overflow)
}
// ✅ 正确:有终止条件
- (void)recursiveWithDepth:(int)depth {
if (depth <= 0) return; // 终止条件
int localVar[1000];
[self recursiveWithDepth:depth - 1];
}
原因 2:大局部变量
c
// ❌ 危险:大数组在栈上
- (void)example {
int hugeArray[1000000]; // 4MB 在栈上!
// 可能栈溢出
}
// ✅ 安全:大数组在堆上
- (void)example {
int *hugeArray = malloc(1000000 * sizeof(int)); // 堆上
// 使用...
free(hugeArray);
}
7.4 面试常问点
Q:栈溢出如何避免?
答案要点:
- 避免无限递归:确保递归有终止条件
- 大变量用堆 :大数组、大结构体用
malloc或对象 - 限制递归深度:设置最大递归深度
- 增加栈大小 :
pthread_attr_setstacksize(不推荐,治标不治本)
8. 类/元类查找链与方法缓存
8.1 方法查找流程(完整版)
less
// 调用:[obj methodName]
// 步骤 1:通过 isa 找到类对象
Class cls = object_getClass(obj); // obj->isa
// 步骤 2:在类对象的方法列表中查找
Method method = class_getInstanceMethod(cls, @selector(methodName));
// 步骤 3:如果没找到,沿 superclass 链向上查找
while (cls && !method) {
cls = class_getSuperclass(cls);
method = class_getInstanceMethod(cls, @selector(methodName));
}
// 步骤 4:如果找到,调用 method->imp(函数指针)
8.2 方法缓存(Method Cache)
为什么需要缓存?
方法查找需要遍历类的方法列表,如果每次都查找,性能很差。
缓存机制:
kotlin
// 伪代码:方法缓存结构
struct MethodCache {
// 哈希表:selector → IMP
HashMap<Selector, IMP> cache;
};
// 查找流程(带缓存):
IMP imp = cache.get(selector);
if (imp) {
return imp; // 缓存命中,直接返回
} else {
// 缓存未命中,查找方法列表
imp = findMethodInClass(selector);
cache.set(selector, imp); // 加入缓存
return imp;
}
8.3 类方法 vs 实例方法
less
@interface Person : NSObject
- (void)instanceMethod; // 实例方法
+ (void)classMethod; // 类方法
@end
// 调用实例方法
Person *p = [[Person alloc] init];
[p instanceMethod];
// 查找路径:p->isa(Person 类)→ 查找实例方法列表
// 调用类方法
[Person classMethod];
// 查找路径:Person 类对象->isa(Person 元类)→ 查找类方法列表
8.4 元类链(完整)
objectivec
// 元类链(简化)
Person 实例
└─ isa → Person 类对象
├─ isa → Person 元类
│ ├─ isa → NSObject 元类
│ │ └─ isa → NSObject 元类(根元类指向自己)
│ └─ superclass → NSObject 元类
└─ superclass → NSObject 类对象
└─ isa → NSObject 元类
8.5 面试常问点
Q:方法查找的完整流程?
答案要点:
- 实例方法:对象 isa → 类对象 → 方法列表 → superclass 链向上查找
- 类方法:类对象 isa → 元类 → 方法列表 → 元类的 superclass 链向上查找
- 缓存优化:查找结果缓存到 MethodCache,下次直接命中
- 消息转发 :如果最终没找到,进入消息转发机制(
forwardingTargetForSelector:等)
9. OC vs C++ 内存模型差异
9.1 对象创建位置
Objective-C
swift
// OC 对象总是在堆上
NSObject *obj = [[NSObject alloc] init];
// obj 是指针(栈上),指向堆上的对象
C++
arduino
// C++ 对象可以在栈上
class MyClass {
public:
int value;
};
void example() {
MyClass obj; // 栈上对象
obj.value = 10;
} // obj 自动析构
// 也可以在堆上
MyClass *obj = new MyClass(); // 堆上对象
delete obj; // 手动释放
9.2 内存管理方式
Objective-C:引用计数
ini
NSObject *obj1 = [[NSObject alloc] init]; // 引用计数 = 1
NSObject *obj2 = obj1; // 引用计数 = 2
obj1 = nil; // 引用计数 = 1
obj2 = nil; // 引用计数 = 0,对象释放
C++:RAII(资源获取即初始化)
scss
class MyClass {
public:
MyClass() { /* 构造 */ }
~MyClass() { /* 析构,自动调用 */ }
};
void example() {
MyClass obj; // 构造
// 使用 obj...
} // 自动析构(栈上对象)
// 堆上对象需要手动管理
MyClass *obj = new MyClass();
delete obj; // 手动析构
9.3 多态实现方式
Objective-C:isa 指针 + 消息发送
less
@interface Animal : NSObject
- (void)speak;
@end
@interface Dog : Animal
- (void)speak; // 重写
@end
Animal *animal = [[Dog alloc] init];
[animal speak]; // 运行时查找,调用 Dog 的 speak
// 通过 isa 指针找到实际类型
C++:虚函数表(vtable)
csharp
class Animal {
public:
virtual void speak() { /* 基类实现 */ }
// 有虚函数,对象有 vptr(虚函数表指针)
};
class Dog : public Animal {
public:
void speak() override { /* 派生类实现 */ }
};
Animal *animal = new Dog();
animal->speak(); // 通过 vptr 找到虚函数表,调用 Dog::speak
9.4 Objective-C++ 混编注意事项
arduino
// Objective-C++ 文件(.mm)
// OC 对象
NSObject *obj = [[NSObject alloc] init];
// C++ 对象
std::vector<int> vec;
vec.push_back(1);
// ⚠️ 注意:C++ 异常不能穿越 OC 代码
// 如果 C++ 代码抛异常,必须在 C++ 代码里捕获
9.5 面试常问点
Q:OC 和 C++ 的内存管理有什么区别?
答案要点:
- OC:引用计数(ARC),对象在堆上,通过 isa 实现多态
- C++ :RAII,对象可在栈/堆,通过虚函数表实现多态
- OC:自动管理(ARC),但需注意循环引用
- C++ :手动管理(new/delete)或智能指针(shared_ptr/unique_ptr)
10. 虚拟内存与物理内存映射
10.1 什么是虚拟内存?
虚拟内存 = 进程看到的"假地址空间"
css
进程视角(虚拟地址):
0x00000000 ──────────┐
│
0x10000000 ──────────┤ 代码段
│
0x20000000 ──────────┤ 数据段
│
0x30000000 ──────────┤ 堆
│
0x40000000 ──────────┤ 栈
│
0x7FFFFFFF ──────────┘
实际物理内存:
[物理地址 0x1000] ← 可能映射到虚拟地址 0x10000000
[物理地址 0x2000] ← 可能映射到虚拟地址 0x20000000
...
10.2 页(Page)的概念
页 = 内存管理的最小单位(通常 4KB 或 16KB)
less
// 虚拟地址空间被分成页
虚拟地址:0x10000000 - 0x10000FFF → 页 1
虚拟地址:0x10001000 - 0x10001FFF → 页 2
虚拟地址:0x10002000 - 0x10002FFF → 页 3
// 每页可以独立映射到物理内存
页 1 → 物理页 A
页 2 → 物理页 B
页 3 → 未映射(访问会触发缺页异常)
10.3 页表(Page Table)
页表 = 虚拟地址到物理地址的映射表
虚拟地址:0x10000000
↓
页表查找
↓
物理地址:0x50000000
10.4 写时复制(Copy-On-Write, COW)
objectivec
// 场景:fork 进程或复制大对象
// 1. 父进程有数据
NSMutableArray *arr = [NSMutableArray arrayWithObjects:@1, @2, nil];
// 2. 子进程 fork(或复制)
// 此时:父子进程共享同一份物理内存(只读)
// 3. 子进程修改数据
[arr addObject:@3];
// 4. 触发写时复制
// 系统复制物理页,子进程有自己的副本
// 现在:父子进程有独立的物理内存
10.5 代码段页共享
less
// 多个进程运行同一个 App
进程 A:加载 MyApp
进程 B:加载 MyApp
// 代码段(__TEXT)的物理页被共享
// 节省物理内存!
10.6 ASLR(地址空间布局随机化)
arduino
// 没有 ASLR(固定地址)
代码段起始:0x10000000(固定)
// 有 ASLR(随机地址)
进程 1 代码段起始:0x10001234(随机)
进程 2 代码段起始:0x10005678(随机)
// 目的:防止攻击者预测地址
10.7 面试常问点
Q:虚拟内存的作用?
答案要点:
- 地址空间隔离:每个进程有独立的虚拟地址空间
- 内存保护:不同段有不同的读写执行权限
- 按需加载:只有访问的页才映射到物理内存
- 共享内存:多个进程可以共享代码段的物理页
- 安全性:ASLR 随机化地址,防止攻击
11. Weak 表实现与性能
11.1 Weak 表的底层结构
objectivec
// 伪代码:Weak 表结构
struct WeakTable {
// 全局哈希表
// key: 对象的地址(作为弱引用目标)
// value: 指向这个对象的所有 weak 指针的数组
HashMap<void *, Array<void **>> weakReferences;
};
// 示例:
NSObject *obj = [[NSObject alloc] init];
__weak NSObject *weak1 = obj;
__weak NSObject *weak2 = obj;
// Weak 表记录:
// obj 的地址 → [weak1 的地址, weak2 的地址]
11.2 Weak 指针注册流程
scss
NSObject *obj = [[NSObject alloc] init]; // 对象创建
__weak NSObject *weakObj = obj; // weak 指针赋值
// 系统内部操作(伪代码):
void weak_assign(id *location, id newObj) {
// 1. 如果之前有 weak 指针,先移除
if (*location) {
removeWeakReference(*location, location);
}
// 2. 设置新的 weak 指针
*location = newObj;
// 3. 如果新对象不为 nil,注册到 Weak 表
if (newObj) {
addWeakReference(newObj, location);
}
}
11.3 对象释放时的 Weak 清理
scss
// 对象释放流程(伪代码)
void object_release(id obj) {
// 1. 引用计数减 1
if (retainCount(obj) > 1) {
retainCount(obj)--;
return;
}
// 2. 引用计数为 0,准备释放
// 3. 查找 Weak 表,找到所有指向这个对象的 weak 指针
Array<void **> weakRefs = getWeakReferences(obj);
// 4. 把所有 weak 指针置为 nil
for (void **weakPtr in weakRefs) {
*weakPtr = nil;
}
// 5. 从 Weak 表中移除记录
removeWeakTableEntry(obj);
// 6. 释放对象内存
free(obj);
}
11.4 Weak 表的性能考虑
优势
- 哈希表查找:O(1) 平均时间复杂度
- 批量清理:对象释放时一次性清理所有 weak 指针
潜在开销
objectivec
// 场景:大量 weak 指针指向同一个对象
NSObject *obj = [[NSObject alloc] init];
for (int i = 0; i < 10000; i++) {
__weak NSObject *weak = obj; // 每个 weak 都注册到 Weak 表
}
// 对象释放时,需要清理 10000 个 weak 指针
// 虽然还是 O(n),但 n 可能很大
11.5 面试常问点
Q:Weak 表如何实现?性能如何?
答案要点:
- 数据结构:全局哈希表,key 是对象地址,value 是 weak 指针数组
- 注册:weak 指针赋值时,注册到 Weak 表
- 清理:对象释放时,遍历 Weak 表,把所有 weak 指针置 nil
- 性能:哈希表查找 O(1),但大量 weak 指针时清理可能较慢
- 优化:系统有优化机制,实际性能通常可接受
总结:完整知识线回顾
从对象到硬件内存的完整路径
css
1. 代码层面
└─ OC 对象(Person *p = [[Person alloc] init])
├─ isa 指针 → 类对象
├─ 成员变量(内存对齐)
└─ 引用计数管理
2. 运行时层面
└─ 类对象 / 元类
├─ 方法列表
├─ 属性列表
└─ 方法缓存
3. 内存布局层面
└─ 虚拟地址空间
├─ 代码段(__TEXT):类的方法实现
├─ 数据段(__DATA):类对象、全局变量
├─ 堆:对象实例
└─ 栈:局部变量、函数调用
4. 系统层面
└─ 虚拟内存 → 物理内存映射
├─ 页表映射
├─ ASLR 随机化
└─ 写时复制
5. 硬件层面
└─ 物理内存(RAM)
└─ CPU 缓存(L1/L2/L3)
面试重点检查清单
- ARC 的 retain/release 插入规则
- 循环引用的典型场景和解决方案
- Weak 表的工作机制
- 内存对齐规则和对象大小计算
- Tagged Pointer 的原理和优势
- Mach-O 文件结构和段的作用
- AutoreleasePool 与 RunLoop 的关系
- 堆分配策略和内存碎片
- 栈溢出原因和避免方法
- 类/元类查找链和方法缓存
- OC vs C++ 内存模型差异
- 虚拟内存到物理内存的映射
- Weak 表的实现和性能
祝你面试顺利! 🚀