本文分析了使用 AFNetworking 组件时遇见的内存泄漏问题的根本原因,并给出解决方案。

当第一次学会使用 Leaks 时,便拿了公司的 APP 项目练手测试了一下。结果测试刚开始,便出现了 Leaks 经典的小红叉。查看了一下小红叉的原因,就是 AFNetworking。由于 APP 在启动时,需要使用 AFNetworking 请求并下载相关的资源更新。当时并没有对这个小红叉的成因继续深究下去,趁最近有空,回过头来研究一下。

1. Demo 测试

编写一个 Demo,复现一下当时内存泄漏的情况。AFNetworking 的版本为 3.2.1。

#import "ViewController.h"
#import <AFNetworking.h>@implementation ViewController- (void)viewDidLoad {[super viewDidLoad];// Do any additional setup after loading the view.UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem];button.frame = CGRectMake(0, 0, 100, 50);button.center = self.view.center;[button setTitle:@"Request" forState:UIControlStateNormal];[button addTarget:self action:@selector(requestButtonEvent) forControlEvents:UIControlEventTouchUpInside];[self.view addSubview:button];
}- (void)requestButtonEvent {NSString *url = @"https://www.baidu.com";AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];manager.responseSerializer = [AFHTTPResponseSerializer serializer];[manager GET:url parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {NSLog(@"%@", responseObject);NSLog(@"%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {NSLog(@"Error: %@", error);}];
}@end

每当点击一次 Request 按钮,就会调用 AFNetworking 的 get 接口请求百度首页。然后,使用 Leaks 来查看一下内存泄漏的情况。

在 Leaks 的图中,可以看出每点击一次 Request 按钮,内存就会上涨一次,而且附带着内存泄漏的情况发生。查看下方的 Cycles & Roots,可以看到总共有两个引用环存在,刚好对应两次点击 Request 事件。

查看这两个引用环的场景,发现原因都是相同的。AFHTTPSessionManager 对象强引用 NSURLSession 类型的变量 _session,NSURLSession 类通过 delegate 强引用 AFHTTPSessionManager 对象。这个引用环的存在,导致 AFHTTPSessionManager 对象和 NSURLSession 对象都无法释放,造成了内存泄漏。

2. 内存泄漏原因分析

与常见的 Block 造成的循环引用不同,这是由 Delegate 造成的循环引用。

根据上图中的引用环,先查看 AFHTTPSessionManager 文件,发现 session 属性在 AFHTTPSessionManager 的父类 AFURLSessionManager 中,发现 AFURLSessionManager 强引用了 session。

/**The managed session.*/
@property (readonly, nonatomic, strong) NSURLSession *session;

再查看 session 的类 —— NSURLSession 中的 Delegate 的实现,发现在 NSURLSession 中的 Delegate 属性的内存管理语义居然是 retain!

@property (nullable, readonly, retain) id <NSURLSessionDelegate> delegate;

正常情况下,Delegate 的内存管理语义是 weak 或者 assign,其中更推荐使用 weak。但在 NSURLSession 中,为什么要将 delegate 的内存管理语义设为 retain 呢?

遇事不决,先查文档。

Discussion

This delegate object is responsible for handling authentication challenges, for making caching decisions, and for handling other session-related events. The session object keeps a strong reference to this delegate until your app exits or explicitly invalidates the session. If you do not invalidate the session, your app leaks memory until it exits.

文档中已经说明,这个 delegate 负责处理身份认证、决定缓存策略和其它与 session 有关的事务。这个 session 对象会一直保留强引用,直到 APP 退出或者主动使 session 失效。如果不使 session 失效,APP 的内存则会泄漏直到 APP 退出为止。

在 AFNetworking 中,delegate 正是 AFHTTPSessionManager。在 AFHTTPSessionManager 调用初始化方法时,会调用父类 AFURLSessionManager 的 initWithSessionConfiguration: 方法,让 NSURLSession 的 delegate 指向 self。

- (instancetype)initWithSessionConfiguration:(NSURLSessionConfiguration *)configuration {self = [super init];if (!self) {return nil;}if (!configuration) {configuration = [NSURLSessionConfiguration defaultSessionConfiguration];}self.sessionConfiguration = configuration;self.operationQueue = [[NSOperationQueue alloc] init];self.operationQueue.maxConcurrentOperationCount = 1;self.session = [NSURLSession sessionWithConfiguration:self.sessionConfiguration delegate:self delegateQueue:self.operationQueue];self.responseSerializer = [AFJSONResponseSerializer serializer];self.securityPolicy = [AFSecurityPolicy defaultPolicy];#if !TARGET_OS_WATCHself.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
#endifself.mutableTaskDelegatesKeyedByTaskIdentifier = [[NSMutableDictionary alloc] init];self.lock = [[NSLock alloc] init];self.lock.name = AFURLSessionManagerLockName;[self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks, NSArray *downloadTasks) {for (NSURLSessionDataTask *task in dataTasks) {[self addDelegateForDataTask:task uploadProgress:nil downloadProgress:nil completionHandler:nil];}for (NSURLSessionUploadTask *uploadTask in uploadTasks) {[self addDelegateForUploadTask:uploadTask progress:nil completionHandler:nil];}for (NSURLSessionDownloadTask *downloadTask in downloadTasks) {[self addDelegateForDownloadTask:downloadTask progress:nil destination:nil completionHandler:nil];}}];return self;
}

这说明了,AFNetworking 的内存泄漏,本质上是 NSURLSession 的 delegate 的内存管理方式的原因。由于 NSURLSession 对 delegate,即 AFHTTPSessionManager 保持强引用,并且 AFHTTPSessionManager 将 NSURLSession 作为属性,保持对其的强引用关系,导致两者之间形成了一个引用环,造成内存泄漏。

如果 delegate 的语义改成 weak 或者 assign 呢?这也可能导致 delegate 对象正在处理 session 的回调时,在无意间被释放,引发程序异常。

引申:为什么 AFNetworking 要分成 AFHTTPSessionManager 和 AFURLSessionManager 两个类?

这个问题可以从面向对象的角度回答。面向对象有五大基本原则:单一职责原则、开放封闭原则、里式替换原则、依赖倒置原则、接口分离原则。根据单一职责原则,一个类应该只做一类事情,一个类应该只负责一个功能。AFHTTPSessionManager 的作用是拼接并发送 HTTP 请求,AFURLSessionManager 的作用则是管理 NSURLSession 对象,并处理其回调事件。

3. 内存泄漏解决方案

(1) 主动使 session 失效,解开引用环

正如文档中所述,只要使 session 失效,就可以解开 session 和 delegate 之间的强引用。使 session 失效的接口有以下两个,一个是 finishTasksAndInvalidate, 另一个是 invalidateAndCancel。前一个会等待现存的任务完成后,并触发 URLSession:didBecomeInvalidWithError: 回调,再释放强引用。而后一个则立即取消所有任务。

/* -finishTasksAndInvalidate returns immediately and existing tasks will be allowed* to run to completion.  New tasks may not be created.  The session* will continue to make delegate callbacks until URLSession:didBecomeInvalidWithError:* has been issued. ** -finishTasksAndInvalidate and -invalidateAndCancel do not* have any effect on the shared session singleton.** When invalidating a background session, it is not safe to create another background* session with the same identifier until URLSession:didBecomeInvalidWithError: has* been issued.*/
- (void)finishTasksAndInvalidate;/* -invalidateAndCancel acts as -finishTasksAndInvalidate, but issues* -cancel to all outstanding tasks for this session.  Note task * cancellation is subject to the state of the task, and some tasks may* have already have completed at the time they are sent -cancel. */
- (void)invalidateAndCancel;

在 AFNetworking 中,也有对应的方法 invalidateSessionCancelingTasks:,参数选择 YES 对应 invalidateAndCancel,选择  NO 则对应 finishTasksAndInvalidate。

那么,为了避免内存泄漏问题,当这个请求接收之后,无论成功与否,就可以把这个 AFHTTPSessionManager 引用的 session 失效。

以 Demo 中的 Request 按钮事件为例,在 get 接口的 success 和 failure 回调中,都加上 invalidateSessionCancelingTasks: 接口即可。此处参数传 YES 或者 NO 都可以,因为这个 session 只有这一个请求任务。

- (void)requestButtonEvent {NSString *url = @"https://www.baidu.com";AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];manager.responseSerializer = [AFHTTPResponseSerializer serializer];[manager GET:url parameters:nil progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id  _Nullable responseObject) {NSLog(@"%@", responseObject);NSLog(@"%@", [[NSString alloc] initWithData:responseObject encoding:NSUTF8StringEncoding]);// 成功后使 session 失效[manager invalidateSessionCancelingTasks:NO];} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {NSLog(@"Error: %@", error);// 失败后使 session 失效[manager invalidateSessionCancelingTasks:NO];}];
}

重新运行一下 Leaks,可以发现,AFNetworking 引起的内存泄漏问题已经被避免。

(2) 使用单例模式,使 AFURLSessionManager/AFHTTPSessionManager 常驻

如果直接搜索关键词“AFNetworking 内存泄漏”,可以发现更多的是使用单例模式,而并非是使 session 失效。这其中是否有什么原因呢?

对比一下使 session 失效的差别。如果采用使 session 失效的方法,每一次都会新建一个 AFHTTPSessionManager 对象,包括内部的 session 也需要重新创建。

// 第一次点击 Request 按钮
<AFHTTPSessionManager: 0x28240c000, baseURL: (null), session: <__NSURLSessionLocal: 0x10a2028c0>, operationQueue: <NSOperationQueue: 0x10a200a80>{name = 'NSOperationQueue 0x10a200a80'}>// 第二次点击 Request 按钮
<AFHTTPSessionManager: 0x2824101e0, baseURL: (null), session: <__NSURLSessionLocal: 0x109706e10>, operationQueue: <NSOperationQueue: 0x109706c10>{name = 'NSOperationQueue 0x109706c10'}>

打印 AFHTTPSessionManager 的地址信息,可见 AFHTTPSessionManager 对象以及 session 对象的内存地址都发生了改变。

为了得到更多的信息,可以使用 Wireshark 工具抓包。从抓包获取的数据中可以发现,当 session 失效后,TCP 连接也会断开。等到下次发送时,还需要再来一次三次握手、身份认证等等。这意味着,如果在 AFNetworking 收到回调后便释放 session,那么每一次调用接口,即使接口相同,也需要重新建立连接,需要付出额外的开销。

那么,如何使用单例模式实现呢?这个答案在网络上已经很多了,这里就搬运 AFNetworking Example 中的 AFAppDotNetAPIClient 为例。

// ---- AFAppDotNetAPIClient.h ----
#import <Foundation/Foundation.h>
// 模块导入语法,等同于"#import <AFNetworking/AFNetworking.h>"
@import AFNetworking;@interface AFAppDotNetAPIClient : AFHTTPSessionManager+ (instancetype)sharedClient;@end// ---- AFAppDotNetAPIClient.m ----
#import "AFAppDotNetAPIClient.h"static NSString * const AFAppDotNetAPIBaseURLString = @"https://api.app.net/";@implementation AFAppDotNetAPIClient+ (instancetype)sharedClient {static AFAppDotNetAPIClient *_sharedClient = nil;static dispatch_once_t onceToken;dispatch_once(&onceToken, ^{_sharedClient = [[AFAppDotNetAPIClient alloc] initWithBaseURL:[NSURL URLWithString:AFAppDotNetAPIBaseURLString]];// https 证书校验方式,默认为 AFSSLPinningModeNone,不使用固定证书校验。_sharedClient.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModeNone];});return _sharedClient;
}

这个 Example 中,AFAppDotNetAPIClient 继承了 AFHTTPSessionManager,添加了单例模式。由于单例模式下,AFAppDotNetAPIClient 内的属性,包括 session,会在内存中常驻且可以被访问到,因此不会造成内存泄漏。由于每一次访问的都是同一个 session,当请求相同接口时,可以减少重新初始化对象、重新建立 TCP 连接的开销。

当然,Example 中的写法还有两个问题。一个是单例模式的实现还未完善,二是一个单例模式只能响应一个接口。

第一个暂且不谈。

第二个可以修改单例模式的写法,使用 NSDictionary 或者 NSCache 来存储每一对接口的 URL 和对应的 AFHTTPSessionManager。当每次请求新的接口时,将 URL 作为 key,AFHTTPSessionManager 作为 value 存储起来,以便下次使用。当不需要使用时,从 NSDictionary 或者 NSCache 中移除,并调用使 session 失效的接口即可。

4. 总结

使用 AFNetworking 请求时发生内存泄漏的原因是因为,NSURLSession 的 delegate 的内存管理语义为 retain,导致 NSURLSession 和 AFURLSessionManager 相互强引用。

解决内存泄漏的方法至少有两种,一是通过 NSURLSession 的接口主动使 NSURLSession 对 AFURLSessionManager 的强引用失效,二是是 AFURLSessionManager 常驻内存,保证每次使用的都是同一个 NSURLSession。

两种方法的差异在于,第一种方法在释放 session 之后,请求相同接口也需要重新建立 TCP 连接,需要额外的开销;而第二种方法让 AFURLSessionManager 常驻内存,需要占用部分内存空间,也可以手动管理其生命周期。

如果某个接口只使用一次,推荐使用第一种方法,用完释放即可。如果接口需要被多次使用,更推荐使用第二种方法。

[iOS] AFNetworking 的内存泄漏分析相关推荐

  1. android释放acitity内存,Android 内存泄漏分析与解决方法

    在分析Android内存泄漏之前,先了解一下JAVA的一些知识 1. JAVA中的对象的创建 使用new指令生成对象时,堆内存将会为此开辟一份空间存放该对象 垃圾回收器回收非存活的对象,并释放对应的内 ...

  2. 记一次 .NET 某外贸Web站 内存泄漏分析

    一:背景 1. 讲故事 上周四有位朋友加wx咨询他的程序内存存在一定程度的泄漏,并且无法被GC回收,最终机器内存耗尽,很尴尬. 沟通下来,这位朋友能力还是很不错的,也已经做了初步的dump分析,发现了 ...

  3. 内存泄漏分析_调查内存泄漏第2部分–分析问题

    内存泄漏分析 这个小型系列的第一个博客介绍了如何创建一个非常泄漏的示例应用程序,以便我们可以研究解决服务器应用程序上基于堆的问题的技术. 它展示了Producer-Consumer模式的一个大问题,即 ...

  4. 4大JVM性能分析工具详解,及内存泄漏分析方案

    谈到性能优化分析一般会涉及到: Java代码层面的,典型的循环嵌套等 还会涉及到Java JVM:内存泄漏溢出等 MySQL数据库优化:分库分表.慢查询.长事务的优化等 阿里P8架构师谈:MySQL慢 ...

  5. Android内存泄漏分析及调试

    2019独角兽企业重金招聘Python工程师标准>>> Android内存泄漏分析及调试 分类: Android2013-10-25 11:31 5290人阅读 评论(5) 收藏 举 ...

  6. Android 内存泄漏分析指北

    android 内存泄漏分析指北 简单来说内存泄漏就是当对象不再被应用程序使用,但是垃圾回收器却不能移除它们,因为它们正在被引用 java 垃圾回收介绍: Java 虚拟机运行所管理的内存包括以下几个 ...

  7. Android常见的内存泄漏分析

    内存泄漏原因 当应用不需要在使用某个对象时候,忘记释放为其分配的内存,导致该对象仍然保持被引用状态(当对象拥有强引用,GC无法回收),从而导致内存泄漏. 常见的内存泄漏源头 泄漏的源头有很多,有开源的 ...

  8. Android 内存泄漏分析与解决方法

    Android 内存泄漏分析与解决方法 参考文章: (1)Android 内存泄漏分析与解决方法 (2)https://www.cnblogs.com/start1225/p/6903419.html ...

  9. 7 php 内存泄漏_PHP内存泄漏分析定位

    说明:本文来自作者  邹毅 在 GitChat 上分享「  PHP 内存泄漏分析定位」 目录 场景一 程序操作数据过大 场景二 程序操作大数据时产生拷贝 场景三 配置不合理系统资源耗尽 场景四 无用的 ...

最新文章

  1. mysq改变字段类型
  2. Android 多线程之Handler
  3. php矢量瓦片,矢量瓦片相关计算函数
  4. 一分钟明确 VS manifest 原理
  5. Linux+Nginx+Asp.net Core部署
  6. 使用php发送Http请求,抓取网页数据
  7. python爬虫-Python爬虫入门这一篇就够了
  8. 头条的_signature这个如何_如何彻底防止反编译,dex加密怎么做
  9. 天才绅士少女助手克里斯蒂娜 [数学+树状数组]
  10. LC串联谐振的分析方法
  11. 自己做量化交易软件(40)小白量化实战13--Alpha101及自编因子公式
  12. ES6 filter 过滤数组 | 图片onload同步等待获取图片宽高
  13. 搞定分布式系列:缓存 热key 问题解决方案
  14. Java bean 复制克隆工具
  15. 基于opencv和pillow实现人脸识别系统(附demo)
  16. 教程:客制化您的输入法
  17. WEB 请求处理二:Nginx 请求 反向代理
  18. Python基础之图像识别
  19. 关于模拟信号,数字信号,电磁波,基带传输的一点点感悟
  20. 电脑出现负片情况,底片效果怎么解决?(win10颜色滤镜功能)

热门文章

  1. OpenCV(Python)颜色识别(一)
  2. 浅谈矩阵变换——Matrix
  3. 如何通过百度搜索的下拉词和相关搜索找长尾关键词?
  4. 达内java培优训练营 2106班
  5. Apache ServiceComb社区常见问题解答问答精选(第一期)
  6. 全国计算机等级考试Java上机真题
  7. Flex和Flash一起使用开发项目各取所长
  8. 电脑文件数据恢复有哪些方法?电脑怎么恢复已删除的文件数据?
  9. 国外问卷调查为什么这么热门?
  10. Linux文件相关指令