在iOS开发中,内存泄漏是高频且棘手的技术问题,尤其网络层的泄漏的隐蔽性强、排查难度高,容易导致App卡顿、崩溃,影响用户体验。最近在项目中排查并解决了一次AFNetworking(以下简称AFN)相关的内存泄漏,从工具定位到底层原理深挖,完整走了一遍闭环,整理成技术笔记分享给大家,既是自己的复盘沉淀,也希望能帮到踩过同类坑、正在排查相关问题的同行。
全程实战无空话,包含「工具使用→问题定位→原理深挖→零风险修复→验证闭环」,所有操作均经过项目实测可落地,代码可直接复用,看完既能解决实际问题,也能加深对iOS网络底层、内存管理的理解。
一、问题发现:内存只涨不跌,定位泄漏对象
项目上线前做内存巡检,用Xcode自带的 Instruments 工具排查,发现了明显的内存泄漏问题,具体现象和定位过程如下:
操作步骤:Xcode → Product → Profile → 选择 Leaks(泄漏检测)→ 运行App,频繁发起网络请求(比如下拉刷新、切换页面触发接口调用);
异常现象:App内存持续上涨,即使退出当前页面、停止请求,内存也无法回落,长时间操作后会出现卡顿,甚至有崩溃风险;
初步定位:通过Leaks工具的「Call Tree」和「Reference Cycle」功能,锁定泄漏对象主要是
AFHTTPSessionManager和其持有的NSURLSession,这两个对象的引用计数始终不为0,无法被系统回收。
这里补充一个实用小技巧:如果Leaks工具定位不够精准,可以配合「Debug Memory Graph」(调试栏的内存图标),能更清晰地看到对象的引用链,快速找到是谁在持有泄漏对象,避免盲目排查,提升排查效率。
二、问题深挖:打破认知误区,找到泄漏根因
定位到泄漏对象后,第一步先排查网络层代码,很快发现了第一个异常点,但真正的根因,藏在iOS网络底层原理里,需要层层拆解才能找到。
1. 代码层面:异常的AFN使用方式
项目中的网络层,没有遵循AFN的常规使用规范——没有将AFHTTPSessionManager设计为单例,而是在每个网络请求的类方法中,都新建了一个AFHTTPSessionManager实例,代码大致如下(简化版,保留核心逻辑):
+ (NSURLSessionDataTask *)GET:(NSString *)URLString parameters:(id)parameters success:(SuccessBlock)success failure:(FailureBlock)failure {
// 每个请求都新建manager,作为局部变量
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
// 发起请求、参数处理等核心逻辑...
return task;
}
一开始我陷入了一个常见的认知误区:manager是局部变量,方法执行完毕后,ARC应该会自动回收它。但实际情况是,即使请求完成,manager依然无法被释放,这说明一定有隐式的强引用,打破了ARC的自动回收机制,需要进一步深挖底层。
2. 原理层面:ARC的局限性 + NSURLSession的底层特性
这是本次泄漏的核心,也是多数开发者容易忽略的点——NSURLSession看似是普通的OC对象,但它的底层封装逻辑远比表面复杂:
NSURLSession 底层封装了
CFNetwork框架,而CFNetwork是基于纯C语言实现的,属于系统底层网络框架;ARC(自动引用计数)的核心作用是管理OC对象的引用计数,实现自动回收,但它有明显的局限性——对C语言层面的底层资源(比如CF对象、系统内核级的session句柄),完全无法管控,也无法自动释放;
双重强引用形成循环:① AFHTTPSessionManager 强引用 NSURLSession(manager作为持有方,管理session的生命周期);② NSURLSession 会被系统内置的delegate队列、网络进程强持有(系统为了保证请求不被中断,会隐式持有session,直到请求完全结束并主动销毁)。
简单来说:manager持有session,系统也持有session,而session又通过内部逻辑间接关联着manager,形成了「manager→session→系统→session→manager」的隐式循环引用,ARC对此无能为力,无法触发自动回收。最终导致每个请求都会产生一个无法释放的manager+session实例,请求次数越多,内存泄漏越严重,长期运行会导致App内存溢出。
3. 根因总结(核心重点)
本次内存泄漏的根本原因,不是「局部变量使用不当」,也不是「简单的循环引用」,而是多重因素叠加导致:
每个请求新建的AFHTTPSessionManager持有NSURLSession,而NSURLSession因底层CFNetwork(纯C实现)的特性,以及系统层面的强引用,无法被ARC自动回收;同时代码中没有主动销毁NSURLSession的逻辑,最终形成累积式内存泄漏,影响App性能。
三、解决方案:零风险修复,不改动业务逻辑
找到根因后,修复思路就非常清晰了——主动打破强引用循环,触发NSURLSession的底层资源释放。同时要兼顾项目线上稳定性,避免重构整个网络层(重构成本高、风险大,容易引入新的业务bug,且需要大量回归测试),最终采用「最小侵入式修复」方案,只修改关键代码,零业务风险,上线后无任何异常。
具体修复步骤如下,代码可直接复制复用:
1. 给manager添加__block修饰符
因为manager是局部变量,而我们需要在请求的success/failure回调中修改它(将其置为nil,释放引用),所以必须给manager添加__block修饰符,打破Block对外部局部变量的只读限制,确保回调中能正常赋值。
2. 在回调末尾添加清理逻辑
在每个请求的success和failure回调最后,添加两行核心清理代码,主动销毁NSURLSession、释放manager,彻底打破强引用循环:
// 关键修复:给manager加__block修饰,允许回调中修改
__block AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
// 发起请求,保留原有业务逻辑不变
NSURLSessionDataTask *task = [manager GET:URLString parameters:parameters success:^(NSURLSessionDataTask *task, id responseObject) {
// 原有业务逻辑...(HUD提示、日志打印、数据解析等)
// 新增:主动清理session和manager,释放内存
[manager.session finishTasksAndInvalidate];
manager = nil;
} failure:^(NSURLSessionDataTask *task, NSError *error) {
// 原有业务逻辑...(错误提示、日志打印、异常处理等)
// 新增:主动清理session和manager,释放内存
[manager.session finishTasksAndInvalidate];
manager = nil;
}];
关键说明(核心细节,必看)
- 为什么选择
finishTasksAndInvalidate方法?
这是苹果官方推荐的、安全的NSURLSession销毁方法,其核心作用是「等待当前所有已提交的任务执行完毕后,销毁NSURLSession,同时拒绝接收新的任务」。这种方式不会中断当前正在执行的请求,也不会影响任何业务逻辑,完全适配项目现有场景;
需避免使用invalidateAndCancel方法——该方法会立即取消所有未完成的任务,强行中断请求,可能导致数据丢失、业务异常,风险极高,仅适用于主动取消所有请求的场景。
- 控制器持有task会有问题吗?
完全不会!在实际开发中,控制器持有task是常规操作,主要用于取消未完成的请求、判断请求状态(执行中/已完成/已取消)等场景。task是独立的对象,session销毁后,task依然可用,常规操作(如调用cancel、读取state属性)不会触发僵尸对象,只有直接操作task.session(此时session已销毁)才会有风险,而正常业务开发中几乎不会出现这种非常规操作。
四、修复验证:闭环验证,确保无泄漏、无异常
修复完成后,必须进行闭环验证,确保泄漏问题彻底解决,同时不引入新的bug,保障项目稳定性,具体验证步骤如下:
工具验证:用Leaks工具重新检测,频繁发起网络请求后,AFHTTPSessionManager和NSURLSession能正常释放,无任何泄漏;同时用Instruments的Allocations工具查看内存使用情况,内存稳定,无持续上涨现象。
功能验证:回归所有网络相关业务,所有请求能正常发起、响应,success/failure回调正常触发,HUD提示、日志打印、页面跳转、数据解析等原有业务逻辑和原来完全一致,用户无任何感知。
边界验证:测试控制器持有task的场景(如页面销毁时取消请求、主动调用cancel取消请求、读取task.state判断状态),均无崩溃、无异常,边界场景完全适配。
五、复盘与收获
这次内存泄漏排查与修复,虽然过程不算复杂,但让我彻底跳出了固有的认知误区,也加深了对iOS内存管理、网络底层的理解,总结几点核心收获,与大家共勉:
排查内存泄漏,不能凭经验猜测,一定要用对工具(Leaks + Debug Memory Graph),通过查看引用链、分析持有关系,精准定位泄漏根因,才能高效解决问题,避免盲目修改代码。
iOS网络层的很多坑,都藏在底层封装里,深入理解NSURLSession、CFNetwork的基础特性,掌握ARC的局限性,能少走很多弯路,也能更快速地排查复杂问题。
项目线上问题修复,优先选择「最小侵入式」方案,重构虽能从根本上优化代码,但成本高、风险大,能通过少量代码修改解决问题,兼顾稳定性和效率,才是更优的工程实践。
实战是提升技术能力的最好方式,每一次排查问题、解决bug,都是一次复盘沉淀的机会,把这些实战经验整理下来,既能巩固自身知识,也能帮助到更多同行。