iOS开发基础44-网络编程之NSURLSession&AFN

iOS 网络请求三种方式:NSURLConnection、NSURLSession 与 AFNetworking

本文系统梳理 iOS 网络请求的三种方式:NSURLConnection(已废弃,了解)、NSURLSession(苹果推荐,含三种任务类型与断点续传)、AFNetworking(基于 NSURLSession 的第三方库,含序列化与网络状态检测),以及 Swift 对应的 Alamofire。


一、NSURLConnection(已废弃,了解)

NSURLConnection 是 iOS 早期的网络 API,iOS 9 起被 NSURLSession 取代,新项目不推荐使用。

1. 代理方式请求

@interface MyViewController () <NSURLConnectionDataDelegate>
@property (nonatomic, strong) NSMutableData *receivedData;
@end

@implementation MyViewController

- (void)startRequest {
    NSURL *url = [NSURL URLWithString:@"https://example.com/api"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    // startImmediately:YES 时自动开始,无需手动 start
    [[NSURLConnection alloc] initWithRequest:request delegate:self];
}

#pragma mark - NSURLConnectionDataDelegate

- (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
    self.receivedData = [[NSMutableData alloc] init];
}

- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
    [self.receivedData appendData:data];
}

- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
    NSLog(@"请求完成: %@", [[NSString alloc] initWithData:self.receivedData encoding:NSUTF8StringEncoding]);
}

- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
    NSLog(@"请求失败: %@", error.localizedDescription);
}

@end

2. 线程与 RunLoop

NSURLConnection 的回调默认在创建 connection 的线程的 RunLoop 中执行(不是固定在主线程)。在主线程创建时回调在主线程;在子线程创建时,子线程必须有 RunLoop 才能接收回调。

dispatch_async(dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{
    NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com"]];
    // startImmediately:NO,手动调度到当前 RunLoop
    NSURLConnection *connection = [[NSURLConnection alloc] initWithRequest:request delegate:self startImmediately:NO];
    [connection scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
    [connection start];
    // 保持 RunLoop 运行(否则线程立即退出,回调无法执行)
    [[NSRunLoop currentRunLoop] run];
});

子线程默认没有 RunLoop,[NSRunLoop currentRunLoop] 第一次调用时创建,但 run 会阻塞当前线程。子线程执行网络请求推荐用 NSURLSession(自动管理线程),无需手动处理 RunLoop。


二、NSURLSession(推荐)

NSURLSession 是 iOS 7+ 引入的现代网络 API,取代 NSURLSession,支持后台下载、任务管理、断点续传、配置灵活。

1. 创建 Session

// 共享 session(基本配置,无法设置代理)
NSURLSession *sharedSession = [NSURLSession sharedSession];

// 自定义配置 session
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.timeoutIntervalForRequest = 15;       // 请求超时
config.timeoutIntervalForResource = 30;      // 资源超时
config.HTTPAdditionalHeaders = @{@"User-Agent": @"MyApp/1.0"};
// delegateQueue:nil 时,代理方法在子线程串行队列执行;传 mainQueue 则在主线程
NSURLSession *session = [NSURLSession sessionWithConfiguration:config
                                                        delegate:self
                                                   delegateQueue:[NSOperationQueue mainQueue]];

Session 配置类型:

配置 说明
defaultSessionConfiguration 默认配置,磁盘缓存 + 凭证存储
ephemeralSessionConfiguration 临时配置,不写磁盘(内存缓存),类似无痕浏览
backgroundSessionConfigurationWithIdentifier: 后台配置,App 退到后台仍可下载/上传

2. 三种任务类型

任务类型 说明 数据去向
Data Task NSURLSessionDataTask 普通请求,获取 JSON/XML 等小数据 内存(NSData)
Download Task NSURLSessionDownloadTask 文件下载,支持断点续传和后台下载 临时文件(磁盘)
Upload Task NSURLSessionUploadTask 文件上传,支持后台上传 从文件/NSData 读取

3. Data Task(普通请求)

NSURL *url = [NSURL URLWithString:@"https://api.example.com/data"];
NSURLSessionDataTask *task = [[NSURLSession sharedSession] dataTaskWithURL:url
                                                           completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
    if (error) {
        NSLog(@"请求失败: %@", error);
        return;
    }
    // 解析数据(completionHandler 在子线程执行,UI 更新需回主线程)
    NSDictionary *json = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
    dispatch_async(dispatch_get_main_queue(), ^{
        NSLog(@"结果: %@", json);
    });
}];
[task resume]; // 任务默认挂起,必须 resume

