iOS 内存管理:ARC & MRC 深度解析

0 阅读18分钟
  1. 内存管理基础概念

1.1 引用计数(Reference Counting)

Objective-C 使用引用计数(retain count)来管理对象生命周期:

  • 每个对象内部维护一个引用计数器
  • 当有持有者持有该对象时,引用计数 +1
  • 当持有者放弃持有时,引用计数 -1
  • 当引用计数降为 0 时,系统自动调用 dealloc,释放内存
对象A 引用计数 = 2
  ├── 变量 x 持有(retain)
  └── 数组 arr 持有(retain)

变量 x = nil → 引用计数 = 1
arr 释放     → 引用计数 = 0 → dealloc 被调用 → 内存释放

1.2 堆 vs 栈

区域存放内容管理方式
栈(Stack)局部变量、基本类型(int/float/struct)、指针本身自动,函数返回即释放
堆(Heap)OC 对象(NSObject 子类)需要引用计数管理

重点:OC 对象总在堆上,指针在栈上。NSObject *obj 中,obj 变量在栈上,它指向的对象在堆上。

1.3 对象的生命周期

alloc/new/copy/mutableCopy  →  创建对象  →  retain count = 1
retain                       →  引用计数 +1
release                      →  引用计数 -1
autorelease                  →  延迟 release(在 pool drain 时执行)
retain count == 0            →  dealloc  →  内存回收

2. MRC(手动引用计数)

MRC(Manual Reference Counting)是 ARC 出现之前 iOS 开发使用的内存管理模式,由程序员手动调用 retain / release / autorelease

2.1 核心方法

方法作用retain count 变化
alloc / new创建对象+1(初始为1)
copy / mutableCopy复制对象+1(新对象初始为1)
retain持有对象+1
release放弃持有-1
autorelease延迟 releasepool drain 时 -1
retainCount查询当前引用计数
dealloc引用计数为0时系统调用

2.2 MRC 黄金法则

谁创建,谁释放。谁持有,谁释放。

NSObject *obj = [[NSObject alloc] init]; // retain count = 1
[obj retain];                            // retain count = 2
[obj release];                           // retain count = 1
[obj release];                           // retain count = 0 → dealloc

2.3 MRC 下的属性 setter 实现

// Person.h
@interface Person : NSObject {
    NSString *_name;
}
@property (nonatomic, retain) NSString *name;
@end

// Person.m
@implementation Person

// MRC retain 属性的标准 setter
- (void)setName:(NSString *)name {
    if (_name != name) {        // 防止自赋值:如果 name 就是 _name,
                                // 先 release 会导致对象被销毁
        [_name release];        // 释放旧值
        _name = [name retain];  // 持有新值
    }
}

- (NSString *)name {
    return _name;
}

// dealloc:必须释放所有持有的对象
- (void)dealloc {
    [_name release];
    _name = nil;
    [super dealloc];  // MRC 必须调用!编译器不会帮你加
}

@end

自赋值问题详解

// 假设 _name 和 name 指向同一个对象

// ❌ 错误写法1:没有判断自赋值
- (void)setName:(NSString *)name {
    [_name release];        // 如果 name == _name,对象被销毁!
    _name = [name retain];  // 访问已销毁对象 → 崩溃或数据错误
}

// ❌ 错误写法2:顺序颠倒
- (void)setName:(NSString *)name {
    _name = [name retain];  // 先 retain 再 release 没问题(自赋值场景)
    [_name release];        // 但这里 _name 已经是新值了,release 的是新值!
}

// ✅ 正确写法:先判断,再 release 旧,再 retain 新
- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        _name = [name retain];
    }
}

2.4 copy 属性

@property (nonatomic, copy) NSString *title;

- (void)setTitle:(NSString *)title {
    if (_title != title) {
        [_title release];
        _title = [title copy];  // copy 而非 retain,得到不可变副本
    }
}

为什么 NSString 属性用 copy 而不是 retain?

调用者可能传入 NSMutableString。如果用 retain,外部修改 mutableString 会悄悄改变你的属性值,违反封装。用 copy 得到一个不可变副本,安全。

2.5 MRC 常见错误

过度释放(Over-release)

