iOS 循环引用篇 菜鸟都能看懂

iOS 内存管理完整补充知识

从对象到类、从结构体到元类、从 C++ 到内存分布区、到手机硬件内存的完整知识线


目录

  1. [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")
  2. 内存对齐与对象大小
  3. [Tagged Pointer 技术](#Tagged Pointer 技术 "#3-tagged-pointer-%E6%8A%80%E6%9C%AF")
  4. [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")
  5. [AutoreleasePool 与 RunLoop 关系](#AutoreleasePool 与 RunLoop 关系 "#5-autoreleasepool-%E4%B8%8E-runloop-%E5%85%B3%E7%B3%BB")
  6. 堆分配策略与内存碎片
  7. 栈基础与栈溢出
  8. 类/元类查找链与方法缓存
  9. [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")
  10. 虚拟内存与物理内存映射
  11. [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 不是运行时技术,而是编译时技术!

编译器会在编译阶段 自动插入 retainreleaseautorelease 调用。

示例代码对比

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 = nilb = 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 循环引用的核心理解

循环引用 = 两个或多个对象互相强引用,形成闭环,导致都无法释放

关键点:

  1. 必须是"强引用" :weak 引用不会形成循环引用
  2. 必须是"互相" :A → B → A(闭环)
  3. 结果:引用计数永远不会变成 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 工具
  1. 打开 Xcode
  2. Product → Profile(或 Cmd + I)
  3. 选择 Leaks
  4. 运行应用,执行可能产生循环引用的操作
  5. 查看是否有内存泄漏
方法 3:使用 Xcode Memory Graph
  1. 运行应用
  2. 在 Debug Navigator 中点击 Memory Graph
  3. 查看对象是否正常释放
方法 4:使用 MLeaksFinder(第三方工具)

自动检测内存泄漏,在开发阶段就能发现问题。


✅ 如何避免循环引用?

  1. 使用 weak 引用:在 block、delegate、通知等场景使用 weak
  2. 及时断开引用:在不需要时手动设置为 nil
  3. 使用 weak-strong dance:在 block 中使用 weak-strong dance 模式
  4. 代码审查:定期检查代码,特别是 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
  • 形成循环:selfblockself(循环引用!)
  • weakSelf 后,block 只弱引用 self,打破循环

第二步:在 block 内部使用 __strong typeof(weakSelf) strongSelf = weakSelf;
ini 复制代码
self.block = ^{
    __strong typeof(weakSelf) strongSelf = weakSelf;
    // ...
};

作用:

  • 在 block 执行时 ,把 weakSelf 转成 strongSelf(强引用)
  • 如果 weakSelfnilstrongSelf 也是 nil
  • 如果 weakSelf 不是 nilstrongSelf增加引用计数 ,保证执行期间 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 已经被释放,weakSelfnilstrongSelf 也是 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(临时强引用,执行完就释放)
关键点总结
  1. block 捕获的是什么?

    • block 捕获的是 weakSelf(弱引用),不是 strongSelf
    • 所以 block 不强引用 self,不会形成循环
  2. strongSelf 是什么?

    • strongSelf 是 block 执行时 才创建的局部变量
    • 不是 block 捕获的,是执行时的临时强引用
    • block 执行完,strongSelf 就释放了
  3. 为什么不会形成循环?

    • 循环引用的关键是: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 的作用:

  1. weakSelf :打破循环引用,让 block 不强持有 self
  2. strongSelf :在 block 执行期间强持有 self,防止执行中途被释放
  3. 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 表如何实现?

答案要点:

  1. 全局哈希表:以对象地址为 key,存储所有指向它的 weak 指针
  2. 对象释放时:遍历 Weak 表,找到所有相关 weak 指针,置为 nil
  3. 性能优化:使用哈希表,查找是 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:为什么需要内存对齐?

答案要点:

  1. CPU 读取效率:未对齐的数据可能需要多次内存访问
  2. 硬件要求:某些 CPU 架构要求数据必须对齐,否则崩溃
  3. 缓存行优化:对齐的数据更容易放入 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 的优势

  1. 节省内存:不需要堆分配
  2. 提高性能:不需要引用计数管理
  3. 减少碎片:不占用堆空间

3.5 面试常问点

Q:Tagged Pointer 如何工作?

答案要点:

  1. 利用指针的未使用位:64 位指针只用 48 位,剩余位用来存 tag 和数据
  2. 特殊标记位:最低位通常是 1,表示这是 Tagged Pointer
  3. 类型编码:用几个位表示类型(NSNumber/NSString/NSDate 等)
  4. 数据编码:剩余位存实际数据

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:类对象在哪里?

答案要点:

  1. 编译时:类信息写在 Mach-O 的 __DATA 段
  2. 运行时:dyld 加载 Mach-O,将类信息注册到 runtime
  3. 内存位置:类对象在进程的虚拟地址空间中(具体地址由 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?

答案要点:

  1. 主线程:RunLoop 自动创建和销毁 AutoreleasePool
  2. 子线程:没有 RunLoop(或 RunLoop 不活跃),没有隐式池子
  3. 后果:autorelease 的对象会积压,直到线程结束才释放,导致内存峰值过高
  4. 解决 :手动创建 @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:如何优化内存分配性能?

答案要点:

  1. 对象池:复用对象,减少分配/释放次数
  2. 批量分配:一次性分配大块内存,自己管理
  3. 避免频繁小对象分配:合并小对象,或使用结构体
  4. 使用 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:栈溢出如何避免?

答案要点:

  1. 避免无限递归:确保递归有终止条件
  2. 大变量用堆 :大数组、大结构体用 malloc 或对象
  3. 限制递归深度:设置最大递归深度
  4. 增加栈大小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:方法查找的完整流程?

答案要点:

  1. 实例方法:对象 isa → 类对象 → 方法列表 → superclass 链向上查找
  2. 类方法:类对象 isa → 元类 → 方法列表 → 元类的 superclass 链向上查找
  3. 缓存优化:查找结果缓存到 MethodCache,下次直接命中
  4. 消息转发 :如果最终没找到,进入消息转发机制(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++ 的内存管理有什么区别?

答案要点:

  1. OC:引用计数(ARC),对象在堆上,通过 isa 实现多态
  2. C++ :RAII,对象可在栈/堆,通过虚函数表实现多态
  3. OC:自动管理(ARC),但需注意循环引用
  4. 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:虚拟内存的作用?

答案要点:

  1. 地址空间隔离:每个进程有独立的虚拟地址空间
  2. 内存保护:不同段有不同的读写执行权限
  3. 按需加载:只有访问的页才映射到物理内存
  4. 共享内存:多个进程可以共享代码段的物理页
  5. 安全性: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 表的性能考虑

优势
  1. 哈希表查找:O(1) 平均时间复杂度
  2. 批量清理:对象释放时一次性清理所有 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 表如何实现?性能如何?

答案要点:

  1. 数据结构:全局哈希表,key 是对象地址,value 是 weak 指针数组
  2. 注册:weak 指针赋值时,注册到 Weak 表
  3. 清理:对象释放时,遍历 Weak 表,把所有 weak 指针置 nil
  4. 性能:哈希表查找 O(1),但大量 weak 指针时清理可能较慢
  5. 优化:系统有优化机制,实际性能通常可接受

总结:完整知识线回顾

从对象到硬件内存的完整路径

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 表的实现和性能

祝你面试顺利! 🚀

相关推荐
前端大卫14 小时前
Vue3 + Element-Plus 自定义虚拟表格滚动实现方案【附源码】
前端
却尘14 小时前
Next.js 请求最佳实践 - vercel 2026一月发布指南
前端·react.js·next.js
ccnocare14 小时前
浅浅看一下设计模式
前端
Lee川14 小时前
🎬 从标签到屏幕:揭秘现代网页构建与适配之道
前端·面试
Ticnix15 小时前
ECharts初始化、销毁、resize 适配组件封装(含完整封装代码)
前端·echarts
纯爱掌门人15 小时前
终焉轮回里,藏着 AI 与人类的答案
前端·人工智能·aigc
twl15 小时前
OpenClaw 深度技术解析
前端
崔庆才丨静觅15 小时前
比官方便宜一半以上!Grok API 申请及使用
前端
星光不问赶路人15 小时前
vue3使用jsx语法详解
前端·vue.js
天蓝色的鱼鱼15 小时前
shadcn/ui,给你一个真正可控的UI组件库
前端