首页 » iOS编程(第4版) » iOS编程(第4版)全文在线阅读

《iOS编程(第4版)》21.1 Web服务

关灯直达底部

网页浏览器能通过HTTP协议和指定的Web服务器进行通信。以最简单的交互过程为例,浏览器向服务器发出请求,要求获取指定URL的内容。作为应答,服务器会返回相应的页面(通常是HTML文件和图片)。然后浏览器对这些返回的数据进行排版与显示。

对更复杂的交互过程,浏览器可以在发出的请求中包含其他参数,例如表单数据(form data)。服务器会处理这些参数并返回定制的或动态生成的网页。

Web浏览器的使用很广泛,而且经历了很长一段的发展时期。因此,与HTTP有关的技术都已经相当成熟:HTTP数据可以轻松通过多数的防火墙,Web服务器有很高的安全性并且执行效率很高。此外,用于开发Web应用的工具也越来越容易使用。

读者可以编写iOS客户端应用,通过HTTP协议与运行Web服务的服务器进行通信。这个服务器端称为Web服务。通过HTTP协议,iOS客户端可以与Web服务相互发出请求和给予响应。

HTTP协议没有规定传输数据的格式,所以客户端和服务端可以相互传输复杂的数据。这些传输数据的格式通常是JSON(JavaScript Object Notation)或XML。如果读者可以管理服务器,就可以随意选用数据传输格式。否则,就只能根据服务器支持的格式编写应用。

Nerdfeed的任务是向位于http://bookapi.bignerdranch.com的courses服务发起请求。本例中,courses服务将返回Big Nerd Ranch的最新在线课程,数据格式为JSON。

编写Nerdfeed应用

使用Empty Application模板创建一个iPad应用(选择Devices列表中的iPad)并将其命名为Nerdfeed,如图21-3所示(如果读者没有iPad,也可以使用iPad模拟器)。

图21-3 通过Empty Application模板创建iPad应用

在为Nerdfeed实现Web服务功能前,需要先构建一个简单的用户界面。创建一个新的NSObject子类并将其命名为BNRCoursesViewController。在BNRCoursesView- Controller.h中,将父类修改为UITableViewController,代码如下:

@interface BNRCoursesViewController : NSObject

@interface BNRCoursesViewController : UITableViewController

在BNRCoursesViewController.m中实现空的数据源方法,能使项目可以成功构建,代码如下:

- (NSInteger)tableView:(UITableView *)tableView

numberOfRowsInSection:(NSInteger)section

{

return 0;

}

- (UITableViewCell *)tableView:(UITableView *)tableView

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

return nil;

}

在BNRAppDelegate.m中,先创建一个BNRCoursesViewController对象,然后使用该对象创建一个UINavigationController对象,最后将UINavigationController对象设置为UIWindow的rootViewController。代码如下:

#import “BNRAppDelegate.h”

#import “BNRCoursesViewController.h”

@implementation BNRAppDelegate

- (BOOL)application:(UIApplication *)application

didFinishLaunchingWithOptions:(NSDictionary *)launchOptions

{

self.window =

[[UIWindow alloc] initWithFrame:[[UIScreen mainScreen] bounds]];

BNRCoursesViewController *cvc = [[BNRCoursesViewController alloc]

initWithStyle:UITableViewStylePlain];

UINavigationController *masterNav =

[[UINavigationController alloc] initWithRootViewController:cvc];

self.window.rootViewController = masterNav;

self.window.backgroundColor = [UIColor whiteColor];

[self.window makeKeyAndVisible];

return YES;

}

构建并运行应用,Nerdfeed应该会显示一个UINavigationBar对象和一个空的UITableView对象。

NSURL、NSURLRequest、NSURLSession和NSURLSessionTask

要从Web服务器获取数据,Nerdfeed需要使用NSURL、NSURLRequest、 NSURLSession- Task和NSURLSession四个类(见图21-4)。

图21-4 NSURL、NSURLRequest、NSURLSessionTask关系图

在与Web服务器进行通信的过程中,这些类各自扮演了重要的角色。

•NSURL对象负责以URL的格式保存Web应用的位置。对大多数Web服务,URL将包含基地址(base address)、Web应用名和需要传送的参数。