NSString *s = [[NSString alloc] initWithString:@"hello"]; // retain count = 1
[s release]; // = 0 → dealloc
[s release]; // ❌ 野指针访问,EXC_BAD_ACCESS 崩溃

内存泄漏(Leak)

NSString *s = [[NSString alloc] initWithString:@"hello"]; // retain count = 1
// 函数结束,忘记 release
// s 指针销毁,但对象 retain count 仍为 1,永远泄漏

悬空指针(Dangling Pointer)

NSString *a = [[NSString alloc] initWithString:@"hello"];
NSString *b = a;   // b 没有 retain,只是复制了指针值
[a release];       // retain count = 0,对象被销毁
NSLog(@"%@", b);   // ❌ b 是悬空指针,崩溃!

3. ARC(自动引用计数)

ARC(Automatic Reference Counting)由 Clang 编译器在编译期自动插入 retain/release/autorelease 调用,运行时行为与 MRC 完全一致。

3.1 ARC 的本质

ARC 不是 GC(垃圾回收) ,区别如下:

特性ARCGC
工作时机编译期插入代码运行时扫描
回收时机引用计数为0立即回收GC 决定,不确定
性能影响几乎无额外开销Stop-The-World 暂停
循环引用需要 weak 手动解决GC 可处理
// 你写的 ARC 代码
- (void)example {
    NSObject *obj = [[NSObject alloc] init];
    NSLog(@"%@", obj);
}

// 编译器等价生成的 MRC 代码(伪代码)
- (void)example {
    NSObject *obj = [[NSObject alloc] init]; // retain count = 1
    NSLog(@"%@", obj);
    [obj release];  // 编译器插入:函数结束前自动 release → count = 0 → dealloc
}

3.2 ARC 规则(面试必背)

  1. 禁止手动调用retain / release / autorelease / retainCount / [super dealloc]
  2. 禁止使用NSAllocateObject / NSDeallocateObject
  3. C struct 中不能直接放 OC 对象指针(用 __unsafe_unretained 或改用 OC 容器)
  4. *id 与 void 强转**需要 __bridge 系列修饰符
  5. dealloc 可以重写,但不能调用 [super dealloc](编译器自动处理)

3.3 ARC 下的 dealloc

- (void)dealloc {
    // ✅ 可以做:清理 C 资源、移除通知、停止 timer
    [[NSNotificationCenter defaultCenter] removeObserver:self];
    [_timer invalidate];
    free(_cBuffer);

    // ❌ 不能调用:[super dealloc](ARC 编译器自动插入)
    // ❌ 不能调用:[_name release](ARC 自动管理 OC 对象)
}

3.4 ARC 与 MRC 混编

在同一工程中,可以对某些文件单独禁用 ARC:

  • Xcode → Build Phases → Compile Sources
  • 目标文件添加 -fno-objc-arc:该文件使用 MRC
  • 目标文件添加 -fobjc-arc:强制 ARC(用于第三方 MRC 库)

4. 引用类型

4.1 strong(强引用)

@property (nonatomic, strong) NSObject *obj;
// 等价于 MRC 的 retain
// 持有对象,引用计数 +1
// 只要有 strong 引用,对象不会被释放
// 默认修饰符(OC 对象不写修饰符时等价于 strong)

4.2 weak(弱引用)

@property (nonatomic, weak) id<SomeDelegate> delegate;
// 不持有对象,引用计数不变
// 对象释放后,weak 指针自动置 nil(Zeroing Weak Reference)
// 常用于:delegate、父对象引用、避免循环引用

weak 的实现原理:

Runtime 维护全局的 weak_table_t(哈希表),key = 对象地址,value = 指向该对象的所有 weak 指针集合。

对象 A 被释放(retain count = 0)
  ↓ 进入 dealloc 流程
  ↓ objc_object::clearDeallocating()
  ↓ 查询 weak_table,找到所有指向 A 的 weak 指针
  ↓ 将它们全部置为 nil
  ↓ 从 weak_table 中移除记录
  ↓ 内存释放

weak 的性能开销:

  • 注册 weak 引用时:哈希表插入,O(1) 均摊
  • 读取 weak 时:需要加锁(防止多线程并发),有一定开销
  • 对象销毁时:遍历并置 nil,与 weak 引用数量成正比

