iOS开发基础26-内存管理进阶:autoreleasepool、weak 底层、Tagged Pointer、Block 内存与循环引用检测
iOS 内存管理进阶:autoreleasepool、weak 底层、Tagged Pointer、Block 内存与循环引用检测
一、autoreleasepool 底层原理
1. @autoreleasepool 的编译后实现
@autoreleasepool {} 在编译后会被替换为 Runtime 函数调用:
// 源码
@autoreleasepool {
NSString *str = [NSString stringWithFormat:@"%d", 1];
}
// 编译后(等价实现)
void *poolToken = objc_autoreleasePoolPush();
NSString *str = [NSString stringWithFormat:@"%d", 1]; // 内部调用 autorelease
objc_autoreleasePoolPop(poolToken); // pop 时释放池中所有 autoreleased 对象
2. AutoreleasePoolPage 结构
自动释放池不是简单的数组,而是一个以 AutoreleasePoolPage 为节点的双向链表(栈结构):
AutoreleasePoolPage (双向链表)
├── parent: 指向前一个 page
├── child: 指向后一个 page
├── next: 指向下一个可存储对象的位置
└── 存储区: 存放 autoreleased 对象指针(每页约 4096 字节)
- 每个 page 固定大小为一页内存(4096 字节),前 56 字节存储元数据,剩余空间存储对象指针。
- 一个 page 存满后自动创建下一个 page,形成链表。
push操作在当前 page 插入一个哨兵对象(POOL_BOUNDARY),返回哨兵位置作为 token。pop操作从next位置往回逐个调用release,直到遇到哨兵对象。
3. 线程与 autoreleasepool
- 每个线程有独立的 autoreleasepool 栈,线程间互不影响。
- 主线程:RunLoop 在每次循环开始时自动
push一个 pool,循环结束时pop,释放本次循环产生的所有 autoreleased 对象。 - 子线程:默认没有 RunLoop,也不会自动创建 pool。如果子线程中产生 autoreleased 对象,必须手动包裹
@autoreleasepool,否则对象会泄漏(没有 pool 来释放它)。
// 子线程中必须手动创建 autoreleasepool
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@autoreleasepool {
for (NSInteger i = 0; i < 10000; i++) {
NSString *str = [NSString stringWithFormat:@"%ld", i];
// str 是 autoreleased,由 pool 管理
}
} // pool 销毁,所有 str 被 release
});
4. 手动创建 autoreleasepool 的场景
| 场景 | 原因 |
|---|---|
| 循环内产生大量临时对象 | 降低内存峰值,避免循环结束前对象堆积 |
| 子线程中产生 autoreleased 对象 | 子线程无自动 pool,不创建会泄漏 |
| 非主线程 RunLoop 中 | 需手动管理 pool 生命周期 |
// 循环内降低内存峰值
for (NSInteger i = 0; i < 100000; i++) {
@autoreleasepool {
UIImage *image = [UIImage imageNamed:[NSString stringWithFormat:@"img_%ld", i]];
// 处理 image...
} // 每次循环结束释放本次的 autoreleased 对象
}
没有
@autoreleasepool时,这 10 万个 autoreleased 对象会堆积到当前 RunLoop 循环结束才释放,内存峰值极高。
二、weak 引用底层实现
1. weak 表结构
weak 引用不存储在对象内部,而是存储在全局的 SideTable 中。每个 SideTable 包含:
SideTable
├── spinlock_t slock: 自旋锁,保护 weak 表和引用计数表
├── RefcountMap refcnts: 引用计数表(对象地址 → 引用计数)
└── weak_table_t weak_table: weak 引用表
├── weak_entry_t *weak_entries: 哈希桶数组
├── num_entries: 条目数量
└── mask: 哈希掩码
└── 每个 weak_entry_t 存储一个对象的所有 weak 指针地址
2. weak 引用的注册与读取
// 源码
__weak Person *weakPerson = person;
// 编译后调用 Runtime 函数
Person *weakPerson = nil;
objc_initWeak(&weakPerson, person);
// 内部调用 objc_storeWeak,在 weak 表中注册 weakPerson 的地址与 person 的映射
读取 weak 引用时:
// 源码
Person *p = weakPerson;
// 编译后
Person *p = objc_loadWeakRetained(&weakPerson);
// 内部:从 weak 表查找对象,执行 retain + autorelease,返回对象
// 如果对象已销毁,weakPerson 已被置 nil,返回 nil
weak 引用的读取涉及锁操作和引用计数操作,性能比 strong 引用慢。高频访问的 weak 引用建议先赋值给 strong 局部变量(weak-strong dance)。
3. 对象销毁时 weak 置 nil 流程
对象销毁时,Runtime 遍历 weak 表,将所有指向该对象的 weak 指针置 nil:
对象销毁流程中 weak 清理部分:
objc_destructInstance
→ weak_clear_no_lock(weak_table, obj)
1. 根据对象地址在 weak 表中哈希查找 weak_entry
2. 遍历该 entry 中所有 weak 指针地址
3. 将每个 weak 指针的值置为 nil
4. 从 weak 表中移除该 entry
这就是 weak 引用能自动置 nil 的底层原因。unsafe_unretained 不经过 weak 表,对象销毁后指针仍指向已释放内存,成为野指针。
4. weak 引用的注意事项
| 特性 | 说明 |
|---|---|
| 线程安全 | weak 表操作有自旋锁保护,多线程安全 |
| 性能开销 | 注册、读取、销毁都有锁和哈希查找开销,比 strong 慢 |
| 只能修饰对象 | weak 只能修饰 OC 对象类型,不能修饰基本数据类型 |
| 不能修饰非 OC 对象 | CoreFoundation 对象不支持 weak(用 __unsafe_unretained) |
三、Tagged Pointer 小对象优化
1. 什么是 Tagged Pointer
Tagged Pointer 是一种小对象优化技术:对于某些小型值对象(如 NSNumber、短 NSString、NSDate),不分配堆内存,而是将数据直接编码在指针本身中。指针的特殊位标记它是 Tagged Pointer 而非真实对象指针。
普通对象指针(arm64):
0x0000000100123456 → 指向堆内存中的对象
Tagged Pointer(arm64):
最低位 = 1(标记为 Tagged Pointer)
第 1-3 位 = 类型标记(NSNumber / NSString / NSDate / UIColor 等)
剩余位 = 实际数据(如数值、字符串内容)
2. 支持 Tagged Pointer 的类
| 类 | 说明 |
|---|---|
NSNumber |
小整数(如 @1、@YES)直接存在指针中 |
NSString |
短字符串(NSTaggedPointerString,通常 ≤ 10 字符) |
NSDate |
某些时间值 |
UIColor |
标准颜色(如 redColor、blueColor) |
NSIndexPath |
小索引路径 |
3. Tagged Pointer 的特性
NSNumber *n1 = @1;
NSNumber *n2 = @1;
NSLog(@"%p %p", n1, n2); // 地址相同,且最低位为 1(Tagged Pointer 标记)
NSLog(@"%@", @([n1 retainCount])); // 引用计数无意义,输出很大的值或 0
| 特性 | 说明 |
|---|---|
| 不分配堆内存 | 数据存在指针中,无需 malloc/free |
| 引用计数无意义 | objc_retain/objc_release 对 Tagged Pointer 直接返回,不操作引用计数 |
| 访问极快 | 无需解引用指针,直接从指针位域解码数据 |
| 内存泄漏检测失效 | Leaks 工具无法检测 Tagged Pointer(因为不分配堆内存) |
| 无 weak 引用 | Tagged Pointer 不经过 weak 表,__weak 指向它时直接保留指针值 |
4. 对内存管理的影响
Tagged Pointer 对象的存在意味着:
- 引用计数为 0 不代表所有对象都会调用
dealloc(Tagged Pointer 不经过 dealloc 流程)。 retainCount的值对 Tagged Pointer 无意义,不要依赖retainCount调试内存。- 短字符串和小数字的创建/销毁几乎零成本,无需担心频繁创建的性能问题。
四、Block 的内存管理
1. Block 的三种类型
| 类型 | 存储位置 | 触发条件 | copy 行为 |
|---|---|---|---|
NSGlobalBlock |
全局区(类似单例) | 不捕获外部变量 | copy 后仍是自身,无变化 |
NSStackBlock |
栈上 | 捕获了外部变量 | copy 后变为 NSMallocBlock(迁移到堆上) |
NSMallocBlock |
堆上 | 对 StackBlock 执行 copy 后 | copy 后引用计数 +1 |
// NSGlobalBlock:不捕获外部变量
void (^globalBlock)(void) = ^{
NSLog(@"global");
};
NSLog(@"%@", NSStringFromClass([globalBlock class])); // __NSGlobalBlock__
// NSStackBlock:捕获外部变量(MRC 下明显,ARC 下可能自动 copy)
int value = 10;
void (^stackBlock)(void) = ^{
NSLog(@"%d", value); // 捕获了 value
};
// MRC 下是 __NSStackBlock__,ARC 下作为强引用时自动 copy 为 __NSMallocBlock__
ARC 下的自动 copy:ARC 环境中,Block 被强引用(赋值给 strong 属性、作为参数传递、被 copy 修饰的属性持有)时,编译器会自动插入
copy,将 StackBlock 迁移到堆上。因此 ARC 下通常看到的是NSMallocBlock或NSGlobalBlock,很少看到NSStackBlock。
2. Block 捕获变量的内存管理
@interface Person : NSObject
@property (nonatomic, copy) NSString *name;
@end
// Block 捕获 OC 对象
Person *person = [[Person alloc] init];
person.name = @"Tom";
void (^block)(void) = ^{
NSLog(@"%@", person.name); // Block 强引用 person(引用计数 +1)
};
// person 引用计数:2(外部变量 + Block 内部)
block();
// block 销毁时,内部强引用释放,person 引用计数 -1
| 捕获类型 | Block 对其引用 | 说明 |
|---|---|---|
| 基本数据类型(int/BOOL) | 值拷贝 | Block 内部保存值的副本,外部修改不影响内部 |
OC 对象(__strong) |
强引用 | Block 强引用对象,引用计数 +1 |
OC 对象(__weak) |
弱引用 | Block 弱引用对象,不增加引用计数 |
__block 修饰的变量 |
强引用(包装为结构体) | 可在 Block 内外读写同一变量 |
3. __block 底层实现
__block 修饰的变量可以在 Block 内部修改,其底层是将变量包装为一个 __Block_byref 结构体:
// 源码
__block int count = 0;
void (^block)(void) = ^{
count = 10; // 修改 __block 变量
};
block();
NSLog(@"%d", count); // 10
// 编译后等价结构
struct __Block_byref_count_0 {
void *__isa;
__Block_byref_count_0 *__forwarding; // 指向自身(堆上拷贝后指向堆上的副本)
int __flags;
int __size;
int count; // 实际变量值
};
struct __main_block_impl_0 {
struct __block_impl impl;
struct __main_block_desc_0 *Desc;
__Block_byref_count_0 *count; // Block 持有 __block 结构体指针
};
__block变量被包装为__Block_byref结构体,存储在堆上。- Block 和外部作用域都持有这个结构体的指针,实现读写共享。
__forwarding指针确保栈上和堆上的副本访问的是同一个值(Block copy 到堆上时,__forwarding指向堆上的副本)。__block修饰的 OC 对象默认是强引用(ARC 下),需注意循环引用。
4. Block 循环引用
// 循环引用:self 强引用 block,block 强引用 self
@interface ViewController ()
@property (nonatomic, copy) void (^completionBlock)(void);
@end
@implementation ViewController
- (void)setup {
self.completionBlock = ^{
[self doSomething]; // Block 强引用 self → 循环引用!
};
}
- (void)doSomething {}
@end
解决方案:Weak-Strong Dance
__weak typeof(self) weakSelf = self;
self.completionBlock = ^{
__strong typeof(weakSelf) strongSelf = weakSelf;
if (!strongSelf) return; // self 已释放,直接返回
[strongSelf doSomething];
};
如果 Block 内需要多次访问 self 或有异步嵌套,使用
__strong持有 self 防止执行过程中 self 被释放;如果只需一次访问,直接用weakSelf即可。
五、NSTimer / CADisplayLink 内存泄漏完整方案
1. 问题根源
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// timer 强引用 target=self,RunLoop 强引用 timer
// self 强引用 timer → 循环引用!self 永远无法释放
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
}
- (void)tick {}
- (void)dealloc {
[self.timer invalidate]; // 永远不会执行,因为 self 无法释放
}
@end
引用链:RunLoop → NSTimer → target(self) → NSTimer,形成闭环。
CADisplayLink有完全相同的问题(RunLoop → CADisplayLink → target(self) → CADisplayLink)。
2. 方案一:NSProxy 弱引用代理
NSProxy 是一个抽象基类,专门用于消息转发。创建一个弱引用 target 的 Proxy,timer 强引用 Proxy,Proxy 弱引用 target,打破循环:
// WeakProxy.h
@interface WeakProxy : NSProxy
@property (nonatomic, weak, readonly) id target;
+ (instancetype)proxyWithTarget:(id)target;
@end
// WeakProxy.m
@implementation WeakProxy
+ (instancetype)proxyWithTarget:(id)target {
WeakProxy *proxy = [WeakProxy alloc];
proxy->_target = target;
return proxy;
}
// 消息转发:将消息转发给 target
- (id)forwardingTargetForSelector:(SEL)selector {
return _target;
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {
return [_target methodSignatureForSelector:sel];
}
- (void)forwardInvocation:(NSInvocation *)invocation {
[invocation invokeWithTarget:_target];
}
@end
使用:
// timer 强引用 proxy,proxy 弱引用 self,无循环引用
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
target:[WeakProxy proxyWithTarget:self]
selector:@selector(tick)
userInfo:nil
repeats:YES];
业界优秀实现:YYWeakProxy(ibireme 开源),比上述基础实现更完善,处理了
respondsToSelector:、协议等边界情况。
3. 方案二:iOS 10+ Block-based Timer
iOS 10 引入了 Block 形式的 timer,target 不再是 self,而是 Block 内部弱引用 self:
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0
repeats:YES
block:^(NSTimer * _Nonnull timer) {
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf tick];
}];
Block-based timer 的 target 是 timer 内部的一个 block 拷贝对象,不再直接强引用 self。Block 内用
__weak引用 self,打破循环引用。但仍需在适当时候调用invalidate停止 timer。
4. 方案三:手动 invalidate(适用于可控生命周期)
在视图消失时主动 invalidate,打破 RunLoop 对 timer 的强引用:
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated];
[self.timer invalidate];
self.timer = nil;
}
此方案适用于视图消失时确实需要停止 timer 的场景。如果视图消失后仍需 timer 运行(如后台播放),则不适用。
5. invalidate 的注意事项
| 注意点 | 说明 |
|---|---|
| 必须在注册的 RunLoop 线程调用 | invalidate 必须在 timer 注册的那个 RunLoop 所在线程调用,否则可能不生效 |
| 调用后 timer 立即失效 | invalidate 后 timer 从 RunLoop 移除,强引用解除 |
| 重复调用安全 | 对已 invalidate 的 timer 再次调用无副作用 |
| 必须赋值为 nil | invalidate 后将属性置 nil,解除 self 对 timer 的强引用 |
六、dealloc 底层销毁流程
对象引用计数归 0 时,Runtime 执行完整的销毁流程:
引用计数归 0
↓
objc_release → _objc_rootRelease → 引用计数 -1,归 0 时调用 dealloc
↓
[obj dealloc](开发者可重写,ARC 下自动调用 [super dealloc])
↓
_objc_rootDealloc(obj)
↓
object_dispose(obj)
↓
objc_destructInstance(obj)
├── 1. cxx_destruct
│ ├── 调用 C++ 成员变量的析构函数
│ └── 释放 __block 变量(__Block_byref 结构体)
├── 2. objc_clear_deallocating
│ └── 设置对象标记为"正在销毁",阻止新的 weak 引用注册和关联对象添加
├── 3. weak_clear_no_lock
│ └── 遍历 weak 表,将所有指向该对象的 weak 指针置 nil
└── 4. association_remove_all
└── 移除该对象的所有关联对象(objc_setAssociatedObject 设置的)
↓
free(obj) → 释放堆内存
各阶段说明
| 阶段 | 说明 |
|---|---|
cxx_destruct |
由编译器自动生成,调用所有 C++ 成员变量的析构函数,并释放 __block 变量。ARC 下成员变量的 release 也在此阶段完成 |
objc_clear_deallocating |
标记对象正在销毁,此时如果有新的 weak 引用指向该对象会直接置 nil,添加关联对象会被忽略 |
weak_clear_no_lock |
核心步骤:遍历 weak 表中该对象的所有 weak 指针,逐个置 nil。这是 weak 自动置 nil 的底层实现 |
association_remove_all |
移除所有通过 objc_setAssociatedObject 设置的关联对象,并执行对应的 release/copy 清理 |
free |
最终释放对象占用的堆内存 |
dealloc 的执行顺序:子类的
dealloc先执行(ARC 下自动调用[super dealloc]),然后父类的dealloc依次执行,最后到NSObject的dealloc触发_objc_rootDealloc进入底层销毁流程。
七、循环引用检测与内存分析工具
1. Instruments Leaks
Xcode 自带的内存泄漏检测工具:
- 打开方式:Xcode → Product → Profile → 选择 Leaks → 启动录制
- 原理:定时扫描堆内存,查找没有被任何根对象引用的对象(即泄漏对象)
- 使用技巧:
- 进入可疑页面后操作,再退出,观察是否有对象未释放
- Leaks 检测到泄漏后,点击可查看引用历史(Retain/Release 调用栈)
- 配合 Allocations 工具查看对象的创建和销毁情况
2. FBRetainCycleDetector
Facebook 开源的循环引用检测库,运行时自动检测循环引用:
// 仅在 Debug 模式下启用
#ifdef DEBUG
#import <FBRetainCycleDetector/FBRetainCycleDetector.h>
FBRetainCycleDetector *detector = [FBRetainCycleDetector new];
[detector addCandidate:self];
NSSet *cycles = [detector findRetainCycles];
NSLog(@"检测到循环引用: %@", cycles);
#endif
- 原理:从候选对象出发,深度遍历强引用图,查找回到自身的路径(即循环)
- 优点:运行时检测,能定位到具体的循环引用链
- 缺点:有性能开销,仅建议 Debug 模式使用
3. MLeaksFinder
腾讯开源的内存泄漏检测库,无需手动操作,自动检测 UIViewController 和 UIView 的泄漏:
- 集成后,
UIViewControllerpop/dismiss 后会自动检测该控制器及其子视图是否在 2 秒后释放 - 检测到泄漏时弹出 Alert 提示,并打印泄漏对象的信息
- 可配合
FBRetainCycleDetector进一步定位循环引用链 - 零侵入,Debug 模式自动生效,Release 模式自动禁用
4. Allocations
Instruments 中的内存分配分析工具:
- 跟踪所有对象的创建、存活、销毁
- 可按类名筛选,查看特定类的实例数量和内存占用
- Mark Generation:标记时间点,对比两个时间点之间的内存增长,定位泄漏
- 配合
#import <Foundation/Foundation.h>的NSStringFromClass调试
5. 调试辅助代码
// 在 dealloc 中打印,确认对象是否释放
- (void)dealloc {
NSLog(@"%@ dealloc", NSStringFromClass([self class]));
}
// 检测指定类的实例数量(Debug 辅助)
#ifdef DEBUG
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
Class class = [self class];
SEL originalSel = @selector(init);
// 通过 Method Swizzling 统计 init 和 dealloc 次数
});
}
#endif
八、Swift 内存管理补充
1. 值类型与引用类型
| 类型 | 存储位置 | 内存管理 | 示例 |
|---|---|---|---|
| 值类型(struct/enum) | 栈上 | 无需引用计数,作用域结束自动销毁 | Int、String、Array、Dictionary、自定义 struct |
| 引用类型(class) | 堆上 | ARC 引用计数管理 | UIViewController、自定义 class |
// 值类型:拷贝,互不影响
struct Point { var x: Int; var y: Int }
var p1 = Point(x: 1, y: 2)
var p2 = p1 // 值拷贝
p2.x = 10
print(p1.x) // 1,p1 不受影响
// 引用类型:共享,互相影响
class Person { var name: String; init(name: String) { self.name = name } }
let person1 = Person(name: "Tom")
let person2 = person1 // 引用拷贝,指向同一对象
person2.name = "Jerry"
print(person1.name) // Jerry,person1 被影响
2. Copy-on-Write(写时复制)
Swift 的标准库值类型(Array、String、Dictionary、Set)实现了写时复制优化:
var array1 = [1, 2, 3]
var array2 = array1 // 此时不复制,共享底层存储
array2.append(4) // 修改时才真正复制底层存储(copy-on-write)
// array1 = [1, 2, 3],array2 = [1, 2, 3, 4]
- 赋值时不立即复制,共享底层存储(引用计数管理)
- 修改时检查引用计数,如果被多个变量共享则复制一份再修改
- 大幅优化值类型的性能和内存占用
3. @escaping 与非逃逸闭包
| 类型 | 说明 | 内存管理 |
|---|---|---|
| 非逃逸闭包(默认) | 闭包在函数返回前执行完毕,不会被存储 | 无需循环引用考虑,闭包执行完即释放 |
逃逸闭包(@escaping) |
闭包会被存储到属性或变量中,函数返回后才执行 | 需注意循环引用,使用 [weak self] |
// 非逃逸闭包:函数返回前执行,无需 weak
func syncTask(completion: () -> Void) {
completion() // 同步执行
}
// 逃逸闭包:被存储,函数返回后执行,需要 weak
var storedCompletion: (() -> Void)?
func asyncTask(completion: @escaping () -> Void) {
storedCompletion = completion // 闭包被存储,逃逸了
}
// 使用逃逸闭包时注意循环引用
class ViewController {
func loadData() {
asyncTask { [weak self] in // [weak self] 避免循环引用
self?.updateUI()
}
}
func updateUI() {}
}
4. async/await 与 Task 的内存管理
Swift 并发中的 Task 闭包默认强引用捕获的对象:
class ViewController {
func fetchData() {
// Task 闭包强引用 self,Task 被系统持有直到完成
// 如果 Task 永不完成(如无限等待),self 会泄漏
Task {
let data = await API.fetch()
self.updateUI(data) // 强引用 self
}
}
func fetchDataSafe() {
Task { [weak self] in // 显式 weak 引用
let data = await API.fetch()
self?.updateUI(data)
}
}
func updateUI(_ data: Data) {}
}
Task的闭包是逃逸闭包,被系统的并发调度器持有直到任务完成。对于可能长时间运行或取消的 Task,建议使用[weak self]。
九、总结
- autoreleasepool:以
AutoreleasePoolPage双向链表实现,每个线程独立,主线程由 RunLoop 自动管理,子线程需手动创建;循环内大量临时对象时手动@autoreleasepool降低内存峰值。 - weak 底层:weak 引用存储在全局
SideTable的weak_table_t中,对象销毁时weak_clear_no_lock遍历将所有 weak 指针置 nil;读写有锁开销,高频访问建议 weak-strong dance。 - Tagged Pointer:小对象(NSNumber、短 NSString、UIColor 等)不分配堆内存,数据编码在指针中,引用计数无意义,访问极快。
- Block 内存:三种类型(Global/Stack/Malloc),ARC 下强引用时自动 copy 到堆;
__block变量包装为__Block_byref结构体实现读写共享;Block 循环引用用 weak-strong dance 解决。 - NSTimer 泄漏:根源是 RunLoop→timer→target(self)→timer 循环引用;解决方案包括 NSProxy 弱引用代理(YYWeakProxy)、iOS 10+ Block-based timer、viewWillDisappear 中 invalidate。
- dealloc 流程:
dealloc→_objc_rootDealloc→object_dispose→objc_destructInstance(cxx_destruct → clear_deallocating → weak_clear_no_lock → association_remove_all)→free。 - 检测工具:Instruments Leaks/Allocations、FBRetainCycleDetector、MLeaksFinder,配合 dealloc 打印调试。
- Swift 补充:值类型在栈上无需引用计数,引用类型在堆上用 ARC;写时复制优化标准库值类型;
@escaping闭包和Task需注意循环引用,用[weak self]。

浙公网安备 33010602011771号