iOS的多线程下数据安全和内存泄漏以及工具使用监测内存泄漏
iOS的NSMutableArray的add, remove,indexof如何确保在多线程的情况下安全?
NSMutableArray
的所有读写操作(addObject:
、removeObject:
、indexOfObject:
等)本身都不是线程安全的。
要在多线程环境下安全使用,只有一条铁律:
“同一时间最多只能有一个线程在写,或者可以有多个线程同时读,但绝不能读写并发。”
下面给出 4 种 iOS 里真正常用、可落地的线程保护方案,按“性能 ➜ 代码量”排序,并附最小可运行示例。
一、最简方案:@synchronized 互斥锁
适用:并发量低、不想引入额外框架
成本:一次内核互斥,约 1-2 µs
@interface ThreadSafeArray : NSObject
@property (nonatomic, strong) NSMutableArray *array;
@end@implementation ThreadSafeArray
- (instancetype)init {self = [super init];_array = [NSMutableArray array];return self;
}
- (void)addObject:(id)obj {@synchronized(self.array) {[self.array addObject:obj];}
}
- (void)removeObject:(id)obj {@synchronized(self.array) {[self.array removeObject:obj];}
}
- (NSUInteger)indexOfObject:(id)obj {@synchronized(self.array) {return [self.array indexOfObject:obj];}
}
@end
二、高效方案:dispatch_barrier 读写锁
特点:读并发、写独占;性能比 @synchronized 高 2-3 倍
@interface RWArray : NSObject
@property (nonatomic, strong) NSMutableArray *array;
@property (nonatomic, strong) dispatch_queue_t rwQueue; // CONCURRENT
@end@implementation RWArray
- (instancetype)init {self = [super init];_array = [NSMutableArray array];_rwQueue = dispatch_queue_create("com.demo.rw", DISPATCH_QUEUE_CONCURRENT);return self;
}
// 读:可并发
- (NSUInteger)indexOfObject:(id)obj {__block NSUInteger idx = NSNotFound;dispatch_sync(self.rwQueue, ^{idx = [self.array indexOfObject:obj];});return idx;
}
// 写:独占
- (void)addObject:(id)obj {dispatch_barrier_async(self.rwQueue, ^{[self.array addObject:obj];});
}
- (void)removeObject:(id)obj {dispatch_barrier_async(self.rwQueue, ^{[self.array removeObject:obj];});
}
@end
三、无锁方案:immutable copy(读远大于写)
思路:底层永远持一个 不可变 NSArray,每次写都生成新数组再原子替换;读无锁。
@interface CopyOnWriteArray : NSObject
@property (nonatomic, strong) NSArray *array; // 永远 immutable
@end@implementation CopyOnWriteArray
- (instancetype)init {self = [super init];_array = @[];return self;
}
- (void)addObject:(id)obj {NSArray *newArray;do {NSArray *old = self.array;newArray = [old arrayByAddingObject:obj];} while (!OSAtomicCompareAndSwapPtr(old, newArray, (void **)&_array)); // 原子替换
}
- (NSUInteger)indexOfObject:(id)obj {// 读操作无锁return [self.array indexOfObject:obj];
}
@end
注:iOS10+ 可用
atomic
+compareAndSwap
或OSAtomic
系列,更现代可用std::atomic
封装。
四、现成线程安全容器:OSAtomic + OSSpinLock(已废弃,仅作了解)
苹果已废弃 OSSpinLock,不推荐;替代为 os_unfair_lock
或 dispatch_semaphore
。
五、一句话总结(面试秒答)
“NSMutableArray 本身不线程安全;
多读单写用 dispatch_barrier
最均衡,小并发直接 @synchronized
,读远多于写可 immutable copy 无锁;
核心原则:读写不能并发,写必须互斥。”
iOS的常见的内存泄漏,比如Delegate 强引用循环,Block 自引用循环以及NSTimer 死循环的样式以及解决方法
iOS 面试里“内存泄漏”三板斧:delegate 强引用、block 自引用、Timer 死循环。
Delegate 强引用循环(最老但最易错)
泄漏代码
@interface Boss : NSObject
@property (nonatomic, strong) id <WorkerDelegate> delegate; // ❌ 强引用
@end
@interface Worker : NSObject <WorkerDelegate>
@property (nonatomic, strong) Boss *boss;
@end
原因:Boss ↔ Worker 互相强引用,dealloc 永不被调用。
修复
@property (nonatomic, weak) id <WorkerDelegate> delegate; // ✅ 弱引用
2025 加分点
“用 __weak typeof(self) weakSelf = self; 之外,还可开启 -fsanitize-address 在运行时检测循环引用;Xcode 15 Memory Graph 能直接可视化箭头。”
Block 自引用循环(ARC 下最常见)
泄漏代码
self.someBlock = ^{NSLog(@"%@", self.name); // ❌ 直接捕获 self → 强引用
};
原因:block 拷贝到堆后强引用 self,self 又强引用 block。
修复模板(weak-strong dance)
__weak typeof(self) weakSelf = self;
self.someBlock = ^{__strong typeof(weakSelf) strongSelf = weakSelf; // 避免中途释放if (!strongSelf) return;NSLog(@"%@", strongSelf.name);
};
2025 新语法糖(Clang 15+)
self.someBlock = ^(void) weakify(self) {strongify(self);NSLog(@"%@", self.name);
};
weakify
/strongify
来自 libextobjc,2025 年 CocoaPods 仍活跃维护。
NSTimer 死循环(RunLoop 强引用 target)
泄漏代码
@interface ViewController ()
@property (nonatomic, strong) NSTimer *timer;
@end
- (void)viewDidLoad {[super viewDidLoad];self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0target:selfselector:@selector(fire)userInfo:nilrepeats:YES];
}
- (void)dealloc {[self.timer invalidate]; // ❌ 永远不会执行
}
原因:RunLoop → Timer → self 强引用,self 无法释放。
经典修复(iOS 10 之前)
- (void)viewWillDisappear:(BOOL)animated {[self.timer invalidate];self.timer = nil;
}
2025 推荐修复(iOS 10+ block API)
__weak typeof(self) weakSelf = self;
self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0repeats:YESblock:^(NSTimer * _Nonnull timer) {__strong typeof(weakSelf) ss = weakSelf;if (ss) [ss fire];
}];
block 版 timer 对 self 是 weak 捕获,无需手动 invalidate 也不会循环引用。
CADisplayLink / 通知中心忘记移除
泄漏代码
self.link = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
// 忘记 remove
修复
- (void)dealloc {[self.link invalidate]; // 必须
}
2025 通用模板(NSHashTable 弱引用)
@interface TimerHolder : NSObject
@property (nonatomic, strong) CADisplayLink *link;
@end
@implementation TimerHolder
- (void)start {__weak typeof(self) w = self;self.link = [CADisplayLink displayLinkWithTarget:w selector:@selector(step)];[self.link addToRunLoop:[NSRunLoop currentRunLoop] forMode:NSRunLoopCommonModes];
}
- (void)dealloc { [self.link invalidate]; }
@end
2025 检测工具一键定位
工具 | 用法 | 2025 新特性 |
---|---|---|
Xcode Memory Graph | 运行 → Debug Navigator → Memory → 蓝色箭头 | 自动标出“循环引用”紫色闪电 |
Instruments Leaks | Profile → Leaks → Call Tree → Invert | 可导出 .trace 与 CI 集成 |
Sanitizer Address | Scheme → Diagnostics → Address Sanitizer | 运行时检测 UAF & 循环引用 |
FBRetainCycleDetector | CocoaPods 引入 | 2024 支持 Swift/ObjC 混编 |
“内存泄漏三板斧:
delegate 用 weak,block 先 weak 再 strong,Timer 用 block API 或及时 invalidate;
配合 Memory Graph 紫色箭头,10 秒定位循环引用。”
iOS 使用Instruments的Leaks工具,定位内存泄露的代码
环境
Xcode Version 10.2.1 (10E1001)
iPhone Version12.1.3
使用流程
启动
暂停
红色x表示存在内存泄露的地方。如果查找到了内存泄露,可以点击暂停,进入第4步
选择 Leaks > Call Tree
勾选
双击,即可定位内存泄露代码。
按照如上配置完成后,有时候我们仍然无法定位到内存泄露的具体代码,你可以尝试如下配置,开启debug环境下的dsYM:
利用 Xcode 内存表(Debug Memory Graph)检测内测泄漏
前言
平常我们都会用 Instrument 的 Leaks / Allocations 或其他一些开源库进行内存泄露的排查,但它们都存在各种问题和不便,
在这个 ARC 时代更常见的内存泄露是循环引用导致的 Abandoned memory,Leaks 工具查不出这类内存泄露,应用有限。
今天介绍一种简单直接的检测内测泄漏的方法:Debug Memory Graph
就是这货:
正文
我最近的项目中,退出登录后(跳转到登录页),发现首页控制器没有被销毁,依旧能接收通知。
退出登录代码:
代码语言:javascript
代码运行次数:0
运行
AI代码解释
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:@"Login" bundle:[NSBundle mainBundle]];
AppDelegate *appDelegate = (AppDelegate *)[UIApplication sharedApplication].delegate;
appDelegate.window.rootViewController = [storyboard instantiateViewControllerWithIdentifier:@"LoginVC"];
很明显发生了循环引用导致的内测泄漏。
接下来就使用 Debug Memory Graph 来查看内测泄漏了。
运行程序
首先启动 Xcode 运行程序。
Debug Memory Graph
点击 Debug Memory Graph 按钮后,可以看到红框内的是当前内存中存在的对象。其中,绿色的就是视图控制器。
这样,我们随时都可以查看内测中存在的对象,换句话说,就是可以通过观察 Memory Graph 查看内测泄漏。
调试你的App
继续运行你的程序
然后对App进行调试、push、pop 操作,再次点击 Debug Memory Graph 按钮。那些该释放而依旧在内测中的 控制器
或 对象
就能一一找出来了。
接下来,只要进入对应的控制器找到内测泄漏的代码就OK了,一般是Block里引用了 self
,改为 weakSelf
就解决了。
代码语言:javascript
代码运行次数:0
运行
AI代码解释
#define WS(weakSelf) __weak __typeof(&*self)weakSelf = self;WS(weakSelf)
sView.btnBlock = ^(NSInteger idx){[weakSelf.tableView reloadSections:[NSIndexSet indexSetWithIndex:idx] withRowAnimation:UITableViewRowAnimationAutomatic];
};
结语
就这样,利用 Debug Memory Graph,可以简单快速的检测内测泄漏。
一般由两个对象循环引用的内测泄漏是比较好发现的,如果是由三个及其三个以上的对象形成的大的循环引用,就会比较难排查了。