•NSURLRequest对象负责保存需要传送给Web服务器的全部数据,这些数据包括:一个NSURL对象、缓存方案(caching policy)、等待Web服务器响应的最长时间和需要通过HTTP协议传送的额外信息(NSMutableURLRequest是NSURLRequest的可变子类)。

•每一个NSURLSessionTask对象都表示一个NSURLRequest的生命周期。NSURLSessionTask可以跟踪NSURLRequest的状态,还可以对NSURLRequest执行取消、暂停和继续操作。NSURLSessionTask有多种不同功能的子类,包括NSURLSession- DataTask,NSURLSessionUploadTask和NSURLSessionDownloadTask。

•NSURLSession对象可以看作是一个生产NSURLSessionTask对象的工厂。可以设置其生产出的NSURLSessionTask对象的通用属性,例如请求头的内容(参见本章21.6节)、是否允许在蜂窝网络下发送请求等。NSURLSession对象还有一个功能强大的委托,可以跟踪NSURLSessionTask对象的状态、处理服务器的认证要求等。

构建URL与发送请求

不同的Web服务,请求格式也不同。Web服务没有一成不变的规则,读者需要查阅相关Web服务的文档,然后根据指定的格式构建请求。客户端发送的数据必须符合服务器的要求,才能得到相应的响应。

根据Big Nerd Ranch在线课程的Web服务要求,相应的URL示例如下:

<http://bookapi.bignerdranch.com/courses.json>

上述URL的基地址是bookapi.bignerdranch.com,Web服务是courses,服务器响应的数据格式是json。

Web服务请求根据实际需要会有多种构成格式。以上Web服务请求实际上访问了服务器端的courses路径(读者可以将基地址后的“/”类比为Mac文件系统中的目录路径),这类格式很常见——服务器端根据功能将Web服务分配到不同的路径,然后要求客户端以路径作为参数请求不同的Web服务。因此,以上Web服务请求的含义是:“请求所有在线课程信息,并以JSON格式返回”。

还有一种很常见的Web服务请求构成格式:

<http://baseURL.com/serviceName?argumentX=valueX&argumentY=valueY>

例如,如果要获取指定年份和月份的在线课程,那么可以构建如下Web服务请求:

http://bookapi.bignerdranch.com/courses.json?year=2014&month=11

URL字符串必须是URL安全的(URL-safe)。例如,在URL中,不允许出现空格字符和双引号,必须使用转义序列(escape-sequence)来替换这些字符:

NSString *search = @“Play some /”Abba/“”;

NSString *escaped =

[search stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];

// 转义后的字符串是“Play%20some%20%22Abba%22”

之后,如果需要对字符串解除转义,可以使用NSString的实例方法stringByRemovingPercentEncoding。

- (NSString *)stringByRemovingPercentEncoding

Big Nerd Ranch的服务器处理完来自客户端的请求后,会返回包含在线课程列表的JSON数据。BNRCoursesViewController对象(发送Web服务请求的对象)的UITableView对象需要显示各个在线课程的标题。

NSURLSession

NSURLSession有两种不同的含义:第一种含义指NSURLSession类;第二种含义指一组用于处理网络请求的API。为了区别这两种含义,本书会在第一种含义下直接使用“NSURLSession”,而在第二种含义下则使用“NSURLSession API”。

在BNRCoursesViewController.m的类扩展中添加一个属性,用于保存NSURLSession对象:

@interface BNRCoursesViewController ()

@property (nonatomic) NSURLSession *session;

@end

接下来覆盖BNRCoursesViewController.m的initWithStyle:,创建NSURLSession对象:

- (instancetype)initWithStyle:(UITableViewStyle)style

{

self = [super initWithStyle:style];

if (self) {

self.navigationItem.title = @“BNR Courses”;

NSURLSessionConfiguration *config =

[NSURLSessionConfiguration defaultSessionConfiguration];

_session = [NSURLSession sessionWithConfiguration:config

delegate:nil

delegateQueue:nil];

}

return self;

}

用于创建NSURLSession对象的工厂方法sessionWithConfiguration有三个参数:NSURLSessionConfiguration对象、委托和委托队列,本例中全部使用默认值即可。其中,第一个参数需要创建一个默认的NSURLSessionConfiguration对象,而第二个和第三个参数只需要传入nil就可以获得相应的默认值。

