[iOS] AFNetworking 的内存泄漏分析

本文分析了使用 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_WATCH
    self.reachabilityManager = [AFNetworkReachabilityManager sharedManager];
#endif

    self.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 常驻内存,需要占用部分内存空间,也可以手动管理其生命周期。

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

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值