4.3 unsafe_unretained

@property (nonatomic, unsafe_unretained) id delegate;
// 不持有对象,引用计数不变
// 对象释放后,指针【不会】自动置 nil → 悬空指针 → 访问崩溃
// 使用场景极少,仅在需要极致性能且能保证对象生命周期时使用

weak vs unsafe_unretained:
都是弱引用,区别在于对象释放后:

  • weak:指针自动置 nil,安全
  • unsafe_unretained:指针变悬空指针,不安全

4.4 assign

@property (nonatomic, assign) NSInteger age;
@property (nonatomic, assign) CGFloat price;
@property (nonatomic, assign) BOOL isLoading;

// 用于基本数据类型(int/float/BOOL/CGRect/CGSize 等)
// 对 OC 对象使用 assign = unsafe_unretained,对象释放后指针不置 nil
// ❌ 不要对 OC 对象使用 assign(除非清楚自己在做什么)

4.5 copy

@property (nonatomic, copy) NSString *title;
@property (nonatomic, copy) void (^completion)(void);

// 调用 copy 方法得到副本并持有副本(引用计数 +1)
// NSString:防止传入 NSMutableString 被外部修改
// Block:必须 copy,Block 默认在栈上,copy 后移到堆上

Block 为什么必须 copy?

// Block 有三种存储类型:
// __NSGlobalBlock__:无捕获外部变量,存在全局段
// __NSStackBlock__:捕获了外部变量,在 MRC 下默认在栈上
// __NSMallocBlock__:copy 后在堆上(ARC 下赋值给 strong/copy 属性时自动 copy)

// ⚠️ MRC 下危险
typedef void (^MyBlock)(void);
MyBlock getBlock() {
    int x = 10;
    return ^{ NSLog(@"%d", x); }; // 栈 Block,函数返回后栈帧销毁,Block 失效
}

// ✅ ARC 下,赋值给 strong 属性时编译器自动 copy 到堆
// 但明确写 copy 更清晰,是业界最佳实践

4.6 修饰符总结

修饰符retain count对象释放后指针适用场景
strong+1继续持有默认,拥有关系
weak不变自动置 nildelegate、避免循环引用
unsafe_unretained不变悬空指针极少使用
assign不变基本类型无影响 / OC对象变悬空指针基本数据类型
copy+1(副本)继续持有副本NSString、Block

5. 循环引用(Retain Cycle)

5.1 什么是循环引用

两个(或多个)对象互相持有,引用计数永远不为 0,内存永远无法释放。

A ──strong──> B
B ──strong──> A

A.retainCount >= 1B 持有它)
B.retainCount >= 1A 持有它)
→ 两者都不会被释放 → 内存泄漏

5.2 Delegate 循环引用

// ❌ ViewController strong 持有 View,View 也 strong 持有 VC
@interface MyView : UIView
@property (nonatomic, strong) id<MyViewDelegate> delegate; // ❌
@end

// ✅ delegate 用 weak 打破循环
@interface MyView : UIView
@property (nonatomic, weak) id<MyViewDelegate> delegate; // ✅
@end

5.3 Block 循环引用(最高频考点)

// ❌ 经典循环引用
@interface MyVC : UIViewController
@property (nonatomic, copy) void (^block)(void);
@end

@implementation MyVC
- (void)viewDidLoad {
    self.block = ^{
        NSLog(@"%@", self.title); // Block 捕获 self(strong 引用)
        // self ──strong──> block(self.block 属性)
        // block ──strong──> self(捕获变量)
        // 循环引用!dealloc 永远不调用
    };
}
@end

解法1:__weak(推荐)

__weak typeof(self) weakSelf = self;
self.block = ^{
    // weakSelf 不持有 self,不增加引用计数
    NSLog(@"%@", weakSelf.title);
};

解法2:weakSelf + strongSelf(推荐,多行访问时)