4. 任务控制

[task resume];   // 开始/继续
[task suspend];  // 暂停(挂起)
[task cancel];   // 取消

cancelByProducingResumeData:NSURLSessionDownloadTask 有此方法,用于取消时保存续传数据。Data Task 和 Upload Task 不支持。


三、NSURLSession 代理方法

1. Data Task 代理

@interface ViewController () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSMutableData *receivedData;
@end

@implementation ViewController

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                     didReceiveResponse:(NSURLResponse *)response
                                     completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    self.receivedData = [NSMutableData data];
    completionHandler(NSURLSessionResponseAllow); // 必须调用,允许继续接收
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                     didReceiveData:(NSData *)data {
    [self.receivedData appendData:data];
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                       didCompleteWithError:(NSError *)error {
    if (error) {
        NSLog(@"失败: %@", error);
    } else {
        NSLog(@"完成: %@", [[NSString alloc] initWithData:self.receivedData encoding:NSUTF8StringEncoding]);
    }
}

@end

didReceiveResponse:completionHandler: 中必须调用 completionHandler(NSURLSessionResponseAllow),否则不会继续接收数据。

2. Download Task 代理(进度 + 完成)

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                                           didWriteData:(int64_t)bytesWritten
                                      totalBytesWritten:(int64_t)totalBytesWritten
                              totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
    // 下载进度
    CGFloat progress = (CGFloat)totalBytesWritten / totalBytesExpectedToWrite;
    dispatch_async(dispatch_get_main_queue(), ^{
        self.progressView.progress = progress;
    });
}

- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask
                              didFinishDownloadingToURL:(NSURL *)location {
    // location 是临时文件,必须移动到目标路径
    NSString *caches = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) firstObject];
    NSString *dest = [caches stringByAppendingPathComponent:downloadTask.response.suggestedFilename];
    [[NSFileManager defaultManager] moveItemAtURL:location toURL:[NSURL fileURLWithPath:dest] error:nil];
}

3. Upload Task 代理(上传进度)

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task
                                    didSendBodyData:(int64_t)bytesSent
                                     totalBytesSent:(int64_t)totalBytesSent
                           totalBytesExpectedToSend:(int64_t)totalBytesExpectedToSend {
    CGFloat progress = (CGFloat)totalBytesSent / totalBytesExpectedToSend;
    // 更新上传进度
}

四、断点续传(两种方式)

方式一:Download Task 原生断点续传(推荐)

NSURLSessionDownloadTask 原生支持断点续传,取消时生成 resumeData,恢复时用 resumeData 创建新任务:

@interface ViewController ()
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLSessionDownloadTask *downloadTask;
@property (nonatomic, strong) NSData *resumeData;
@end

@implementation ViewController

- (NSURLSession *)session {
    if (!_session) {
        NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
        _session = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
    }
    return _session;
}

// 开始下载
- (void)startDownload {
    NSURL *url = [NSURL URLWithString:@"https://example.com/large.mp4"];
    self.downloadTask = [self.session downloadTaskWithURL:url];
    [self.downloadTask resume];
}

// 暂停(取消并保存 resumeData)
- (void)pauseDownload {
    [self.downloadTask cancelByProducingResumeData:^(NSData *resumeData) {
        self.resumeData = resumeData;
        self.downloadTask = nil;
    }];
}

