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

祝你面试顺利! 🚀

相关推荐
Alsn862 小时前
26.IDEA 专业版中创建简单的 Web 项目并打包部署到本地Tomcat 9
前端·tomcat·intellij-idea
霍理迪2 小时前
HTML行内块标签——img、表单、音视频标签
前端·html
小小前端_我自坚强2 小时前
边缘函数 (Edge Functions)详解
前端
幼儿园技术家2 小时前
Hydration Mismatch 原理详解:SSR 项目中最容易踩的坑
前端
June bug2 小时前
【Vue】EACCES: permission denied 错误
前端·javascript·vue.js
陈随易2 小时前
PostgreSQL v18发布,新增AIO uuidv7 OAuth等功能
前端·后端·程序员
一只小阿乐2 小时前
react 中的组件性能优化
前端·javascript·react.js·react组件性能优化
柯南二号2 小时前
【大前端】【iOS】iOS 真实项目可落地目录结构方案
前端·ios
肉清2 小时前
linux自用命令
linux·服务器·前端