__weak typeof(self) weakSelf = self;
self.block = ^{
    // 为什么要 strongSelf?
    // weakSelf 是 weak 引用,可能在 block 执行中途被置为 nil
    // 例如:第一行访问时非 nil,线程切换后 self 释放,第二行访问时变 nil
    // strongSelf 在 block 执行期间强持有 self,确保整个 block 执行完
    __strong typeof(weakSelf) strongSelf = weakSelf;
    if (!strongSelf) return; // self 已经释放,提前退出
    
    NSLog(@"%@", strongSelf.title);
    [strongSelf doSomething];
}; // strongSelf 出作用域,release,block 执行完后 self 可以正常释放

解法3:__block(打破循环,MRC 时代遗留)

// ARC 下 __block 变量本身是 strong 的,不推荐用这个方式
__block MyVC *blockSelf = self; // 注意:这里仍然是强引用!
self.block = ^{
    NSLog(@"%@", blockSelf.title);
    blockSelf = nil; // 手动赋 nil 打破循环,但只在 block 执行后才能释放
};
// 问题:如果 block 从未执行,循环引用依然存在

5.4 NSTimer 循环引用

// ❌ 循环引用
@interface MyVC : UIViewController
@property (nonatomic, strong) NSTimer *timer;
@end

@implementation MyVC
- (void)viewDidLoad {
    // NSTimer 内部 strong retain target(self)
    // self ──strong──> timer(self.timer 属性)
    // timer ──strong──> self(target 参数)
    // → dealloc 永远不调用!
    self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0                                                 target:self                                               selector:@selector(tick)                                               userInfo:nil                                                repeats:YES];
}
@end

解法1:在合适时机主动 invalidate(最简单)

// viewWillDisappear 或 viewDidDisappear 中
- (void)viewWillDisappear:(BOOL)animated {
    [super viewWillDisappear:animated];
    [self.timer invalidate]; // timer 释放对 self 的 retain
    self.timer = nil;
}

解法2:iOS 10+ Block API + weakSelf

__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
                                            repeats:YES
                                              block:^(NSTimer *t) {
    [weakSelf tick]; // weakSelf 不持有 self
}];
// timer ──strong──> block,block ──weak──> self
// self 可以正常释放,dealloc 里 invalidate timer 即可

解法3:NSProxy 中间代理

// WeakProxy:持有一个 weak 引用指向真正的 target
// timer ──strong──> proxy ──weak──> self
// self 释放后,proxy 收到消息,直接让 timer invalidate
@interface WeakProxy : NSProxy
@property (nonatomic, weak) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end

self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0                                             target:[WeakProxy proxyWithTarget:self]
                                           selector:@selector(tick)
                                           userInfo:nil
                                            repeats:YES];

5.5 检测循环引用的工具

工具使用场景特点
Instruments → Leaks运行时自动检测泄漏,有 backtrace
Instruments → Allocations运行时对比进入/退出页面前后对象数量
Xcode Memory Graph调试时可视化引用关系图,直观
FBRetainCycleDetector测试/运行时Meta 开源,代码集成,自动报告

6. autorelease 与 AutoreleasePool

6.1 autorelease 是什么

autorelease 不是立即释放,而是把对象注册到当前 autorelease pool,等 pool drain 时统一 release。

// MRC
NSString *s = [[[NSString alloc] initWithString:@"hello"] autorelease];
// s 注册到 pool,函数结束不崩溃,等 pool drain 时才 release

// 常见返回 autorelease 对象的方法(非 alloc/new/copy/mutableCopy 开头)
NSString *s2 = [NSString stringWithFormat:@"hello %d", 1]; // autorelease
NSArray  *arr = [NSArray arrayWithObject:@"a"];             // autorelease

6.2 命名约定(持有规则)

方法名前缀返回对象持有状态调用者责任
alloc / new / copy / mutableCopyretain count = 1调用者负责 release
其他所有方法autorelease 对象无需 release(pool 统一处理)

6.3 AutoreleasePool 工作机制

@autoreleasepool {
    // 进入:向当前线程的 pool 栈 push 一个新 pool
    NSString *s = [NSString stringWithFormat:@"hello %d", 1];
    // s 是 autorelease 对象,被注册到当前 pool
} // 出块:drain pool,对所有注册的对象发送 release

