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.0
                                                  target:self
                                                selector:@selector(fire)
                                                userInfo:nil
                                                 repeats: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.0
                                              repeats:YES
                                                block:^(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,可以简单快速的检测内测泄漏。

一般由两个对象循环引用的内测泄漏是比较好发现的,如果是由三个及其三个以上的对象形成的大的循环引用,就会比较难排查了。

相关推荐
可乐鸡翅yeah_14 小时前
fmp4 碎片 MP4 格式 HLS 流,和传统 TS 分片有什么不一样
开发语言·javascript·ios·音视频·safari·m3u8·m3u8在线播放
神奇的小猴程序员17 小时前
苹果Mac电脑玩我的世界没有版本?用PCL-R即可!
macos
茶底世界之下18 小时前
为什么高精度透明图片上传到 Metal 时,必须把“拷贝像素”与“混合合成”分开
ios
灵韵aura18 小时前
macOS 的指令缩写
macos
老李IT笔记18 小时前
iOS 27 更新延迟只能给 1 到 90 天:两个参数怎么配,配错了会发生什么
ios·智能手机
Helix25019 小时前
移动端浏览器内核之争:iOS 为何强制使用 WebKit?
ios·safari·webkit·wkwebview·浏览器内核·blink·ios浏览器内核
传奇开心果编程1 天前
【SwiftUI提高练中学】第13课 实时活动与灵动岛:ActivityKit 实战
学习·ui·ios·swiftui·swift
Digitally1 天前
如何在没有Wi-Fi的情况使用移动数据或电脑更新iPhone
电脑·cocoa·iphone
可乐鸡翅yeah_1 天前
M3U8 防盗链 URL‑Token 签名,新手开发实操避坑
开发语言·javascript·ios·音视频·safari