// 恢复下载
- (void)resumeDownload {
    if (self.resumeData) {
        self.downloadTask = [self.session downloadTaskWithResumeData:self.resumeData];
    } else {
        [self startDownload];
    }
    [self.downloadTask resume];
    self.resumeData = nil;
}

@end

cancelByProducingResumeData: 仅在服务器支持 Range 请求且已下载部分数据时才能生成 resumeData,否则回调中 resumeData 为 nil。resumeData 可持久化到磁盘,App 重启后仍可恢复。

方式二:Data Task + Range 手动实现

通过 HTTP Range 请求头手动实现断点续传,已下载部分写入文件,暂停后从文件大小位置继续:

@interface ViewController () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSession *session;
@property (nonatomic, strong) NSURLSessionDataTask *dataTask;
@property (nonatomic, strong) NSOutputStream *outputStream;
@property (nonatomic, assign) long long totalLength;
@property (nonatomic, assign) long long currentLength;
@end

@implementation ViewController

- (NSString *)filePath {
    return [NSTemporaryDirectory() stringByAppendingPathComponent:@"downloadedFile"];
}

- (long long)currentFileSize {
    NSDictionary *attrs = [[NSFileManager defaultManager] attributesOfItemAtPath:[self filePath] error:nil];
    return [attrs[NSFileSize] longLongValue];
}

- (void)startDownload {
    self.currentLength = [self currentFileSize];
    
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/large.mp4"]];
    // Range: bytes=X- 表示从第 X 字节开始下载
    [request setValue:[NSString stringWithFormat:@"bytes=%lld-", self.currentLength] forHTTPHeaderField:@"Range"];
    
    self.dataTask = [self.session dataTaskWithRequest:request];
    [self.dataTask resume];
}

- (void)pauseDownload {
    [self.dataTask cancel];
    [self.outputStream close];
    self.dataTask = nil;
}

#pragma mark - NSURLSessionDataDelegate

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask
                                     didReceiveResponse:(NSURLResponse *)response
                                     completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
    // Range 请求时 expectedContentLength 是剩余部分大小,加上已下载部分才是总大小
    self.totalLength = response.expectedContentLength + self.currentLength;
    self.outputStream = [NSOutputStream outputStreamToFileAtPath:[self filePath] append:YES];
    [self.outputStream open];
    completionHandler(NSURLSessionResponseAllow);
}

- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data {
    [self.outputStream write:data.bytes maxLength:data.length];
    self.currentLength += data.length;
    CGFloat progress = (CGFloat)self.currentLength / self.totalLength;
    dispatch_async(dispatch_get_main_queue(), ^{
        self.progressView.progress = progress;
    });
}

- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
    [self.outputStream close];
    if (error) {
        NSLog(@"下载失败: %@", error);
    } else {
        NSLog(@"下载完成: %@", [self filePath]);
    }
}

@end

两种方式对比:Download Task 原生方式简单、支持后台下载、系统管理临时文件;Data Task + Range 方式灵活、可完全控制写入逻辑,但需手动管理文件和 Range。推荐使用 Download Task 原生方式。


五、AFNetworking

AFNetworking 是基于 NSURLSession 的 Objective-C 网络库,封装了请求、序列化、上传下载、网络状态检测等功能。

1. 安装

pod 'AFNetworking', '~> 4.0'

2. AFHTTPSessionManager(核心类)

AFNetworking 3.0+ 完全基于 NSURLSession,核心类是 AFHTTPSessionManagerAFHTTPRequestOperationManager(基于 NSURLConnection)在 3.0 起已被移除。

GET 请求

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
[manager GET:@"https://api.example.com/data"
  parameters:nil
     headers:nil
    progress:nil
     success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"成功: %@", responseObject); // 自动解析为 NSDictionary/NSArray
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    NSLog(@"失败: %@", error);
}];