底层结构:

  • AutoreleasePool 是一个双向链表,每个节点是 AutoreleasePoolPage(4096 bytes)
  • 每个 page 存储一批对象指针
  • push 在 page 上标记一个 POOL_BOUNDARY(边界标记)
  • pop 从栈顶 release 所有对象,直到遇到边界

6.4 RunLoop 与 AutoreleasePool 的关系

主线程的 RunLoop 在每次循环:

  • kCFRunLoopEntry(进入 RunLoop):创建 autorelease pool
  • kCFRunLoopBeforeWaiting(即将休眠):drain 旧 pool,创建新 pool
  • kCFRunLoopExit(退出 RunLoop):drain pool

这是为什么大多数代码不手动写 @autoreleasepool 也能正常运行的原因。

6.5 需要手动添加 @autoreleasepool 的场景

场景1:子线程

dispatch_async(dispatch_get_global_queue(0, 0), ^{
    // ⚠️ 子线程没有默认 RunLoop,也没有 autorelease pool
    // autorelease 对象会泄漏(直到线程结束才由系统清理)
    @autoreleasepool {
        for (int i = 0; i < 10000; i++) {
            NSString *s = [NSString stringWithFormat:@"%d", i];
            // 不加 pool:所有对象堆积到循环结束
            // 加 pool:每次 pool 出块时 drain,内存峰值稳定
        }
    }
});

场景2:大量临时对象的循环

// 不加 pool:内存持续增长到循环结束
for (int i = 0; i < 100000; i++) {
    @autoreleasepool {
        UIImage *img = [UIImage imageNamed:[NSString stringWithFormat:@"img_%d", i]];
        // 处理 img
    } // 每次迭代后立即 drain,内存峰值极低
}

7. ARC 底层原理

7.1 编译器如何插入 retain/release

Clang 基于所有权语义静态分析代码流,在以下时机插入:

  • 对象赋值给 strong 变量:插入 objc_retain
  • strong 变量超出作用域或被覆盖:插入 objc_release
  • 函数参数、返回值传递:插入必要的 retain/release
  • 跨作用域的对象:通过 objc_retainAutoreleasedReturnValue 等函数优化
// 你写的
- (void)foo {
    NSObject *a = [[NSObject alloc] init]; // +1(alloc 自带)
    NSObject *b = a;                       // b 是 strong 变量
    a = nil;
    [b doSomething];
}

// 编译器生成(伪代码)
- (void)foo {
    NSObject *a = objc_msgSend(NSObject, @selector(alloc));
    a = objc_msgSend(a, @selector(init)); // count = 1
    
    NSObject *b = objc_retain(a);  // b = a,+1,count = 2
    
    objc_storeStrong(&a, nil);     // a = nil,release 旧值,count = 1
    
    objc_msgSend(b, @selector(doSomething));
    
    objc_storeStrong(&b, nil);     // b 出作用域,release,count = 0 → dealloc
}

7.2 ARC 使用的关键 C 函数

id objc_retain(id obj);              // +1
void objc_release(id obj);           // -1
id objc_autorelease(id obj);         // 注册到 pool

// 优化版:跨函数传递时避免多余的 retain/release
id objc_retainAutoreleasedReturnValue(id obj);
id objc_autoreleaseReturnValue(id obj);

// weak 相关
id objc_initWeak(id *location, id obj);
id objc_loadWeakRetained(id *location);
void objc_destroyWeak(id *location);
void objc_storeWeak(id *location, id obj);

7.3 weak 变量的底层实现

// __weak NSObject *weakObj = obj;
// 展开后:

// 1. 赋值时
objc_initWeak(&weakObj, obj);
// → 在全局 weak_table 中注册 {obj地址 → &weakObj}

// 2. 读取 weakObj 时
NSObject *tmp = objc_loadWeakRetained(&weakObj);
// → 加锁,检查对象是否存在,存在则 retain,返回
// 使用 tmp...
objc_release(tmp);

// 3. weakObj 出作用域时
objc_destroyWeak(&weakObj);
// → 从 weak_table 移除注册

// 4. 对象 dealloc 时
// → clearDeallocating → 找到所有 weak 指针 → 全部置 nil

7.4 TaggedPointer(小对象优化)