下面在BNRCoursesViewController.m中实现fetchFeed方法:首先创建一个NSURLRequest对象(用于连接bookapi.bignerdranch.com并查询在线课程列表),然后使用NSURLSession对象创建一个NSURLSessionDataTask对象,将NSURLRequest对象发送给服务器。代码如下:

- (void)fetchFeed

{

NSString *requestString =

@“http://bookapi.bignerdranch.com/courses.json”;

NSURL *url = [NSURL URLWithString:requestString];

NSURLRequest *req = [NSURLRequest requestWithURL:url];

NSURLSessionDataTask *dataTask =

[self.session dataTaskWithRequest:req

completionHandler:

^(NSData *data, NSURLResponse *response, NSError *error) {

NSString *json = [[NSString alloc] initWithData:data

encoding:NSUTF8StringEncoding];

NSLog(@“%@”, json);

}];

[dataTask resume];

}

创建NSURLRequest对象的过程非常简单,首先创建NSURL对象,然后使用NSURL对象创建NSURLRequest对象。

由于Nerdfeed只会发送单个简单请求,因此代码使用sharedSession方法获取使用默认配置的NSURLSession单例就可以了。实际上,如果应用中的请求种类较多,参数复杂,那么一个应用中也可能存在多个NSURLSession对象。

现在,只要为NSURLSession对象提供一个NSURLRequest对象和一个Block对象(completionHandler),NSURLSession对象就会创建一个NSURLSessionTask对象。由于Nerdfeed是从Web服务中请求数据,所以应该使用NSURLSessionTask的子类——NSURLSessionDataTask。NSURLSessionTask在刚创建的时候都处于暂停状态,所以需要手动调用resume(继续)方法,让NSURLSessionTask开始向Web服务发送请求。另外,completionHandler中暂时只向控制台中输出返回的JSON数据,下一节会介绍如何处理JSON数据。

Nerdfeed应该在创建BNRCoursesViewController对象后就发起网络请求。修改BNRCoursesViewController.m的initWithStyle:,调用fetchFeed方法:

- (instancetype)initWithStyle:(UITableViewStyle)style

{

self = [super initWithStyle:style];

if (self) {

self.navigationItem.title = @“BNR Courses”;

NSURLSessionConfiguration *config =

[NSURLSessionConfiguration defaultSessionConfiguration];

_session = [NSURLSession sessionWithConfiguration:config

delegate:nil

delegateQueue:nil];

[self fetchFeed];

}

return self;

}

构建并运行应用。确保iOS设备可以访问网络(如果是iOS模拟器,确保Mac可以访问网络),稍等片刻,应用会从Web服务获取JSON数据,然后将其转换为字符串并输出到控制台(如果控制台中没有任何输出,请检查Web服务的URL是否正确)。

JSON数据

读者可能会觉得控制台中输出的JSON数据非常复杂,但其实JSON的语法非常简单易懂。JSON只包含有限的几种基础对象,用于表示来自服务器的模型对象,例如数组、字典、字符串和数字。数组可能包含多个字符串、数字、字典或其他数组;而字典则可能包含一到多个键-值对,其中键是字符串,而值可能是字符串、数字、数组或其他字典。每个JSON文件都是由这些基础对象嵌套组合而成的。

以下是一个非常简单的JSON文件:

{

“name” : “Christian”,

“friends” : [“Aaron”, “Mikey”],

“job” : {

“company” : “Big Nerd Ranch”,

“title” : “Senior Nerd”

}

}

JSON中的字典通过花括号({和})表示,花括号内是字典的键-值对。可以看出,以上JSON文件以左花括号开始,右花括号结束,意味着JSON文件的顶层对象(top-level object)是一个字典,该字典包含三个键-值对,分别是name、friends和job。

字符串通过引号中的文本表示。在JSON字典中,字符串既可以作为键,也可以作为值。顶层字典name键的值是字符串“Christian”。

JSON中的数组通过中括号([和])表示。数组可以包含其他JSON对象。顶层字典friends键的值是一个字符串数组,包括Aaron和Mikey。

字典中还可以包含其他字典。顶层字典job键的值是另一个字典,包含两个键-值对,分别是company和title。

Nerdfeed将解析服务器返回的JSON数据,封装为在线课程模型对象,并将模型对象存储到courses属性中,如图21-5所示。

图21-5 在线课程模型对象图

解析JSON数据

