NSURLRequest
网络请求的关键的就是NSURLRequest类,它的实例表示了请求报文实体以及请求的缓存策略等等,各种网络框架的最终目标都是把这个对象编译成为请求报文发送出去。下面用一个实例来说明它的用法。
//1、设置url和请求方法 NSString *urlString = [NSString stringWithFormat:@"http://maplecode.applinzi.com"]; NSMutableURLRequest *request = [[NSMutableURLRequest alloc] init]; [request setURL:[NSURL URLWithString:urlString]]; [request setHTTPMethod:@"POST"]; //2、设置报文头 NSString *contentType = [NSString stringWithFormat:@"text/xml"]; [request addValue:contentType forHTTPHeaderField: @"Content-Type"]; //3、设置报文体 NSMutableData *postBody = [NSMutableData data]; [postBody appendData:[[NSString stringWithFormat:@"id=%@&password=%@",@"admin02",@"admin02"] dataUsingEncoding:NSUTF8StringEncoding]]; [postBody appendData:[[NSString stringWithFormat:@"<Request Action="Login">"] dataUsingEncoding:NSUTF8StringEncoding]]; [request setHTTPBody:postBody]; //4、发送请求报文并处理响应报文 NSHTTPURLResponse* urlResponse = nil; NSError *error = [[NSError alloc] init]; NSData *responseData = [NSURLConnection sendSynchronousRequest:request returningResponse:&urlResponse error:&error]; NSString *result = [[NSString alloc] initWithData:responseData encoding:NSUTF8StringEncoding];
注意到我们往往使用mutable的对象,方便修改。此外NSURLConnection类已经被建议废弃,现在可以使用NSURLSession创建task。
AFNetworking
1、基于NSOperation发送网络请求
本方法只适用于2.x版本的AFNetworking,新版本不再支持基于NSURLConnection的API。多线程往往用于实现异步网络请求,配合封装了通信接口的NSURLSession, CFNetwork, AFNetworking使用。下面重点介绍AFNetworking。
这个库集XML解析,Json解析,plist解析,数据流上传,下载,缓存等众多功能于一身,配合操作队列的用法如下:
NSString *str=[NSString stringWithFormat:@"https://alpha-api.app.net/stream/0/posts/stream/global"]; NSURL *url = [NSURL URLWithString:[str stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding]]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; AFHTTPRequestOperation *operation = [[AFHTTPRequestOperation alloc] initWithRequest:request]; [operation setCompletionBlockWithSuccess:^(AFHTTPRequestOperation *operation, id responseObject) { NSString *html = operation.responseString; NSData* data=[html dataUsingEncoding:NSUTF8StringEncoding]; id dict=[NSJSONSerialization JSONObjectWithData:data options:0 error:nil]; NSLog(@"获取到的数据为:%@",dict); }failure:^(AFHTTPRequestOperation *operation, NSError *error) { NSLog(@"发生错误!%@",error); }]; NSOperationQueue *queue = [[NSOperationQueue alloc] init]; [queue addOperation:operation];
继承关系:AFHTTPRequestOperation : AFURLConnectionOperation : NSOperation,也就是说AFHTTPRequestOperation类和NSInvocationOperation的使用方法是一致的,把它加入操作队列就可以了。
2、新版AFNetworking基于NSURLSession
如果需要细致的控制请求报文,那么对于低版本的AFNetworking,可以使用AFHTTPClient类,它的实例表示了一个客户端,可以发出GET/POST请求。不过对于新版来说,这个类已经不存在了,可以用AFHTTPSessionManager来代替发送GET/POST请求,而且基于NSURLSession,还可以很方便的实现全局配置和文件上传下载。
@interface AFHTTPSessionManager - (NSURLSessionDataTask *)GET:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *task, id responseObject))success failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure; - (NSURLSessionDataTask *)POST:(NSString *)URLString parameters:(id)parameters success:(void (^)(NSURLSessionDataTask *task, id responseObject))success failure:(void (^)(NSURLSessionDataTask *task, NSError *error))failure; @end
可以看出,用这个类来发送请求,甚至不需要事先生成代表请求报文的NSURLRequest对象,简化了操作过程,也不需要基于NSOperation,但是需要基于新的类NSURLSessionTask,比如AFNetworking 3.x下:
NSURL *URL = [NSURL URLWithString:@"http://example.com/resources/123.json"]; AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; [manager GET:URL.absoluteString parameters:nil progress:nil success:^(NSURLSessionTask *task, id responseObject) { NSLog(@"JSON: %@", responseObject); } failure:^(NSURLSessionTask *operation, NSError *error) { NSLog(@"Error: %@", error); }];
当下载任务结束后,怎么样在回调block中使用task实例和responseObject呢,我们只需要看一看一个task的创建和使用过程:
NSURLSession *session = [NSURLSession sharedSession]; [[session dataTaskWithURL:[NSURL URLWithString:londonWeatherUrl] completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) { // handle response }] resume];
一个task需要持有一个block的引用,作为数据传输结束的处理任务,并且可以调用resume方法启动下载任务。
原生NSURLSession
1、异步请求
依赖于第三方库并不总是希望的情况,比如作为一个有轻微强迫症的开发者,总是希望工程尽可能简单。NSURLSession类本身提供了非常清晰方便的接口,支持网络任务的resume, suspend, cancel, invalidate等等,支持文件直接上传下载,如果可以直接对NSURLSession进行简单封装的处理,就不愿意去依赖AFNetworking。注意头文件中的语句:
@property (readonly, retain) NSOperationQueue *delegateQueue;
这说明NSURLSession类的实例也是通过操作队列完成网络操作,并且以retain方式拥有一个操作队列作为委托对象,因此程序员并不需要在代码中创建NSOperationQueue对象了。
一个NSURLSession类的实例表示一系列会话的集合,程序员可以用一个NSURLSession的实例创造一个task对象来表示网络传输任务,正如上文中的代码片段可以建立一个异步网络请求,session可以维护这个task,并且session对象可以在网络传输结束后,把这个task的回调block放到delegateQueue中执行。
NSURLSession和NSURLSessionDataTask的关系,正是工厂设计模式的一个体现。
2、同步请求
如果程序员要以同步方式完成网络操作,过去通过 NSURLConnection.sendSynchronousRequest() 方法能同步请求数据。从iOS9起,苹果建议废除 NSURLConnection,使用 NSURLSession 代替 NSURLConnection,那么应该怎么办呢?使用信号、信号量就可以实现
public static func requestSynchronousData(request: NSURLRequest) -> NSData? { var data: NSData? = nil let semaphore: dispatch_semaphore_t = dispatch_semaphore_create(0) let task = NSURLSession.sharedSession().dataTaskWithRequest(request, completionHandler: { taskData, _, error -> () in data = taskData if data == nil, let error = error {print(error)} dispatch_semaphore_signal(semaphore); }) task.resume() dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER) return data }
或者用Objective-C的方式
@implementation NSURLSession (Synchronous) + (NSData *)requestSynchronousDataWithRequest:(NSURLRequest *)request{ __block NSData * data; dispatch_semaphore_t sem = dispatch_semaphore_create(0); void (^completion)(NSData * , NSURLResponse * , NSError * ) = ^(NSData * taskData, NSURLResponse * response, NSError * error){ data = taskData; dispatch_semaphore_signal(sem); }; NSURLSessionDataTask * task = [[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:completion]; [task resume]; dispatch_semaphore_wait(sem,DISPATCH_TIME_FOREVER); return data; } @end
由于completion block中的内容不会被主线程执行,而是被其他线程执行,在更新UI时可能会出问题甚至crash,比如当实例化UIWebView的时候,会发出错误:
Tried to obtain the web lock from a thread other than the main thread or the web thread.
This may be a result of calling to UIKit from a secondary thread. Crashing now...
用同步的方式就可以解决这个问题。或者就要使用GCD或者performSelector法某些代码放到主线程执行。
网络请求快捷方式
很多类可以直接获取JSON数据,图片数据等。在UI上,用SDWebImage模块中的分类实现了快捷方法,可以借助 [imageView setImageWithURL: placeholderImage:],可以直接请求需要显示的图片,并且有各种策略选项,减少冗长代码。
封装block
在特定业务环境下,网络请求往往存在大量重复代码,时常需要封装一些以block作为参数的函数。所以最后写一点关于block的感悟:
如果一个函数以block作为参数,那么这个函数的最终目标就是要生成这个block的参数。
这并不难于理解,因为block用于回调,我们构造通用函数,自然就是要构造出正确的值作为参数,然后才能调用传进来的block。同时,一个以block为参数的函数的形式上的返回值往往是不重要的,通常是void。理解这一点有助于实现封装避免重复代码。