对于 NSNumber、短字符串(≤7字节)、NSDate 等小对象,苹果使用 Tagged Pointer 技术:

  • 指针的最低位(或最高位,ARM64)置 1 作为标识
  • 值直接编码在指针本身,不在堆上分配内存
  • retain/release 对 Tagged Pointer 是空操作(不需要引用计数)
  • 内存效率极高,访问更快
NSNumber *n1 = @42;     // Tagged Pointer,不在堆上
NSNumber *n2 = @(1e100); // 数值太大,是真正的堆对象

// 判断是否是 Tagged Pointer
BOOL isTagged = ((uintptr_t)n1 & 1) == 1; // 简化版,实际更复杂

7.5 SideTable 与引用计数存储

对象的引用计数不只存在一个地方:

isa 优化(Non-pointer isa): 在 64 位系统上,isa 指针不只是指向 Class,还内嵌了引用计数(低位部分)和其他标志位。

isa(64位)
bits[0]    : nonpointer(=1 表示非原始指针,内嵌信息)
bits[1]    : has_assoc(有关联对象)
bits[2]    : has_cxx_dtor(有 C++ 析构或 ObjC dealloc)
bits[3-35] : class pointer(类指针)
bits[36-41]: extra_rc(引用计数-1,最多存 64-1 = 63)
bits[42]   : has_sidetable_rc(引用计数溢出时,存入 SideTable)
...

当 extra_rc 溢出时,引用计数会溢出到全局的 SideTable(哈希表,key=对象地址,value=溢出引用计数)。


8. 常见内存问题与排查

8.1 内存泄漏(Memory Leak)

现象: 内存持续增长,进入/退出某个界面内存不回落

常见原因:

  • 循环引用(最常见)
  • delegate 使用 strong
  • Block 捕获 self 未使用 weakSelf
  • NSTimer 未 invalidate
  • C 资源(malloc/CoreFoundation/CF对象)未手动释放
  • 通知未 removeObserver(iOS 9 以下)

排查方法:

Xcode → Debug → Memory Graph Debugger
→ 查看是否有不应存在的对象
→ 点击对象看引用链,找到循环

8.2 野指针(Dangling Pointer / EXC_BAD_ACCESS)

现象: EXC_BAD_ACCESS,访问已释放内存

常见原因:

  • MRC 下 over-release
  • unsafe_unretained 指针指向已释放对象
  • 多线程同时 release 同一对象
  • C 指针使用后未置 NULL

排查方法:

Xcode Scheme → Diagnostics → Enable Zombie Objects
→ 对象被释放后变成 NSZombie,再次访问会输出类名
→ "message sent to deallocated instance 0x..."

8.3 内存峰值过高

现象: 短时间内内存暴涨,可能触发 Memory Warning 或被系统杀进程

常见原因:

  • 大量临时 autorelease 对象未及时 drain
  • 大图未及时释放
  • 缓存无限增长

解决方案:

  • 大循环中嵌套 @autoreleasepool
  • 使用 NSCache 替代 NSMutableDictionary 做缓存(内存压力时自动清理)
  • 图片使用 imageWithContentsOfFile: 替代 imageNamed:(不缓存)

8.4 多线程内存问题

// ❌ 线程不安全的引用计数
// 两个线程同时 release 同一对象可能导致 double-free

// ✅ ARC 下使用 atomic(默认是非原子的)
@property (atomic, strong) NSObject *obj;

// ✅ 或者用 dispatch_queue 保护
@property (nonatomic, strong) dispatch_queue_t queue;

9. 高频面试题精析

Q1:ARC 和 MRC 的区别?

答:

  • MRC 需要程序员手动调用 retain/release/autorelease 管理内存
  • ARC 由 Clang 编译器在编译期自动插入这些调用,运行时机制完全一致
  • ARC 不是 GC,底层依然是引用计数,不会产生 Stop-The-World
  • ARC 不能解决循环引用,需要 weak 手动打破

Q2:weak 和 assign 的区别?

答:

  • 对 OC 对象而言:weak 是弱引用,对象释放后自动置 nil;assign 不增加引用计数,对象释放后变悬空指针
  • assign 通常用于基本数据类型(int/BOOL/CGFloat 等),这些类型不是对象,没有引用计数概念
  • ❌ 不要用 assign 修饰 OC 对象,用 weak 代替