Apple提供了专门用于解析JSON数据的类:NSJSONSerialization。NSJSON- Serialization可以将JSON数据中的对象转换为对应的Objective-C对象——字典会转换为NSDictionary对象;数组会转换为NSArray对象;字符串会转换为NSString对象;数字会转换为NSNumber对象。

打开BNRCoursesViewController.m,修改NSURLSessionDataTask的completion- Handler,使用NSJSONSerialization将JSON数据转换为Objective-C对象:

^(NSData *data, NSURLResponse *response, NSError *error) {

NSString *json = [[NSString alloc] initWithData:data

encoding:NSUTF8StringEncoding];

NSLog(@“%@”, json);

NSDictionary *jsonObject =

[NSJSONSerialization JSONObjectWithData:data

options:0

error:nil];

NSLog(@“%@”, jsonObject);

}

构建并运行应用,稍等片刻后检查控制台。这次控制台会输出解析后的JSON数据——NSLog对象可以格式化输出NSDictionary和NSArray,而jsonObject是NSDictionary对象,有一个NSString键“courses”,值是一个NSArray对象,包括所有在线课程信息。

当NSURLSessionDataTask完成网络请求之后,就可以使用NSJSONSerialization解析Web服务返回的JSON数据。图21-6显示了解析后的数据格式。

图21-6 解析后的JSON数据

在BNRCoursesViewController.m的类扩展中添加一个属性,用于保存在线课程数组。数组中的每一个元素都是一个NSDictionary对象,表示一项课程的详细信息。代码如下:

@interface BNRCoursesViewController ()

@property (nonatomic) NSURLSession *session;

@property (nonatomic, copy) NSArray *courses;

@end

然后修改NSURLSessionDataTask的completionHandler,代码如下:

^(NSData *data, NSURLResponse *response, NSError *error) {

NSDictionary *jsonObject =

[NSJSONSerialization JSONObjectWithData:data

options:0

error:nil];

NSLog(@“%@”, jsonObject);

self.courses = jsonObject[@“courses”];

NSLog(@“%@”, self.courses);

}

接下来修改UITableView的数据源方法,将各项课程的标题(title)显示在相应的UITableViewCell中。最后还需要覆盖viewDidLoad,向UITableView注册UITableView- Cell。代码如下:

- (void)viewDidLoad

{

[super viewDidLoad];

[self.tableView registerClass:[UITableViewCell class]

forCellReuseIdentifier:@“UITableViewCell”];

}

- (NSInteger)tableView:(UITableView *)tableView

numberOfRowsInSection:(NSInteger)section

{

return 0;

return self.courses.count;

}

- (UITableViewCell *)tableView:(UITableView *)tableView

cellForRowAtIndexPath:(NSIndexPath *)indexPath

{

return nil;

UITableViewCell *cell =

[tableView dequeueReusableCellWithIdentifier:@“UITableViewCell”

forIndexPath:indexPath];

NSDictionary *course = self.courses[indexPath.row];

cell.textLabel.text = course[@“title”];

return cell;

}

主线程

随着iPhone 4S中A5处理器的问世,具备多核处理器的iOS设备已经成为主流。多核处理器可以同时运行多段代码,每段代码都在一个独立的线程(thread)上运行,互不干扰。这种特征称为并发性(concurrency)。到目前为止,本书的代码都是在主线程(main thread)中运行的。主线程有时也称为用户界面线程(UI thread),这是由于修改用户界面的代码必须在主线程中运行。

当Web服务请求成功后,BNRCoursesViewController需要重新加载UITableView对象的数据(调用UITableView对象的reloadData方法)。默认情况下,NSURLSession- DataTask是在后台线程中执行completionHandler的,为了让reloadData方法在主线程中运行,可以使用dispatch_async函数。

在BNRCoursesViewController.m中,修改NSURLSessionDataTask的completion- Handler,在主线程中重新加载UITableView对象的数据:

^(NSData *data, NSURLResponse *response, NSError *error) {

NSDictionary *jsonObject =

[NSJSONSerialization JSONObjectWithData:data

options:0

error:nil];

self.courses = jsonObject[@“courses”];

NSLog(@“%@”, self.courses);

dispatch_async(dispatch_get_main_queue(), ^{

[self.tableView reloadData];

});

}

构建并运行应用。当Web服务请求成功后,应该可以在UITableView对象中看到Big Nerd Ranch的在线课程列表。