POST 请求(JSON)

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer]; // 请求体为 JSON
NSDictionary *params = @{@"username": @"test", @"pwd": @"123"};
[manager POST:@"https://api.example.com/login"
   parameters:params
      headers:nil
     progress:nil
      success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"登录成功: %@", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    NSLog(@"登录失败: %@", error);
}];

3. 文件下载

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSURLRequest *request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://example.com/file.zip"]];

NSURLSessionDownloadTask *downloadTask = [manager downloadTaskWithRequest:request
                                                                   progress:^(NSProgress *downloadProgress) {
    // 下载进度
    NSLog(@"进度: %.2f", downloadProgress.fractionCompleted);
} destination:^NSURL *(NSURL *targetPath, NSURLResponse *response) {
    // 指定文件保存路径
    NSURL *caches = [[NSFileManager defaultManager] URLForDirectory:NSCachesDirectory inDomain:NSUserDomainMask appropriateForURL:nil create:NO error:nil];
    return [caches URLByAppendingPathComponent:response.suggestedFilename];
} completionHandler:^(NSURLResponse *response, NSURL *filePath, NSError *error) {
    if (!error) {
        NSLog(@"下载完成: %@", filePath);
    }
}];
[downloadTask resume];

4. 文件上传(multipart/form-data)

AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
NSDictionary *params = @{@"userId": @"12345"};

[manager POST:@"https://api.example.com/upload"
   parameters:params
      headers:nil
constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
    // 从文件路径上传
    NSURL *fileURL = [NSURL fileURLWithPath:@"/path/to/file.jpg"];
    [formData appendPartWithFileURL:fileURL name:@"file" fileName:@"photo.jpg" mimeType:@"image/jpeg" error:nil];
    // 或从 NSData 上传
    // [formData appendPartWithFileData:imageData name:@"file" fileName:@"photo.jpg" mimeType:@"image/jpeg"];
} progress:^(NSProgress *uploadProgress) {
    NSLog(@"上传进度: %.2f", uploadProgress.fractionCompleted);
} success:^(NSURLSessionDataTask *task, id responseObject) {
    NSLog(@"上传成功: %@", responseObject);
} failure:^(NSURLSessionDataTask *task, NSError *error) {
    NSLog(@"上传失败: %@", error);
}];

5. 序列化器

序列化器 说明
AFJSONResponseSerializer(默认) 响应体自动解析为 JSON(NSDictionary/NSArray)
AFHTTPResponseSerializer 不解析,直接返回 NSData
AFXMLParserResponseSerializer 响应体解析为 NSXMLParser
AFXMLDocumentResponseSerializer(macOS) 响应体解析为 NSXMLDocument
AFPropertyListResponseSerializer 响应体解析为 plist
AFCompoundSerializer 组合多个序列化器

请求序列化器:

序列化器 说明
AFHTTPRequestSerializer(默认) 请求体为 form-urlencoded
AFJSONRequestSerializer 请求体为 JSON
AFPropertyListRequestSerializer 请求体为 plist
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
manager.requestSerializer = [AFJSONRequestSerializer serializer];  // 请求 JSON
manager.responseSerializer = [AFJSONResponseSerializer serializer]; // 响应 JSON(默认)
// 接受非 JSON 响应(如 HTML 错误页)
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"application/json", @"text/html", nil];

6. 网络状态检测

AFNetworkReachabilityManager *reachability = [AFNetworkReachabilityManager sharedManager];
[reachability setReachabilityStatusChangeBlock:^(AFNetworkReachabilityStatus status) {
    switch (status) {
        case AFNetworkReachabilityStatusReachableViaWWAN: NSLog(@"蜂窝网络"); break;
        case AFNetworkReachabilityStatusReachableViaWiFi: NSLog(@"WiFi"); break;
        case AFNetworkReachabilityStatusNotReachable:     NSLog(@"无网络"); break;
        case AFNetworkReachabilityStatusUnknown:          NSLog(@"未知"); break;
    }
}];
[reachability startMonitoring]; // 必须调用才开始检测

六、Alamofire(Swift 对应库)