Q3:为什么 Block 属性用 copy?

答:

  • Block 默认可能在栈上(捕获了外部变量的情况下),函数返回后栈帧销毁,Block 失效
  • copy 后 Block 移到堆上,生命周期由引用计数管理,可以安全传递和存储
  • ARC 下赋值给 strong 属性时编译器会自动 copy,但显式写 copy 表达意图更清晰
  • Block 属性永远写 copy 是最佳实践

Q4:为什么 NSString 属性用 copy 而不是 strong?

答:
调用者可能传入 NSMutableString(它是 NSString 的子类):

  • 用 strong/retain:只是持有同一个 mutableString,外部修改它会影响你的属性值
  • 用 copy:得到一个不可变副本(NSString),外部修改原 mutableString 不影响你

注意:如果传入的本来就是 NSString,copy 操作是 O(1) 的(不可变对象 copy 返回 self),没有性能损失。

Q5:循环引用是什么?如何解决?

答:
两个或多个对象互相强引用,引用计数永远不为 0,无法释放,造成内存泄漏。

常见场景与解法:

  1. delegate:delegate 属性改为 weak
  2. Block 捕获 self:用 __weak typeof(self) weakSelf = self,多行访问再加 __strong typeof(weakSelf) strongSelf = weakSelf
  3. NSTimer:在合适时机 invalidate,或用 iOS 10+ block API + weakSelf,或用 NSProxy 中间层

Q6:autoreleasepool 什么时候用?

答:

  1. 子线程中:子线程没有默认 RunLoop,需要手动创建 autoreleasepool
  2. 大量临时对象的循环中:防止内存峰值过高,在循环体内嵌套 @autoreleasepool 让临时对象及时释放
  3. 命令行工具 / 单元测试:没有 RunLoop,需要手动管理

Q7:dealloc 里可以做什么?

答:
可以做的:

  • 移除通知观察者(removeObserver:self
  • 停止 NSTimer([_timer invalidate]
  • 释放 C 资源(free() / fclose() 等)
  • KVO 移除(removeObserver:forKeyPath:

不能做的(ARC):

  • [super dealloc](编译器自动处理)
  • [_obj release](ARC 自动处理 OC 对象)

不要做的:

  • 发送异步通知或执行耗时操作(dealloc 的线程不确定)
  • 调用 self 的其他方法(可能访问已释放成员,危险)

Q8:__weak 修饰的变量为何能在对象释放后自动置 nil?

答:
Runtime 维护一张全局的 weak_table(哈希表):

  • key:对象的内存地址
  • value:所有指向该对象的 weak 变量地址集合

当对象引用计数降为 0,进入 dealloc 流程时,Runtime 调用 clearDeallocating

  1. 在 weak_table 中找到该对象所有 weak 指针
  2. 将它们全部赋值为 nil
  3. 从 weak_table 中移除该对象的记录

所以每次读取 weak 变量时,Runtime 会加锁原子地读取,保证线程安全。

Q9:什么是 TaggedPointer?

答:
一种小对象优化技术。对于 NSNumber、短 NSString、NSDate 等数值较小的对象,苹果将值直接编码在指针中,不在堆上分配内存:

  • 指针最高位(ARM64)= 1 标识这是 Tagged Pointer
  • 其余位编码类型 + 值
  • retain/release 是空操作
  • 内存效率极高,无堆分配,无引用计数开销

Q10:如何在 ARC 中使用 CoreFoundation 对象(CFTypeRef)?

答:
CF 对象不受 ARC 管理,需要用 __bridge 系列修饰符在 OC 和 CF 之间转换:

// __bridge:不转移所有权,双方各管各的
NSString *str = (__bridge NSString *)cfStr; // CF 对象转 OC,CF 还负责 release

// __bridge_retained:OC → CF,OC 放弃所有权,CF 负责 CFRelease
CFStringRef cfStr = (__bridge_retained CFStringRef)ocStr;
// 使用后必须 CFRelease(cfStr)

// __bridge_transfer:CF → OC,CF 放弃所有权,ARC 接管
NSString *str = (__bridge_transfer NSString *)cfStr;
// 不需要 CFRelease,ARC 会 release