AFNetworking内存泄漏排查全流程

在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,保障项目稳定性,具体验证步骤如下:

  1. 工具验证:用Leaks工具重新检测,频繁发起网络请求后,AFHTTPSessionManager和NSURLSession能正常释放,无任何泄漏;同时用Instruments的Allocations工具查看内存使用情况,内存稳定,无持续上涨现象。

  2. 功能验证:回归所有网络相关业务,所有请求能正常发起、响应,success/failure回调正常触发,HUD提示、日志打印、页面跳转、数据解析等原有业务逻辑和原来完全一致,用户无任何感知。

  3. 边界验证:测试控制器持有task的场景(如页面销毁时取消请求、主动调用cancel取消请求、读取task.state判断状态),均无崩溃、无异常,边界场景完全适配。

五、复盘与收获

这次内存泄漏排查与修复,虽然过程不算复杂,但让我彻底跳出了固有的认知误区,也加深了对iOS内存管理、网络底层的理解,总结几点核心收获,与大家共勉:

  • 排查内存泄漏,不能凭经验猜测,一定要用对工具(Leaks + Debug Memory Graph),通过查看引用链、分析持有关系,精准定位泄漏根因,才能高效解决问题,避免盲目修改代码。

  • iOS网络层的很多坑,都藏在底层封装里,深入理解NSURLSession、CFNetwork的基础特性,掌握ARC的局限性,能少走很多弯路,也能更快速地排查复杂问题。

  • 项目线上问题修复,优先选择「最小侵入式」方案,重构虽能从根本上优化代码,但成本高、风险大,能通过少量代码修改解决问题,兼顾稳定性和效率,才是更优的工程实践。

  • 实战是提升技术能力的最好方式,每一次排查问题、解决bug,都是一次复盘沉淀的机会,把这些实战经验整理下来,既能巩固自身知识,也能帮助到更多同行。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容