Alamofire 是 AFNetworking 的 Swift 版本,基于 NSURLSession,API 更简洁:

import Alamofire

// GET 请求
AF.request("https://api.example.com/data").responseJSON { response in
    switch response.result {
    case .success(let value):
        print("成功: \(value)")
    case .failure(let error):
        print("失败: \(error)")
    }
}

// POST 请求(JSON)
let params = ["username": "test", "pwd": "123"]
AF.request("https://api.example.com/login", method: .post, parameters: params, encoding: JSONEncoding.default)
    .responseDecodable(of: UserResponse.self) { response in
        // 自动解码为 Codable 模型
    }

// 下载
let destination: DownloadRequest.Destination = { _, _ in
    let caches = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
    let fileURL = caches.appendingPathComponent("file.zip")
    return (fileURL, [.removePreviousFile, .createIntermediateDirectories])
}
AF.download("https://example.com/file.zip", to: destination)
    .downloadProgress { progress in
        print("进度: \(progress.fractionCompleted)")
    }
    .response { response in
        print("下载完成: \(response.fileURL)")
    }

// 上传
AF.upload(multipartFormData: { formData in
    formData.append(fileURL, withName: "file", fileName: "photo.jpg", mimeType: "image/jpeg")
}, to: "https://api.example.com/upload")
    .uploadProgress { progress in
        print("上传进度: \(progress.fractionCompleted)")
    }
    .responseJSON { response in
        print(response)
    }

// 网络状态
let reachability = NetworkReachabilityManager()
reachability?.startListening { status in
    switch status {
    case .reachable(.cellular): print("蜂窝")
    case .reachable(.ethernetOrWiFi): print("WiFi")
    case .notReachable: print("无网络")
    case .unknown: print("未知")
    }
}

七、三种方式对比

对比项 NSURLConnection NSURLSession AFNetworking
状态 iOS 9 起废弃 苹果推荐 第三方主流
底层 旧网络栈 现代网络栈 基于 NSURLSession
任务管理 Data/Download/Upload Task 封装 Task
后台下载 不支持 支持 支持(封装)
断点续传 手动 Range 原生 resumeData 封装
序列化 手动 手动 自动(JSON/XML/HTTP)
上传 手动构建 body Upload Task multipart 一行代码
网络检测 内置 Reachability
代码量
适用场景 维护老项目 所有项目(推荐) 快速开发

八、总结

  • NSURLConnection:iOS 早期网络 API,iOS 9 起废弃,基于 RunLoop 调度回调,子线程需手动处理 RunLoop,新项目不使用。
  • NSURLSession:iOS 7+ 推荐 API,三种任务类型(DataTask 普通请求/DownloadTask 文件下载/UploadTask 文件上传),支持后台下载、原生断点续传(cancelByProducingResumeData + downloadTaskWithResumeData)、配置灵活(default/ephemeral/background)。任务默认挂起,必须 resume;completionHandler 在子线程执行,UI 更新需回主线程。
  • 断点续传两种方式:① DownloadTask 原生 resumeData(推荐,简单,支持后台);② DataTask + Range 请求头手动实现(灵活,需管理文件和 Range)。
  • AFNetworking:基于 NSURLSession 的 OC 第三方库,3.0+ 移除了基于 NSURLConnection 的 AFHTTPRequestOperationManager,核心类 AFHTTPSessionManager;自动 JSON 序列化、multipart 上传一行代码、内置网络状态检测;Swift 对应库为 Alamofire。
  • 序列化器:请求序列化(HTTP form/JSON/plist)、响应序列化(JSON/HTTP/XML/plist/Compound),默认 JSON。
  • 选择建议:新项目用 NSURLSession 或 AFNetworking/Alamofire;需要简单封装和序列化用 AFNetworking;需要完全控制和后台下载用 NSURLSession;老项目维护才接触 NSURLConnection。

posted @ 2015-08-24 19:18  Mr.陳  阅读(1281)  评论(0)    收藏  举报