第2章 网络编程
学习目标
了解网络的基本概念,包括URL、TCP/IP、Socket
理解NSURLConnection的工作原理
掌握数据解析的原理,会解析XML和JSON文档
掌握HTTP请求,会提交GET和POST请求
掌握文件上传与下载的原理
- 掌握第三方框架,会使用SDWebImage和AFNetworking
在移动互联网时代,几乎所有的应用程序都离不开网络,如QQ、微博、百度地图等,这些应用持续地通过网络进行数据更新,使应用保持着新鲜与活力。一旦没有了网络,应用就缺失了数据的变化,即便外观再华丽,终将只是一潭死水。网络编程是一种实时更新应用数据的常用手段,本章将针对网络编程的内容进行详细的讲解。
2.1 网络基本概念
如果数据不在本地,而是放在远程服务器上,那么如何获得这些数据呢?服务器能给我们提供一些服务,这些服务大多数都是基于超级文本传输协议(HTTP)的。HTTP基于请求和应答,需要的时候建立连接提供服务,不需要的时候断开连接。本节将针对网络编程的一些基本概念进行详细的介绍。
2.1.1 网络编程的原理
网络编程,就是通过使用套接字来达到进程间通信目的的技术。接下来,通过一张示意图来描述网络编程的工作机制,如图2-1所示。
图2-1展示了网络编程的流程,在网络编程中,有如下几个比较重要的概念。
(1)客户端(Client):移动应用(iOS、Android)。
(2)服务器(Server):为客户端提供服务、提供数据、提供资源的机器。
(3)请求(Request):客户端向服务器索取数据的一种行为。
(4)响应(Response):服务器对客户端的请求做出的反应,一般指返回数据给客户端。
由图2-1可知,客户端要想访问数据,首先要提交一个请求,用于告知服务器想要的数据。服务器接收到请求后,根据这个请求到数据库查找相应的资源,无论服务器是否成功拿到资源,都会将结果返回给客户端,这个过程就是响应。
图2-1 网络编程的示意图
值得一提的是,网络上所有的数据都是二进制数据,并且以二进制流的形式从一个节点到另一个节点。
2.1.2 URL介绍
URL的全称是Uniform Resource Locator,即统一资源定位符,通过一个URL可以找到互联网上唯一的资源,类似于计算机上一个文件的路径。为了大家更好地理解,接下来,通过图2-2来描述。
图2-2 URL示例
图2-2展示了一个URL的示例,实际上,上述URL省略了端口号,一个完整的URL是由4部分组成,分别是协议、IP地址、端口和路径,接下来,针对这几部分进行详细讲解。
1.协议
指定使用的传输协议,就可以告诉浏览器如何处理将要打开的文件。不同的协议(Protocol)表示不同的资源查找以及传输方式,最常用的协议如表2-1所示。
表2-1 常见的协议
常见协议 |
代表类型 |
示例 |
---|---|---|
File |
访问本地计算机的资源 |
file:///Users/itcast/Desktop/ book/basic.html |
FTP |
访问共享主机的文件资源 |
ftp://ftp.baidu.com/movies |
HTTP |
超文本传输协议,访问远程网络资源 |
http://image.baidu.com/channel/wallpaper |
HTTPS |
安全的SSL加密传输协议,访问远程网络资源 |
https://image.baidu.com/channel/wallpaper |
Mailto |
访问电子邮件地址 |
mailto:null@itcast.cn |
表2-1列举了一些常见的协议,最常用的就是http协议,它规定了客户端和服务器之间的数据传输格式,使客户端和服务器能够有效地进行数据沟通。值得一提的是,file后面无需添加主机地址。
2.IP地址
IP地址(Hostname)被用来给Internet上的每台电脑一个编号,也叫主机地址,但是IP地址不容易记忆。例如,打开Safari,在地址栏中输入“http://180.97.33.107”,单击“return”键打开了百度的首页,这表示该地址就是百度的IP地址,只是这个地址不易被人们记忆,故使用域名www.baidu.com替代以访问网站,相当于一个速记符号。
3.端口
IP地址后面有时还跟一个冒号和一个端口号,这是为了在一台设备上运行多个程序,人为地设计了端口(Port)的概念,类似于公司内部的分机号码。每个网络程序,无论是客户端还是服务器端,都对应一个或多个特定的端口号,常用的端口号如表2-2所示。
表2-2 服务器的常见端口号
协议 |
端口 |
说明 |
全拼 |
---|---|---|---|
HTTP |
80 |
超文本传输协议 |
Hypertext transfer protocol |
HTTPS |
443 |
超文本传输安全协议 |
Hyper Text Transfer Protocol over Secure Socket Layer |
FTP |
20,21,990 |
文件传输协议 |
File Transfer Protocol |
POP3 |
110 |
邮局协议(版本3) |
Post Office Protocol - Version 3 |
SMTP |
25 |
简单邮件传输协议 |
Simple Mail Transfer Protocol |
telnet |
23 |
远程终端协议 |
teletype network |
表2-2列举了一些常见的端口号。由表可知,每个传输协议都有默认的端口号。它是一个整数,如果输入时省略,则会使用方案的默认端口。若要采用非标准的端口号,这时的URL是不能省略端口号一项的。
4.路径
路径(Path)是由0或者多个“/”符号隔开的字符串,一般用于表示主机上的一个目录或者文件的地址。
总而言之,一个完整的URL是由协议、主机地址、端口号、路径4个部分组成,基本格式如下。
协议:// 主机地址:端口号 /路径
2.1.3 TCP/IP和TCP、UDP
TCP/IP(Transmission Control Protocol/Internet Protocol,传输控制协议/因特网互联协议)是一种网络通信协议,它规范了网络上的所有通信设备,尤其是一个主机和另一个主机之间的数据往来格式及传送方式。提到协议分层,很容易联想到OSI的七层协议经典架构,而基于TCP/IP的参考模型将协议分成4个层次。两者的比较如图2-3所示。
图2-3所示是OSI模型和TCP/IP模型的对照图。由图可知,TCP/IP的层次比较简单,共分为4层,分别为应用层、传输层、网络互连层和网络接口层,详细内容如下。
(1)应用层:应用层对应于OSI参考模型的高层,主要负责应用程序的协议,如HTTP、FTP。
(2)传输层:传输层对应于OSI参考模型的传输层,负责为应用层实体提供端到端的通信功能。该层定义了两个主要的协议,分别为传输控制协议(TCP)和用户数据报协议(UDP)。
图2-3 OSI模型与TCP/IP模型
(3)网络互连层:网络互连层对应于OSI参考模型的网络层,主要用于将传输的数据进行分组,并且将分组后的数据发送到目标计算机或者网络。该层包含网际协议(IP)、地址解析协议(ARP)、互联网组管理协议(IGMP)、互联网控制报文协议(ICMP)4个主要协议。其中,IP是国际互联层最重要的协议,它提供的是一个不可靠、无连接的数据报传递服务。
(4)网络接口层:网络接口层对应于OSI参考模型的数据链路层、物理层,负责监听数据在主机与网络之间的交换。
前面已经提到过,关于传输层有两个非常重要的协议,分别是TCP和UDP。其中,TCP是面向连接的通信协议,而UDP是面向非连接的通信协议,详细内容如下。
1.面向连接的TCP
面向连接是指正式通信前必须要与对方建立起连接,例如,你给别人打电话,只有等到线路接通,对方拿起话筒才能相互通话。TCP(Transmission Control Protocol,传输控制协议)是基于连接的协议,也就是说在正式收发数据之前,必须和对方建立可靠的连接。
一个TCP连接必须要经过“三次握手”才能建立起来,通信完成时需要拆除连接,关于连接建立和连接终止这两个过程,具体内容如下。
(1)连接建立
建立连接的流程是:主动方发出SYN连接请求以后,等待对方回答SYN+ACK,并且最终对对方的SYN执行ACK确认。这为两台计算机之间可靠无差错的数据传输提供了基础,流程如图2-4所示。
图2-4展示了TCP连接建立的过程,大体分为如下3个步骤。
① 客户端发送SYN报文给服务器端,进入SYN_SEND状态。
② 服务器端收到SYN报文,回应一个SYN +ACK报文,进入SYN_RECV状态;
③ 客户端收到服务器端的SYN报文,回应一个ACK报文,进入Established状态。
经历上面的3个步骤,完成了三次握手,客户端与服务器端成功地建立了连接,这时就可以传输数据了。
(2)连接终止
建立一个连接需要三次握手,而终止一个连接要经过四次握手,这是由于TCP的半关闭所造成的,即从执行被动关闭的一端到执行主动关闭的一端流动数据是可能的。具体过程如图2-5所示。
图2-5展示了TCP连接终止的过程,大体分为如下4个步骤。
① 一个应用程序首先调用close,该端就执行了“主动关闭”(active close),于是该端的TCP发送一个FIN分节,表示数据发送完毕。
② 接收到FIN的另一端执行“被动关闭”(passive close),这个FIN由TCP确认。
图2-4 连接建立示意图
图2-5 连接终止
③ 一段时间后,接收到这个文件结束符的应用进程将调用close关闭它的套接字。这导致它的TCP也发送一个FIN。
④ 接收这个最终FIN的原发送端TCP(即执行主动关闭的那一端)确认这个FIN。
2.面向非连接的UDP
“面向非连接”是指在正式通信前不必与对方先建立连接,不管对方状态如何均可以直接发送。与手机发送短信非常相似,仅输入对方的号码就能够发送信息。
UDP(User Data Protocol,用户数据报协议)与TCP正好相反,它是面向非连接的协议,无需与对方建立连接,直接把数据包发送过去即可。因此,UDP适用于一次只传递少量数据,且可靠性要求不高的应用环境,如QQ。
综上所述,TCP和UDP虽然都用于传输,但是两者却有着各自独特的特点,如表2-3所示。
表2-3 TCP和UDP的区别
协议 |
TCP(传输控制协议) |
UDP(用户数据报协议) |
---|---|---|
是否连接 |
建立连接,形成传输数据的通道 |
将数据源和目的封装成数据包中,不需要建立连接 |
应用场合 |
连接中进行大数据传输(数据大小不受限制) |
少量数据(每个数据报的大小限制在64KB之内) |
传输可靠性 |
通过三次握手完成连接,是可靠协议,安全送达 |
只管发送,不确定对方是否接收到。因为不需建立连接,因此也是不可靠协议 |
速度 |
速度慢 |
速度快 |
从表2-3中可以看出,TCP和UDP各有所长、各有所短,适用于不同要求的通信环境。由于TCP面向连接的特性,这就保证了传输数据的安全性,故它是一个被广泛采用的协议。
2.1.4 Socket介绍
在网络中,两个程序之间是通过一个双向的通信连接来实现数据交换的。这个连接的一端称为一个Socket,又称“套接字”,包含了终端的IP地址、端口和传输协议等信息,是系统提供的用于实现网络通信的方法。
Socket是对TCP/IP的封装,但它并不是一个协议,只是给程序员提供了一个发送消息的接口,程序员使用这个接口提供的方法来发送和接收消息。网络通信其实就是Socket之间的通信,数据在两个Socket之间通过IO传输。接下来,通过一张图来描述Socket通信的流程,如图2-6所示。
图2-6 基于TCP的Socket通信
从图2-6中可以看出,左侧是客户端,右侧是服务器端,开发时注意力要集中在客户端。首先通过socket()建立一个Socket对象,connect()建立一个到服务器的连接,然后通过send()给服务器发送数据,发送完成后等待服务器响应,服务器根据接收到的数据也做出一个响应,这样可以一直循环,最后执行close()关闭即可。
要想实现Socket的通信,大致需要经历3个步骤,分别是创建一个Socket并建立连接、发送和接收信息、断开连接,详细介绍如下。
1.创建Socket,建立连接
首先,创建一个Socket对象,通过socket()函数来实现。该函数的定义格式如下:
int socket(int domain, int type, int protocol);
在上述定义中,该函数包含3个int类型的参数,针对这3个参数的介绍如下。
(1)domain:协议域或者协议族,它决定了Socket的地址类型,通信中必须采用对应的地址,例如,AF_INET决定了要用IPv4地址(32位的)与端口号(16位的)的组合。
(2)type:指定Socket类型,常用的类型有SOCK_STREAM、SOCK_DGRAM、SOCK_ RAW、SOCK_PACKET等。
(3)protocol:指定协议,常用的协议有IPPROTO_TCP、IPPROTO_UDP、IPPROTO_ SCTP、IPPROTO_TIPC,分别对应TCP、UDP、STCP、TIPC传输协议。
需要注意的是,type和protocol不能够随意组合,若第3个参数为0时,会自动选择第2个参数类型对应的默认协议。一旦返回值大于0时,则表示创建成功。接下来,需要建立连接,通过一个connect()函数实现,定义格式如下:
int PASCAL FAR connect( SOCKET s, const struct sockaddr FAR* name, int namelen);
在上述定义中,该函数包含3个参数,其中第1个参数表示客户端的Socket,第2个参数是一个指向数据结构sockaddr的指针,它包括目的端口和IP地址,表示服务器的结构体地址,第3个参数表示该结构体的长度,返回值为0表示连接成功。
值得一提的是,系统针对Socket开发提供了一个辅助工具NetCat,是可以通过终端来调试和检查网络的工具包。进入终端,输入如下命令:
nc –lk 端口号
一旦在终端中输入上述命令,就会始终监听本地计算机该端口号的往来数据。
2.发送和接收信息
当连接建立成功之后,就可以发送和接收信息了。发送信息通过send()函数实现,定义格式如下:
ssize_t send(int, const void *, size_t, int) __DARWIN_ALIAS_C(send);
从上述格式看出,该函数包含4个参数,其中,第1个参数表示客户端的Socket,第2个参数表示发送内容的地址,第3个参数表示发送内容的长度,第4个参数表示发送内容的标志,一般为0。如果发送成功,则返回信息内容的字节数。
客户端将信息发送给服务器后,服务器端会接收这个信息,通过recv()函数实现,定义格式如下:
ssize_t recv(int, void *, size_t, int) __DARWIN_ALIAS_C(recv);
在上述定义中,该函数包含4个参数,其中,第1个参数表示客户端的Socket,第2个参数表示接收内容的缓冲地址,第3个参数表示接收内容的长度,第4个参数表示接收标志,若为0,表示阻塞式,即会一直等待服务器返回数据。
3.断开连接
给服务器发送完信息,服务器回复了信息后,需要断开连接,通过close()函数实现,定义格式如下:
int close(int);
2.1.5 实战演练——Socket聊天
Socket提供了发送和接收信息的接口,通过这个接口实现了客户端与服务器端的通信。为了大家更好地理解,接下来,通过一个项目演示如何实现Socket聊天,具体步骤如下。
1.创建工程,设计界面
(1)新建一个Single View Application应用,命名为01-Socket聊天。进入Main.storyboard,从对象库拖曳1个Label、2个Button、2个View、3个Text Field,其中,View表示容器视图,用于放置其他的小控件,2个Button的Title分别为“连接”和“发送”。
(2)一旦屏幕的尺寸发生改变,UI元素的位置和大小也需要做出相应的调整,这时就会用到自动布局,后面章节会有详细地介绍。在平面直角坐标系中,要想准确描述一个视图的位置需要确定以下4个布局属性,即水平位置X(左侧)、垂直位置Y(顶部)、宽度W、高度H。单击编辑窗口右下角的第2个“pin”按钮,弹出如图2-7所示的窗口。
从图2-7中可以看出,可以通过该窗口来指定一个控件的位置。若一个视图要想在垂直或者水平方向上居中显示,需要添加对齐约束。单击编辑窗口右下角的第1个“Align”按钮,弹出如图2-8所示的窗口。
图2-7 添加约束的窗口
图2-8 添加对齐的窗口
(3)以容器视图View为例,该视图应该距离顶部、左侧、右侧的位置固定,高度是固定值,这样依次确定了Y值、X值、W值、H值。选中程序界面的任意一个容器View,单击“pin”按钮,在该窗口上进行设置,如图2-9所示。
图2-9中所示文本框的数值都是自动检测的,依次将距离顶部、左侧、右侧的虚线单击成实线,勾选“Height”对应的复选框,单击“Add 4 Constraints”按钮,这样就成功地确定了View的位置。
(4)按照以上方式,完成其他控件约束的添加。单击运行按钮,模拟器自动根据“iPhone 6”的运行方案,弹出一个4.7英寸的模拟器,如图2-10所示。
2.创建控件对象的关联
(1)单击Xcode右上角的图标,进入控件与代码关联的界面,依次给3个Text Field和1个Label添加4个属性,分别命名为hostText、portText、msgText、recvLabel,用于表示主机名、端口号、发送的信息、回复的信息。
(2)依次选中2个Button,以同样的方式,分别添加两个单击事件,命名为conn、send。
3.实现Socket聊天
按照实现Socket通信的3个步骤,模拟完成一个客户端与服务器端聊天的功能,详细步骤介绍如下。
图2-9 给View添加约束
图2-10 设计好的界面
(1)自定义一个方法,通过传入一个IP地址和端口号连接到服务器,具体代码如下:
1 #import "ViewController.h"
2 #import <sys/socket.h>
3 #import <netinet/in.h>
4 #import <arpa/inet.h>
5 @interface ViewController ()
6 // 主机名
7 @property (weak, nonatomic) IBOutlet UITextField *hostText;
8 // 端口号
9 @property (weak, nonatomic) IBOutlet UITextField *portText;
10 // 发送的信息
11 @property (weak, nonatomic) IBOutlet UITextField *msgText;
12 // 回复的信息
13 @property (weak, nonatomic) IBOutlet UILabel *recvLabel;
14 @property (nonatomic, assign) int clientSocket; // Socket
15 @end
16 @implementation ViewController
17 /**
18 * 连接到服务器
19 */
20 - (BOOL)connectToHost:(NSString *)host port:(int)port{
21 // 1. 创建Socket对象
22 self.clientSocket = socket(AF_INET, SOCK_STREAM, 0);
23 // 2. 建立连接
24 struct sockaddr_in serverAddress;
25 serverAddress.sin_family = AF_INET; // 协议族
26 // IP,查找机器
27 serverAddress.sin_addr.s_addr = inet_addr(host.UTF8String);
28 serverAddress.sin_port = htons(port); //端口,查找程序
29 return (connect(self.clientSocket,
30 (const struct sockaddr *)&serverAddress,
31 sizeof(serverAddress)) == 0);
32 }
33 @end
在上述代码中,第2~4行代码导入了必要的头文件,第24~28行代码声明了一个服务器结构体,并设置了该服务器的协议族、IP地址、端口。
(2)自定义一个方法,用于客户端向服务器端发送一条信息,服务器端向客户端回复一条信息,具体代码如下:
1 /**
2 * 发送和接收
3 */
4 - (NSString *)sendAndRecv:(NSString *)message
5 {
6 // 1.发送信息
7 ssize_t sendLen = send(self.clientSocket, message.UTF8String,
8 strlen(message.UTF8String), 0);
9 // 2.接收信息
10 // 2.1 定义一个数组
11 uint8_t buffer[1024];
12 ssize_t recvLen = recv(self.clientSocket, buffer, sizeof(buffer), 0);
13 // 2.2 获取服务器返回的二进制数据
14 NSData *data = [NSData dataWithBytes:buffer length:recvLen];
15 // 2.3 转换为字符串
16 NSString *str = [[NSString alloc] initWithData:data
17 encoding:NSUTF8StringEncoding];
18 return str;
19 }
(3)自定义一个断开连接的方法,用于中断之前建立的连接,代码如下:
1 /**
2 * 断开连接
3 */
4 - (void)disconnection
5 {
6 close(self.clientSocket);
7 }
(4)单击“连接”按钮,提示连接“成功”或者“失败”的信息;单击“发送”按钮,“发送”按钮自动改为不可用状态,将接收到的信息显示到标签上,具体代码如下:
1 // 单击“连接”后执行的行为
2 - (IBAction)conn {
3 BOOL result = [self connectToHost:self.hostText.text
4 port:self.portText.text.intValue];
5 self.recvLabel.text = result ? @"成功" : @"失败";
6 }
7 // 单击“发送”按钮后执行的行为
8 - (IBAction)send {
9 self.recvLabel.text = [self sendAndRecv:self.msgText.text];
10 }
4.运行程序
(1)单击Xcode工具的运行按钮,在模拟器上运行程序。程序运行成功后,打开终端,输入nc –lk 12345。单击模拟器的“连接”按钮,底部的标签提示“成功”字样;在中间的文本框输入“hello”,单击“发送”按钮,该按钮呈不可用状态,这时终端成功监听到了“hello”,如图2-11所示。
(2)图2-11中的终端中输入“hi”,单击“return”键。这时,模拟器的底部标签提示“hi”,成功地实现了Socket聊天,运行结果如图2-12所示。
图2-11 程序运行的部分场景图
图2-12 程序运行的结果图
2.2 原生网络框架NSURLConnection
iOS 2.0推出了发送HTTP请求的一种方案NSURLConnection,至今已经有十余年的历史。NSURLConnection是一种最古老的、最经典的、最直接的方案,迄今为止没有做过太大的改动。尽管iOS 9已经废弃了NSURLConnection,但是作为一个资深的iOS程序员,是有必要了解细节的。本节将针对NSURLConnection的相关内容进行简单的介绍。
2.2.1 NSURLRequest类
一个NSURLRequest对象就表示一个请求,通过一个URL来创建一个请求对象,为此,NSURLRequest类提供了初始化的方法,定义格式如下:
// 创建并返回一个URL请求,指向一个指定的URL,采用默认缓存策略和超时响应时长
+ (instancetype)requestWithURL:(NSURL *)URL;
//创建并返回一个初始化的URL请求,采用指定的缓存策略和超时时长
+ (instancetype)requestWithURL:(NSURL *)URL
cachePolicy:(NSURLRequestCachePolicy)cachePolicy
timeoutInterval:(NSTimeInterval)timeoutInterval;
//返回一个URL请求,指向一个指定的URL,采用默认的缓存策略和超时响应时长
- (instancetype)initWithURL:(NSURL *)URL;
//返回一个URL请求,采用指定的缓存策略和超时时长
- (instancetype)initWithURL:(NSURL *)URL
cachePolicy:(NSURLRequestCachePolicy)cachePolicy
timeoutInterval:(NSTimeInterval)timeoutInterval;
值得一提的是,默认的缓存策略是NSURLRequestUseProtocolCachePolicy,默认的超时时长是60s。要想指定缓存策略,需要传入一个NSURLRequestCachePolicy类型的值,这是一个枚举类型,包含如下几个值。
(1)NSURLRequestUseProtocolCachePolicy:默认的缓存策略,如果没有缓存,则直接到服务器端获取;如果有缓存,会根据response中的Cache-Control字段判断下一步操作。
(2)NSURLRequestReloadIgnoringLocalCacheData:忽略本地缓存数据,直接到服务器端请求数据。
(3)NSURLRequestReloadIgnoringLocalAndRemoteCacheData:忽略本地缓存、代理服务器和其他中介的缓存,直接请求源服务器端的数据。
(4)NSURLRequestReloadIgnoringCacheData:已经被(2)取代。
(5)NSURLRequestReturnCacheDataElseLoad:如果有缓存就使用,不管其有效性;如果没有缓存就请求服务端。
(6)NSURLRequestReturnCacheDataDontLoad:只加载本地缓存数据,如果没有,就表示失败。
(7)NSURLRequestReloadRevalidatingCacheData:缓存数据必须得到服务端确认有效后才使用。
NSURLRequest对象封装了一个请求,它保存着发给服务器的全部数据,包括一个NSURL对象、请求方法、请求头、请求体、请求超时等。为此,NSURLRequest类声明了一些属性,如表2-4所示。
表2-4 NSURLRequest类的常用属性
属性声明 |
功能描述 |
---|---|
@property (readonly, copy) NSString *HTTPMethod; |
设置请求的方法 |
@property (readonly, copy) NSData *HTTPBody; |
设置请求体 |
@property (readonly) NSTimeInterval timeoutInterval; |
设置请求超时等待时间,超过这个时间就表示请求失败 |
表2-4列举了NSURLRequest类的一些常用属性,由表可知,这3个属性都是readonly修饰的,仅仅只能生成对应的get方法。针对这个情况,iOS提供了一个NSURLRequest类的子类NSMutableURLRequest,表示可变的URL请求,它重新声明了这3个属性,修改为可读写类型。另外,该类还声明了一个常用的方法,用于设置请求头,定义格式如下:
- (void)setValue:(NSString *)value forHTTPHeaderField:(NSString *)field;
例如,如果告诉服务器要使用的设备是iPhone,传入第1个参数是“iPhone”,第2个参数为“User-Agent”即可。
2.2.2 NSURLConnection介绍
为了读取服务器的数据或者向服务器提交数据,iOS提供了一个NSURLConnection类,用于建立客户端与服务器的连接。NSURLConnection类通过使用一个NSURLRequest对象,向远程服务器发送同步或者异步请求,并收集来自服务器的响应数据,如图2-13所示。
图2-13 NSURLConnection连接的示意图
从图2-13中可以看出,NSURLConnection是以NSURLRequest为载体,建立客户端与服务器的连接的。要想使用NSURLConnection类发送一个请求,大体可分为如下3个步骤。
(1)创建一个NSURL对象,设置请求的路径。
(2)根据NSURL创建一个NSURLRequest对象,设置请求头和请求体。
(3)使用NSURLConnection发送NSURLRequest对象。
值得一提的是,要想使用NSURLConnection发送请求,通常可以通过同步请求和异步请求两种方式实现,它们的定义格式如下:
//发送同步请求
+ (NSData *)sendSynchronousRequest:(NSURLRequest *)request returningResponse:
(NSURLResponse **)response error:(NSError **)error;
//发送异步请求
+ (void)sendAsynchronousRequest:(NSURLRequest*) request queue:(NSOperationQueue*)
queue completionHandler: (void (^)(NSURLResponse* response, NSData* data, NSError* connection Error)) handler;
其中,第1个方法是用于发送同步请求的,第2个方法是用于发送异步请求的,针对它们的详细介绍如下。
1.同步请求
该方法有1个NSData类型的返回值,表示根据URL请求返回的数据。此外,该方法需要传递3个参数,针对它们的介绍如下。
(1)request:表示加载的URL请求。
(2)response:表示服务器返回的URL响应头信息。
(3)error:如果处理请求时出现错误,可使用该参数。
需要注意的是,这个方法会阻塞当前线程,直至服务器返回数据,才能执行其他的操作。通常情况下,如果要请求大量数据或者网络不畅时不建议使用。
2.异步请求
开发者无需考虑开启线程,或者创建队列。异步请求的方法没有返回值,该方法包含3个参数,针对它们的介绍如下。
(1)request:表示加载的URL请求。
(2)queue:completionHandler会运行在这个队列。
(3)handler:请求回调的block。该block包含3个参数,其中,response表示服务器的响应,通常用于下载功能;data表示服务器返回的二进制数据,这是开发者最关心的内容;connectionError表示连接错误,任何网络访问都有可能出现错误。
这个方法会将之前创建好的request异步发送给服务器,当接收到服务器的响应之后,由queue负责调度completionHandler的执行。completionHandler表示网络访问已经结束,接收到服务器响应数据后的回调方法。
根据对服务器返回数据处理方式的不同,可以分为两种情况,分别为block回调和代理,上述方式属于block回调。除此之外,还可以通过给NSURLConnection设定一个代理,监听NSURLConnection与服务器响应的状态,为此,NSURLConnection类提供了3个方法,它们的定义格式如下。
+ (NSURLConnection*)connectionWithRequest:(NSURLRequest *)request delegate:(id)delegate;
- (id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate;
- (id)initWithRequest:(NSURLRequest *)request delegate:(id)delegate startImmediately:(BOOL) startImmediately;
在上述定义中,第1个和第2个方法均需要传递两个参数,第3个方法需要传递3个参数,其中,startImmediately表示是否立即下载数据,若设置为NO,需要调用start方法开始发送请求。若要监听服务器返回的数据,前提是要遵守NSURLConnectionDataDelegate协议,该协议包含如下几个常用方法:
//开始接收到服务器的响应时调用
- (void)connection:(NSURLConnection *)connection didReceiveResponse:
(NSURLResponse *)response;
//接收到服务器返回的数据时调用(若返回的数据比较大时会调用多次)
- (void)connection:(NSURLConnection *)connection didReceiveData:
(NSData *)data;
//服务器返回的数据完全接收完毕后调用
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
//请求出错时调用,如请求超时
- (void)connection:(NSURLConnection *)connection didFailWithError:
(NSError *)error;
在开发中,代理对象只要重写某个方法,就能够针对不同的状态进行一些处理。例如,若要获取服务器返回的数据,只要重写connection: didReceiveData:方法即可。
综上所述,NSURLConnection 提供了很多灵活的方法下载URL内容,也提供了一个简单的接口去创建和放弃连接,同时使用很多的delegate方法去支持连接过程的反馈和控制。
2.2.3 Web视图
在iOS中,Web视图使用UIWebView类表示,它是一个内置浏览器控件,用于浏览网页或者文档。UIWebView可以在应用中嵌入网页的内容,通常情况下是html格式,它也支持加载pdf、docx、txt等格式的文件。下面通过图2-14来描述UIWebView的使用场景。
图2-14 微信的帮助文档
图2-14是微信应用的帮助文档。由图可知,UIWebView主要用于加载静态页面,这是应用程序显示内容的一种方式,iPhone的Safari浏览器就是通过UIWebView实现的。
要想在程序中使用UIWebView加载网页,最简单的方式是直接将对象库中的Web View拖曳到程序界面中,还可以通过创建UIWebView类的对象实现,同时,UIWebView类定义一些常用的属性,如表2-5所示。
表2-5 UIWebView的常用属性
属性声明 |
功能描述 |
---|---|
@property (nonatomic, assign) id <UIWebViewDelegate> delegate; |
设置代理 |
@property (nonatomic) BOOL detectsPhoneNumbers; |
是否自动检测网页上的电话号码 |
@property (nonatomic) UIDataDetectorTypes dataDetectorTypes; |
需要进行检测的数据类型 |
@property (nonatomic, readonly, getter=canGoBack) BOOL canGoBack; |
是否能够回退 |
@property (nonatomic, readonly, getter=canGoForward) BOOL canGoForward; |
是否能够前进 |
@property (nonatomic, readonly, getter=isLoading) BOOL loading; |
是否正在加载 |
@property (nonatomic) BOOL scalesPageToFit; |
是否缩放内容至适应屏幕当前的尺寸 |
表2-5列举了UIWebView些常见的属性。其中,delegate为代理属性,如果一个对象想要监听Web视图的加载过程,如Web视图完成加载,该对象可以成为Web视图的代理来实现监听,但是前提是要遵守UIWebViewDelegate协议,该协议的定义格式如下:
@protocol UIWebViewDelegate <NSObject>
@optional
// 当Web视图被指示载入内容时会得到通知
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType;
// 当Web视图已经开始发送一个请求后会得到通知
- (void)webViewDidStartLoad:(UIWebView *)webView;
//当Web视图请求完毕时会得到通知
- (void)webViewDidFinishLoad:(UIWebView *)webView;
//当Web视图在请求加载中发生错误时会得到通知
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
@end
在上述代码中,UIWebViewDelegate声明了4个供代理监听的方法,这些方法会在不同的状态下被调用。例如,webViewDidFinishLoad:方法就是Web视图完成一个请求的加载时调用的方法。
除此之外,UIWebView类还提供了一些常见的方法,用于管理浏览器的导航动作,如回退和前进,如表2-6所示。
表2-6 UIWebView的常用方法
方法声明 |
功能描述 |
---|---|
- (void)reload; |
重新加载 |
- (void)stopLoading; |
停止加载 |
- (void)goBack; |
回退 |
- (void)goForward; |
前进 |
2.2.4 实战演练——Web视图加载百度页面
UIWebView可以创建一个网页浏览器,类似于Safari。为了大家更好地理解,接下来,通过一个案例来演示如何通过Web视图加载网页,具体如下。
1.创建工程,设计界面
(1)新建一个Single View Application应用,命名为02-UIWebView。进入Main.storyboard,从对象库拖曳1个WebView到故事板,该视图是全屏的。
(2)为了让WebView适应屏幕的调整,需要对其进行自动布局。选中WebView,单击“pin”按钮弹出一个窗口,在该窗口中依次添加WebView距离顶部、底部、左侧、右侧的约束,当屏幕发生变化时,让WebView始终保持全屏,设计完成的界面如图2-15所示。
图2-15 设计完成的界面
2.创建控件对象的关联
单击Xcode右上角的图标,进入控件与代码关联的界面,给WebView添加1个属性,命名为webView。
3.实现Web视图加载百度页面
要想通过Web视图加载网络资源,需要3个步骤,即确定要访问的资源;建立请求,向服务器索要数据;建立网络连接,将请求异步发送给服务器,等待服务器的响应。针对这3个步骤的示例代码如例2-1所示。
【例2-1】ViewController.m
1 #import "ViewController.h"
2 @interface ViewController ()
3 @property (weak, nonatomic) IBOutlet UIWebView *webView;
4 @end
5 @implementation ViewController
6 - (void)viewDidLoad {
7 [super viewDidLoad];
8 // 1.确定要访问的资源
9 NSURL *url = [NSURL URLWithString:@"http://m.baidu.com"];
10 // 2.建立请求,向服务器索要资源
11 NSMutableURLRequest *request = [NSMutableURLRequest
12 requestWithURL:url];
13 // 2.1告诉服务器我是iPhone,支持苹果的Web套件
14 [request setValue:@"iPhone AppleWebKit"
15 forHTTPHeaderField:@"User-Agent"];
16 // 3.建立网络连接,将异步请求发送到服务器
17 [NSURLConnection sendAsynchronousRequest:request
18 queue:[[NSOperationQueue alloc] init] completionHandler:^(
19 NSURLResponse *response, NSData *data, NSError *connectionError) {
20 // 3.1 将二进制数据data转换为字符串
21 NSString *html = [[NSString alloc] initWithData:data
22 encoding:NSUTF8StringEncoding];
23 // 3.2 Web视图显示HTML
24 [self.webView loadHTMLString:html baseURL:url];
25 }];
26 }
27 @end
在例2-1中,第9行代码根据字符串创建了一个url,其中字符串是百度提供给移动端的域名。第24行代码调用loadHTMLString: baseURL:方法加载HTML页面,baseURL表示加载资源的参照路径。
4.运行程序
单击Xcode工具的运行按钮,在模拟器上运行程序。程序运行成功后,百度的网页出现在模拟器屏幕上,如图2-16所示。
图2-16 Web视图加载百度页面
注意:
使用Web View加载HTML网页,能够实现类似于Safari浏览器的效果,功能非常强大,但是内存的消耗也是非常直观的。运行程序,随意选择一个视频观看,同时打开Xcode的调试导航面板,如图2-17所示。
图2-17 内存消耗示意图
从图2-17中可以看出,最高峰值已经达到了196MB,内存消耗相当严重。
2.3 数据解析
前面已经能够获取服务器的数据,但是这些数据都是二进制的,故需要对其进行解析。在iOS开发中,最常用的数据格式就是JSON格式,偶尔也会有XML格式,无论是JSON还是XML格式,它们都是一种特殊格式的字符串,按照一定的规则来描述的数据结构。接下来,本章将针对数据解析的相关内容进行详细的讲解。
2.3.1 配置Apache服务器
为了能够有一个免费测试的服务器,需要配置一个Web服务器。Apache是使用最广的Web服务器,它是Mac自带的服务器,只要修改几个配置就可以使用,相对而言比较简单快捷,针对一些特殊的服务器功能,Apache都能够有很好的支持。
要想配置Apache,准备工作是要设置用户密码,避免计算机“裸奔”到互联网。打开Finder中的“系统偏好设置”,单击“用户与群组”,切换到当前的用户后,单击“更改密码”按钮,弹出一个图2-18所示的窗口。
按照图2-18所示的窗口,输入正确的信息即可。用户密码设置完成之后,接下来就是配置服务器的工作,大致分为以下4个步骤。
1.创建一个文件夹,放到Users目录下
(1)打开Finder的“偏好设置”,弹出“Finder偏好设置”的对话框。单击“边栏”选项,该窗口列举了边栏可以显示的项目,中间位置有一个小房子图标,后面跟着Mac的用户名,勾选其对应的复选框即可,如图2-19所示。
图2-18 “更改密码”窗口
图2-19 勾选“sunshine”项目
(2)单击Finder快捷图标,弹出任意一个Finder窗口,该窗口的左侧边栏显示出sunshine(当前用户名)文件夹,其对应路径就是/Users/sunshine。
(3)选中sunshine,右侧窗口切换到该目录。使用N 快速创建一个空文件夹,命名为“Sites”,该名称是随意的。这样,网络用户就可以访问该目录了。
2.通过终端修改配置文件中的两个路径,指向Sites文件夹
(1)打开终端,默认工作目录为sunshine。切换工作目录到apache2,输入如下命令:
$ cd /etc/apache2
需要注意的是,以“$”符号开头的命令可以复制,但不要复制“$”符号。输入上述命令后,单击“return”键,切换至配置apache的目录。为了确认当前目录,可输入如下命令来检测:
$ pwd
另外,如果要以列表的形式查看当前目录的全部内容,可输入如下命令:
$ ls
(2)由于需要改动httpd.conf文件,为了避免出现错误,最好备份该文件,输入如下命令:
$ sudo cp httpd.conf httpd.conf.bak
其中,httpd.conf表示源文件,httpd.conf.bak表示目标文件。若后续出现错误,需要恢复之前备份的httpd.conf文件,输入如下命令:
$ sudo cp httpd.conf.bak httpd.conf
(3)备份完成后,单击“return”键,输入之前设定的密码。需要注意的是,输入密码时,终端没有任何相关的回应。
(4)密码输入完成后,单击“return”键,再次回到apache2目录。输入“ls”命令,可以看到该目录下确实增加了一个httpd.conf.bak,如图2-20所示。
图2-20 查看apache2目录的内容
(5)接下来,就可以编辑httpd.conf文件了,通过vim编辑该文件,输入如下命令:
$ sudo vim httpd.conf
需要注意的是,vim是一个编辑器,在其中只能使用键盘的方向键滚动,无法使用鼠标操作。单击“return”键,这时终端打开了httpd.conf文件。
(6)通过键盘直接输入“/DocumentRoot”,用于查找DocumentRoot,单击“return”键,光标自动定位到DocumentRoot位置,如图2-21所示。
图2-21 查找DocumentRoot
这时,在光标定位的下面会看到两个路径,这就是要修改的路径。
(7)按住键盘的“”键,移动到第1个路径所在的那一行,再按住“”键,移动到该行最后的右双引号位置,输入“i”命令,这时会看到底部显示“--INSERT--”字样,表示进入编辑模式,如图2-22所示。
图2-22 进入编辑模式
(8)按住键盘的“Delete”键,删除右引号与左引号之间的内容,输入“/Users/ sunshine/Sites”。同样,将下面一行双引号之间的内容也更改为“/Users/ sunshine/Sites”。需要注意的是,中间的sunshine表示当前的用户名。
(9)按住键盘的“”键,继续向下查找“Options FollowSymLinks Multiviews”内容,将该内容修改为“Options Indexes FollowSymLinks Multiviews”。需要注意的是,如果Mac的版本为10.9,则可以直接忽略该操作。
(10)单击键盘的“Esc”键,退出编辑模式,返回到命令行模式。输入“/php”命令,查找php,单击“return”键,光标自动定位到带有php的内容。输入“0”,光标自动移动的该行的首字母,再输入“x”删除行首的注释符“#”,最后输入“:wq”命令保存并退出。
3.复制php.ini文件
(1)这时,命令行已经返回到跳入前的状态。切换到etc目录,输入如下命令:
$ cd /etc
输入完成后,单击“return”键,再次输入“pwd”命令,用于确认当前目录是否正确。接下来,就可以复制php.ini文件了,输入如下命令:
$ sudo cp php.ini.default php.ini
输入完成后,单击“return”键,再次输入一遍密码。
(2)输入“sudo apachectl -k restart”命令,重新启动apache服务器。单击“return”键,由于没有DNS服务器,提示一个错误信息,如图2-23所示。
图2-23 提示错误信息
值得一提的是,提示图2-23所示的错误是正常的,若提示其他错误则表示不正常。
4.验证
配置工作完成之后,可以通过如下方式进行验证。打开Safari,在地址栏中输入“localhost”,单击“return”键,出现的页面如图2-24所示。
图2-24展示的页面是一个文件列表,这个目录对应着“/sunshine/Sites”路径。如果要在该页面中添加内容,只要在Finder中找到Sites文件夹,将要添加进去的文件拖曳到该文件夹目录下,单击图2-24中所示的“刷新”按钮即可。
图2-24 配置成功的服务器
注意:
(1)每次启动计算机后,Apache服务器默认是不自动启动的,故需要打开终端,输入如下命名:
$ sudo apachectl -k start
(2)在使用终端进行操作之前,需要注意如下几个事项:
- 关闭中文输入法;
- 命令与参数之间需要有空格;
- 修改系统文件一定记住输入sudo命令,否则会没有权限;
- 目录一定要在/Users/sunshine(当前用户名)下。
2.3.2 XML文档结构
可扩展标记语言(Extensible Markup Language,XML),是一种用于标记电子文件使其具有结构性的标记语言,用于传输和存储数据。下面通过图2-25来描述XML文档的结构。
图2-25 XML结构示意图
图2-25展示了XML文档的结构图。由图可知,XML文档由开始标签“<flag>”和结束标签“</flag>”组成,它们就像一个括号一样将数据括起来。XML文档结构要遵守一定的格式规范,只有按照规范编写的XML文档才是有效的文档,从图中看出,XML文档的基本构架分为以下3个部分。
(1)声明
位于XML文档的最前面,用于声明一个XML文档的类型,这个是必须要编写的。通常情况下,最简单的是声明一个版本。另外,还可以说明文档的字符编码,格式如图2-25所示的第1行内容。
(2)元素
一个元素包含了开始标签和结束标签,这两个标签必须保持一致。一个XML文档只有一个根元素,其他元素都是根元素的子孙元素,一个元素可以嵌套若干个子元素(不可交叉嵌套)。如果开始标签和结束标签之间没有内容,可以缩写成“</flag>”,称为“空标签”。
(3)属性
属性定义在开始标签中,一个元素可以拥有多个属性,且属性值必须使用双引号或者单引号括住。例如图2-25中的第3行内容,id=“1”是note元素的一个属性,id是属性名,1是属性值。
2.3.3 解析XML文档
XML文档的操作包括“读”与“写”,读入XML文档并分析的过程称为“解析”,要想从XML文档中提取有用的信息,必须要学会解析XML文档。针对解析XML文档,目前有两种流行的模式,分别为DOM解析和SAX解析,详细介绍如下。
(1)DOM解析:一次性地将整个XML文档加载到内存中,比较适合解析小文件。
(2)SAX解析:从根元素开始,按照顺序一个元素一个元素向下解析,比较适合解析大文件,iOS重点推荐使用SAX解析。
基于上述两种模式,iOS提供了NSXML和libxml2两个原生框架,此外还有一个第三方框架GDataXML,针对它们的介绍如下。
(1)NSXML:它是基于Objective-C语言的SAX解析框架,是iOS SDK默认的XML解析框架,不支持DOM模式。
(2)libxml2:它是基于C语言的XML解析器,被苹果整合在iOS SDK中,支持SAX和DOM模式。
(3)GDataXML:它是基于DOM模式的解析库,由Google开发,可以读写XML文档,支持XPath查询。
2.3.4 实战演练——使用NSXMLParser解析XML文档
NSXML是iOS SDK自带的,也是苹果默认的解析框架,通过采用SAX模式解析,是SAX解析模式的代表。NSXML框架的核心是NSXMLParser和其委托协议NSXMLParserDelegate,其中,最主要的解析工作是在NSXMLParserDelegate的实现类中完成的,该协议定义了很多回调方法,例如,遇到一个开始标签时触发某个方法。接下来,列举该协议中最常用的5个方法,定义格式如下:
//在文档开始的时候触发
- (void)parserDidStartDocument:(NSXMLParser *)parser;
//遇到一个开始标签时触发,其中namespaceURI部分是命名空间,
//qualifiedName是限定名,attributes是字典类型的属性集合
- (void)parser:(NSXMLParser *)parser didStartElement:
(NSString *)elementName namespaceURI:(NSString *)namespaceURI
qualifiedName:(NSString *)qName attributes:(NSDictionary *)attributeDict;
//遇到字符串时触发
- (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string;
//遇到一个结束标签时触发
- (void)parser:(NSXMLParser *)parser didEndElement:(NSString *)elementName
namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName;
//在文档结束的时候触发
- (void)parserDidEndDocument:(NSXMLParser *)parser;
上述5个方法依次按照解析文档的顺序来触发,理解它们的先后顺序是很重要的。接下来,通过一张图来描述它们的触发顺序,如图2-26所示。
图2-26 UML时序图
图2-26所示是UML时序图,由图可知,对于同一个元素而言,触发顺序是按照图2-26所示的顺序执行的。在整个解析的过程中,方法1和方法5是一对,只会触发一次;方法2和方法4是一对,可触发多次;方法3在方法2和方法4之间触发,且会触发多次。触发的字符包括换行符和回车符等特殊字符,编程时需要注意。
为了大家更好地理解,接下来,通过一个案例演示如何使用NSXMLParser类解析XML文档,具体步骤如下。
1.创建工程,设计界面
新建一个Single View Application应用,命名为03-XML解析。进入Main.storyboard,删掉故事板带有的View Controller,拖曳一个Table View Controller到程序界面,指定其为初始化的控制器,并设置Table View Controller的Class为ViewController,前提是要将View Controller的父类更改为UITableViewController。
2.分析XML文档解析思路
通过前面的讲解,大家已经安装了一个服务器,通过命令行启动服务器,并将videos.xml文件添加到该服务器中。单击Safari快捷图标,在地址栏中输入localhost,在弹出的页面中打开videos.xml,如图2-27所示。
图2-27 Safari打开的videos.xml文件
由图2-27可知,videos实质上是一个数组集合,其内部有多个video模型,每个video模型有多个属性,一个开始标签和一个结束标签将该属性对应的值括起来,解析XML文档按照从上至下的原则。接下来,通过一张图来分析该文件对应的解析思维,如图2-28所示。
图2-28 videos.xml解析思维导图
图2-28所示是针对videos.xml文件分析的思维导图,由图可知,解析的目的在于获取一个videos数组,要想达到这个目的,需要经历5个步骤,具体如下。
(1)开始文档:提前做一些准备工作。
(2)开始节点:若开始节点是videos,无需做任何操作;若开始节点是video,创建一个video模型,并设置videoId的值;若开始节点是name、length等属性,无需做任何操作。
(3)发现节点文字:该步骤会执行多次,每次会拼接步骤(2)中节点的内容。
(4)结束节点:若结束节点是name、length等属性时,设置步骤(2)中的video模型的属性;若结束节点是video时,将video模型添加到videos数组。其中,步骤(2)~(4)是一直在循环执行的。
(5)结束文档:若结束节点是videos,则结束解析文档。
综上所述,要想使用代码完成相应的逻辑,需要准备如下素材,首先需要创建一个video模型,其内容包含name、length等属性;其次,需要创建一个videos数组,用于保存多个video模型对象;再次,需要定义一个当前正在解析的模型成员变量,用于拼接数据;最后,需要定义一个可变字符串,用于拼接步骤(3)中的内容。
需要注意的是,Safari默认打开的效果与图2-27所示稍有差异。若要调整为同样的效果,打开Safari,在屏幕顶部的菜单项“Safari”的下拉菜单中选择“偏好设置”,选中“高级”,勾选底部的“在菜单栏中显示‘开发’菜单”复选框。
3.创建video模型
(1)选中项目文件夹,添加一个分组Model。选中该分组,创建一个类Video,继承自NSObject,按照videos.xml文件中的子元素,在Video.h中定义7个属性,如例2-2所示。
【例2-2】Video.h
1 #import <Foundation/Foundation.h>
2 @interface Video : NSObject
3 /// 视频代号
4 @property (nonatomic, copy) NSNumber *videoId;
5 /// 视频名称
6 @property (nonatomic, copy) NSString *name;
7 /// 视频长度
8 @property (nonatomic, copy) NSNumber *length;
9 /// 视频URL
10 @property (nonatomic, copy) NSString *videoURL;
11 /// 图像URL(相对路径)
12 @property (nonatomic, copy) NSString *imageURL;
13 /// 介绍
14 @property (nonatomic, copy) NSString *desc;
15 /// 讲师
16 @property (nonatomic, copy) NSString *teacher;
17 @end
在例2-2中,全部的属性都是copy修饰的,用于防止多个属性指向同一个对象,造成数据混乱的情况。
(2)由于imageURL是一个相对路径,而且length表示视频的时长,它的值是一个整数,无法很直观地反应视频的时长。为此,需要自定义两个属性,分别用于表示图片的全路径和时长的字符串,代码如下:
1 /// 图像URL(完整路径)
2 @property (nonatomic, strong) NSURL *imageFullURL;
3 /// 时长字符串
4 @property (nonatomic, copy) NSString *timeString;
(3)在Video.m文件中,拼接图片的完整路径,并且转换视频时长的格式,代码如例2-3所示。
【例2-3】Video.m
1 #import "Video.h"
2 #define BASE_URL [NSURL URLWithString:@"http://127.0.0.1/"]
3 @implementation Video
4 - (NSURL *)imageFullURL
5 {
6 if (_imageFullURL == nil) {
7 _imageFullURL = [NSURL URLWithString:self.imageURL
8 relativeToURL:BASE_URL];
9 }
10 return _imageFullURL;
11 }
12 - (void)setLength:(NSNumber *)length
13 {
14 _length = length.copy;
15 int len = self.length.intValue;
16 _timeString = [NSString stringWithFormat:
17 @"%02d:%02d:%02d", len / 3600, (len % 3600) / 60, len % 60];
18 }
19 @end
在例2-3中,第2行代码定义了一个宏,用于表示服务器的基本URL,包括服务器的协议头和域名。
4.封装解析XML文档的操作
定义一个方法,外界只需要传入一个解析器对象,通过一个回调的block,就能够得到解析完成的数据,具体步骤如下。
(1)选中Model分组,创建一个继承自NSObject的类SAXVideo,表示用于解析XML文档的功能类。在SAXVideo.h文件中,定义一个供外界调用的类方法,代码如例2-4所示。
【例2-4】SAXVideo.h
1 #import <Foundation/Foundation.h>
2 @interface SAXVideo : NSObject
3 + (void)saxParser:(NSXMLParser *)parser
4 finished:(void(^)(NSArray *videos))finished;
5 @end
在例2-4中,第3行代码定义了一个类方法,该方法有两个参数,parser表示需要传入的NSXMLParser对象,finished表示解析完成后回调的block。
(2)要想使用NSXMLParser类解析,前提是要遵守NSXMLParserDelegate协议。在SAXVideo.m中,依次定义3个属性,并采用懒加载的方法进行初始化,代码如下:
1 #import "SAXVideo.h"
2 #import "Video.h"
3 @interface SAXVideo() <NSXMLParserDelegate>
4 // 可变数组,用于保存video模型
5 @property (nonatomic, strong) NSMutableArray *videos;
6 // 当前正在解析的模型
7 @property (nonatomic, strong) Video *currentVideo;
8 // 元素内容
9 @property (nonatomic, strong) NSMutableString *elementString;
10 @end
11 @implementation SAXVideo
12 #pragma mark - 懒加载
13 - (NSMutableArray *)videos {
14 if (_videos == nil) {
15 _videos = [NSMutableArray array];
16 }
17 return _videos;
18 }
19 - (NSMutableString *)elementString {
20 if (_elementString == nil) {
21 _elementString = [NSMutableString string];
22 }
23 return _elementString;
24 }
25 @end
在上述代码中,currentVideo属性表示当前正在解析的模型,由于currentVideo是动态变化的,故无法使用懒加载的方法初始化。
(3)指定传入参数parser的代理为SAXVideo对象,并记录回调的block。为此,定义一个block变量,用于记录回调的block,代码如下:
1 // block变量
2 @property (nonatomic, copy) void (^finishedBlock)(NSArray *);
接下来,实现saxParser:finished:方法,在该方法中记录回调的block,并设定代理,代码如下:
1 + (void)saxParser:(NSXMLParser *)parser
2 finished:(void (^)(NSArray *))finished{
3 SAXVideo *sax = [[SAXVideo alloc] init];
4 // 记录回调的block
5 sax.finishedBlock = finished;
6 // 指定解析器的代理
7 parser.delegate = sax;
8 // 解析器开始解析
9 [parser parse];
10 }
在上述代码中,第5行代码记录了回调的块finished,第7行代码指定了parser的代理,第9行代码通过调用parse方法开始解析。
(4)依次实现协议中的5个方法,完成videos.xml文件的解析。首先,依照思维导图的思路,在开始解析文档时,清空数组的内容,代码如下:
1 /**
2 * 1.开始文档
3 */
4 - (void)parserDidStartDocument:(NSXMLParser *)parser
5 {
6 // 清空数组
7 [self.videos removeAllObjects];
8 }
(5)当遇到开始标签时,若元素的名称为video,创建一个video模型,并且设置videoId,代码如下:
1 /**
2 * 2.遇到开始标签
3 */
4 - (void)parser:(NSXMLParser *)parser
5 didStartElement:(NSString *)elementName
6 namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName
7 attributes:(NSDictionary *)attributeDict{
8 // 如果开始标签是video
9 if ([elementName isEqualToString:@"video"]) {
10 // 创建模型
11 self.currentVideo = [[Video alloc] init];
12 // 设置videoId
13 self.currentVideo.videoId = @([attributeDict[@"videoId"]
14 integerValue]);
15 }
16 // 清空字符串内容
17 [self.elementString setString:@""];
18 }
在上述代码中,第17行代码清空了elementString的内容,保证每次只能拼接一个元素的完整内容。
(6)当发现元素字符时,将检测到的字符串追加,代码如下:
1 /**
2 * 3.发现字符
3 */
4 - (void)parser:(NSXMLParser *)parser foundCharacters:(NSString *)string
5 {
6 // 拼接字符串
7 [self.elementString appendString:string];
8 }
(7)当遇到结束标签时,若元素的名称为video,将解析完的模型添加到videos数组中;若元素的名称为其他元素,给每个元素赋值即可,代码如下:
1 /**
2 * 4.遇到结束标签
3 */
4 - (void)parser:(NSXMLParser *)parser
5 didEndElement:(NSString *)elementName
6 namespaceURI:(NSString *)namespaceURI qualifiedName:(NSString *)qName{
7 // 如果 elementName 是 video,添加到数组
8 if ([elementName isEqualToString:@"video"]) {
9 [self.videos addObject:self.currentVideo];
10 } else if (![elementName isEqualToString:@"videos"]){
11 [self.currentVideo setValue:self.elementString forKey:elementName];
12 }
13 }
在上述代码中,第11行代码使用KVC的方式,将每次拼接完整的elementString间接地赋值给对应的elementName。
(8)当遇到元素videos时,表示videos数组解析完成,调用之前记录的回调block,将该数组传递过去,代码如下:
1 /**
2 * 5.结束文档
3 */
4 - (void)parserDidEndDocument:(NSXMLParser *)parser
5 {
6 dispatch_async(dispatch_get_main_queue(), ^{
7 self.finishedBlock(self.videos.copy);
8 });
9 }
5.自定义单元格
由于系统的单元格样式无法满足需求,而且全部单元格的样式比较统一,故通过Storyboard实现自定义单元格,具体步骤如下。
(1)进入Main.storyboard,从对象库拖曳1个Image View、3个Label,并分别对这4个控件添加约束,当屏幕发生改变时,保证页面元素位置的统一,设计好的界面如图2-29所示。
图2-29 设计好的界面
(2)给项目添加一个分组View,选中View分组,新建一个表示单元格的类VideoCell,继承自UITableViewCell。进入Main.storyboard,选中Cell,设置Class为VideoCell类,设置Identifier为Cell,给单元格指定一个标识符。
(3)在VideoCell.h文件中,定义一个Video对象,用于接收外界传递的模型数据,代码如例2-5所示。
【例2-5】VideoCell.h
1 #import <UIKit/UIKit.h>
2 #import "Video.h"
3 @interface VideoCell : UITableViewCell
4 @property (nonatomic, strong) Video *video;
5 @end
(4)给项目文件夹添加一个Lib分组,导入一个第三方框架SDWebImage,用于下载网络上的图片,并且在VideoCell.m文件中导入UIImageView+WebCache.h头文件。
(5)在VideoCell.m文件中,采用拖曳的方式,给故事板中单元格的每个子控件添加一个属性。重写video的setter方法,分别给每个子控件设置数据,代码如例2-6所示。
【例2-6】VideoCell.m
1 #import "VideoCell.h"
2 #import "UIImageView+WebCache.h"
3 @interface VideoCell()
4 @property (weak, nonatomic) IBOutlet UIImageView *iconView; // 图片
5 @property (weak, nonatomic) IBOutlet UILabel *titleLabel; // 标题
6 @property (weak, nonatomic) IBOutlet UILabel *teacherLabel; // 讲师
7 @property (weak, nonatomic) IBOutlet UILabel *timeLabel; // 时长
8 @end
9 @implementation VideoCell
10 - (void)setVideo:(Video *)video
11 {
12 _video = video;
13 self.titleLabel.text = video.name;
14 self.teacherLabel.text = video.teacher;
15 self.timeLabel.text = video.timeString;
16 // 设置图像
17 [self.iconView sd_setImageWithURL:video.imageFullURL
18 placeholderImage:nil options:SDWebImageRetryFailed |
19 SDWebImageLowPriority];
20 }
21 @end
在例2-6中,第17行代码调用sd_setImageWithURL:placeholderImage:options:方法从网络上下载图片。其中,options传入两个参数值,SDWebImageRetryFailed表示图片下载失败时不添加到黑名单,SDWebImageLowPriority表示滚动表格时暂停下载。
6.表格展示数据
通过一个数组接收解析完成的数据,并且展示到表格中,每次下拉表格刷新时,实时地更新数据,具体步骤如下。
(1)进入Main.storyboard,选中View Controller,设置Refreshing为Enabled。这时,文档大纲区增加了一个Refresh Control,用于实现下拉刷新的控件,如图2-30所示。
(2)在ViewController.m中,定义一个数组来保存表格绑定的数据,并在其setter方法中刷新数据,代码如下:
1 #import "ViewController.h"
2 #import "Video.h"
3 #import "VideoCell.h"
4 #import "SAXVideo.h"
5 @interface ViewController ()
6 // 表格绑定的数据
7 @property (nonatomic, strong) NSArray *dataList;
8 @end
9 @implementation ViewController
10 - (void)setDataList:(NSArray *)dataList
11 {
12 _dataList = dataList;
13 // 刷新数据
14 [self.tableView reloadData];
15 // 结束刷新
16 [self.refreshControl endRefreshing];
17 }
18 @end
图2-30 文档大纲增加一个Refresh Control
(3)采用拖曳的方法,为Refresh Control绑定一个方法,每当下拉刷新表格时,重新到网络上请求数据,代码如下:
1 - (IBAction)loadData
2 {
3 // 请求数据
4 NSURL *url = [NSURL URLWithString:@"http://localhost/videos.xml"];
5 NSURLRequest *request = [NSURLRequest requestWithURL:url];
6 [NSURLConnection sendAsynchronousRequest:request
7 queue:[[NSOperationQueue alloc] init]
8 completionHandler:^(NSURLResponse *response,
9 NSData *data, NSError *connectionError) {
10 // 创建解析器
11 NSXMLParser *parser = [[NSXMLParser alloc] initWithData:data];
12 // 解析器开始解析,后续的解析工作全部由代理完成
13 [SAXVideo saxParser:parser finished:^(NSArray *videos) {
14 self.dataList = videos;
15 }];
16 }];
17 }
在上述代码中,第11行代码创建了一个NSXMLParser类的对象,第13行代码将parser传递,并将解析完成的数组赋值给dataList。
(4)实现表格的数据源方法,按照自定义单元格的样式,展示从网络上接收到的数据,代码如下:
1 #pragma mark - 数据源方法
2 - (NSInteger)tableView:(UITableView *)tableView
3 numberOfRowsInSection:(NSInteger)section {
4 return self.dataList.count;
5 }
6 - (UITableViewCell *)tableView:(UITableView *)tableView
7 cellForRowAtIndexPath:(NSIndexPath *)indexPath {
8 VideoCell *cell = [tableView dequeueReusableCellWithIdentifier:
9 @"Cell"];
10 // 设置 Cell...
11 cell.video = self.dataList[indexPath.row];
12 return cell;
13 }
(5)首次运行程序时,同样需要加载网络数据,因此,在viewDidLoad方法中主动调用loadData方法,代码如下:
1 - (void)viewDidLoad {
2 [super viewDidLoad];
3 [self loadData];
4 }
7.运行程序
单击“运行”按钮运行程序,程序运行成功后,模拟器屏幕上面展示了一个表格,每行单元格都包括图片、视频标题、讲师和视频时长的相关信息,下拉表格出现一个指示器,2s左右就消失了,如图2-31所示。
图2-31 程序的运行结果
值得一提的是,一旦更改了videos.xml文件的内容,只要下拉刷新表格,就能够根据服务器的信息自动更新表格数据。
2.3.5 JSON文档结构
JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,它采用完全独立于语言的文本格式,也使用了C语言“家族”的习惯,使其成为理想的数据交换语言。
所谓轻量级,是指与XML文档结构相比而言,描述项目的字符少,故描述相同数据所需的字符个数要少,传输速度就得到提高,从而减少用户的流量。JSON文档主要分为两种结构,分别为对象和数组,详细介绍如下。
1.对象
对象表示为“{}”括起来的内容,数据结构为{key:value,key:value,… }的键值对的结构,其中,key为对应的属性,value为该属性对应的值。若要获取值,直接通过“对象.属性”来获取该属性的值。JSON对象的语法表如图2-32所示。
图2-32 JSON对象的语法表
下面是一个JSON对象的例子:
{
"name" : "Jay",
"age" : 30,
"sex" : ture
}
在上述代码中,JSON对象类似于字典类型,可读性更好。它是一个无序的集合,key必须使用双引号,值之间使用逗号隔开,value可以是数值、字符串、数组、对象等几种类型。
2.数组
数组表示为“[ ]”(中括号)括起来的内容,数据结构为“[value, value, value,…]”。它是值的有序集合,取值方式与其他语言一样,根据索引获取即可。JSON数组的语法表如图2-33所示。
图2-33 JSON数组的语法表
下面是一个JSON数组的例子:
["it","cast","itcast"]
在上述代码中,每个条目之间同样使用逗号隔开,value可以是双引号括起来的字符串、数值、true、false、null、对象或者数组,而且这些结构可以嵌套,如图2-34所示。
图2-34 JSON值的语法结构图
总而言之,对象和数组这两种结构可以嵌套,从而组合成更加复杂的数据结构。
2.3.6 解析JSON文档
将数据从JSON文档读取处理的过程称为“解码”过程,即解析和读取过程。要想解析JSON文档,挖掘出具体的数据,需要将JSON转换为OC数据类型。接下来,通过一张表来比较JSON与OC类型,如表2-7所示。
表2-7 JSON与OC转换对照表
JSON |
OC |
---|---|
{}(大括号) |
NSDictionary |
[ ](中括号) |
NSArray |
“”(双引号) |
NSString |
数字 |
NSNumber |
由于JSON技术比较成熟,在iOS平台上,也有很多框架可以进行JSON的编码或者解码,常见的解析方案有如下4种。
- SBJson:它是一个比较老的JSON编码或解码框架,该框架现在更新仍然很频繁,支持ARC,源码下载地址为https://github.com/stig/json-framework。
- TouchJSON:它也是比较老的一个框架,支持ARC和MRC,源码下载地址为https://github.com/TouchCode/TouchJSON。
- JSONKit:它是更为优秀的JSON框架,它的代码很小,但是解码速度很快,不支持ARC,源码下载地址为https://github.com/johnezang/JSONKit。
- NSJSONSerialization:它是iOS 5之后苹果提供的API,是目前非常优秀的JSON编码或解码框架,支持ARC,iOS之后的SDK已经包含了这个框架,无需额外安装或者配置。
其中,前面3个框架都是由第三方提供的,最后一个是苹果自身携带的。如果要考虑iOS 5之前的版本,JSONKit是一个不错的选择,只是它不支持ARC,使用起来有点麻烦,需要安装和配置到工程环境中去;如果使用iOS 5之后的版本,NSJSONSerialization应该是首选。
2.3.7 实战演练——使用NSJSONSerialization解析天气预报
使用NSJSONSerialization类解析JSON文档,该类提供了两种常见方法,用于序列化或者反序列化网络数据,它们的定义格式如下:
//将指定NSData中包含的JSON数据转换为OC对象(NSDictionary或者NSArray)
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt
error:(NSError **)error;
// 将指定的JSON对象转化为NSData对象
+ (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt
error:(NSError **)error;
在上述定义中,第1种方法有1个opt参数,它是NSJSONReadingOptions类型的。该类型是一个位移枚举类型,定义格式如下:
typedef NS_OPTIONS(NSUInteger, NSJSONReadingOptions) {
NSJSONReadingMutableContainers = (1UL << 0),
NSJSONReadingMutableLeaves = (1UL << 1),
NSJSONReadingAllowFragments = (1UL << 2)
};
该位移枚举类型包含如下3个值。
- NSJSONReadingMutableContainers:容器可变(顶级节点)。
- NSJSONReadingMutableLeaves:叶子可变(其余子节点)。
- NSJSONReadingAllowFragments:顶级节点可以不是NSArray或者NSDictionary类型的。
如果opt参数传入0,也就是参数的值为NSJSONReadingMutableContainers时,表示任何附加操作都不做,这时的效率最高。
接下来,通过使用NSJSONSerialization类解析天气的数据,带领大家完成一个天气预报的案例,具体步骤如下。
1.分析JSON文档解析思路
在Safari中打开http://www.cnblogs.com/wangjingblogs/p/3192953.html页面,该页面是国家气象局提供的天气预报接口,选择第1个接口地址打开,该窗口展示了一个JSON文档,整理后如下:
{"weatherinfo":
{
"city":"北京",
"cityid":"101010100",
"temp":"10",
"WD":"东南风",
"WS":"2级",
"SD":"26%",
"WSE":"2",
"time":"10:25",
"isRadar":"1",
"Radar":"JC_RADAR_AZ9010_JB",
"njd":"暂无实况",
"qy":"1012"
}
}
在上述文档中,最顶层是一个JSON对象,该对象内部包含一个“名称-值”对,key是weatherinfo,value又是一个JSON对象,该对象内部包含多个“名称-值”对。依据表2-7的转换类型得知,最终会得到两个嵌套的NSDictionary对象,只要根据固定的属性名称,获取需要的值即可。
2.解析JSON文档
新建一个Single View Application应用,命名为04-JSON解析。在ViewController.m文件中,定义一个加载数据的方法,用于解析天气预报的数据,代码如例2-7所示。
【例2-7】ViewController.m
1 #import "ViewController.h"
2 @interface ViewController ()
3 @end
4 @implementation ViewController
5 - (void)viewDidLoad {
6 [super viewDidLoad];
7 [self loadData];
8 }
9 /**
10 * 加载网络数据
11 */
12 - (void)loadData
13 {
14 // 根据请求,加载网络数据
15 NSURL *url = [NSURL URLWithString:
16 @"http://www.weather.com.cn/adat/sk/101010100.html"];
17 NSURLRequest *request = [NSURLRequest requestWithURL:url
18 cachePolicy:0 timeoutInterval:10.0];
19 [NSURLConnection sendAsynchronousRequest:request
20 queue:[NSOperationQueue mainQueue]
21 completionHandler:^(NSURLResponse *response, NSData *data,
22 NSError *connectionError) {
23 // 将二进制数据转换为字典
24 NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data
25 options:0 error:NULL];
26 NSLog(@"%@ 市温度 %@ 风向 %@ 风力 %@",
27 result[@"weatherinfo"][@"city"],
28 result[@"weatherinfo"][@"temp"],
29 result[@"weatherinfo"][@"WD"],
30 result[@"weatherinfo"][@"WS"]);
31 }];
32 }
33 @end
运行程序,运行结果如图2-35所示。
图2-35 程序的运行结果
注意:
反序列化:从服务器接收到数据之后,将二进制数据转换成NSArray或者NSDictionary类型。
序列化:在向服务器发送数据之前,将NSArray或者NSDictionary类型转换为二进制数据。
2.4 HTTP请求
HTTP和HTTPS是最常用的传输协议,针对HTTP请求,iOS提供了多个方法,最常用的就是GET和POST方法。接下来,本节将针对HTTP的相关内容进行详细的介绍。
2.4.1 HTTP和HTTPS
首先对HTTP和HTTPS进行介绍,具体如下。
1.HTTP
HTTP是HyperText Transfer Protocol的缩写,即超文本传输协议。网络中使用的基本协议是TCP/IP,目前广泛采用的HTTP、HTTPS、FTP、Archie和Gopher等均是建立在TCP/IP之上的应用层协议,不同的协议对应着不同的应用。
HTTP是一个属于应用层的面向对象的协议,其简捷、快速的方式适用于分布式超文本信息的传输。HTTP于1990年提出,经过多年的使用与发展,得到了不断完善和扩展。HTTP支持C/S网络结构,是无连接协议,也就是说,每一次请求时建立连接,服务器处理完客户端的请求后,应答给客户端后断开连接,不会一直占用网络资源。
HTTP共定义了8种请求方法,分别是OPTIONS、HEAD、GET、POST、PUT、DELETE、TRACE和CONNECT,最重要的是GET和HEAD方法。
GET方法是向指定的资源发出请求,发送的信息“显式”地跟在URL后面。GET方法应该只用在读取数据,例如,从服务器端读取静态图片等。GET方法有点像使用明信片给别人写信,信的内容写在外面,接触到的人都可以看到,因此它是不安全的。
POST方法是向指定资源提交数据,请求服务器进行处理,例如,提交表单或者上传内容文件等,数据被包含在请求体中。POST方法像是把“信内容”装入信封中,接触到的人都看不到,因此它是安全的。
2.HTTPS
HTTPS是HypertextTransfer Protocol Secure的缩写,即超文本传输安全协议,它是超文本传输协议和SSL的组合,用于提供加密通信及对网络服务器身份的鉴定。接下来,通过一张图描述HTTP与HTTPS的区别,如图2-36所示。
图2-36 HTTP与HTTPS的区别
简单地说,HTTPS是HTTP的升级版,它们之间的区别是,HTTPS使用https://;代替http://,HTTPS使用的端口为443,而HTTP使用端口80来与TCP/IP进行通信。
SSL使用40位关键字作为RC4流加密算法,这对于商业信息的加密是合适的。HTTPS和SSL支持使用X.509数字认证,如果需要的话,用户可以确认发送者是谁。
2.4.2 GET和POST方法
前面已经简单地介绍了GET和POST方法,它们有着很大的不同。下面通过多个角度进行比较,以深入地理解这两个方法的独特之处,具体内容如下。
1.数据传递
从直观的角度来说,GET请求会将参数直接暴露在URL中,但是容易被外界发现,操作相对比较简单。不同的是,POST请求会将参数包装到一个数据体里面,该请求相对而言比较复杂,它需要将参数与地址分开,如图2-37所示。
图2-37 GET和POST请求示意图
从图2-37中可以看出,采用GET方法发送请求时,用户名和密码会以特定的格式拼接到URL中,相对而言安全性不高,且地址最多255字节。而采用POST方法发送请求时,参数并未暴露在URL中,它们被包装成二进制的数据体,服务器只能通过解包的形式查看,才会响应正确的信息。这样就提高了安全性,不易被外界所捕获。
2.缓存
从字面上来说,GET表示获取,即从服务器拿数据,效率更高。只要路径相同,拿到的资源永远只会是同一份,故GET请求能够被缓存。
从字面意义上讲,POST表示发送,即向服务器发送数据,也可以获取服务器处理后的结果,效率相对不高。由于数据体的不同,导致同一个路径访问到的资源可能会不同,故POST请求不会被缓存。
3.数据大小
针对GET请求而言,并没有明确对请求的数据大小限制,不过因为浏览器不同,一般限制在2~8KB。
针对POST请求而言,它提交的数据比较大,大小由服务器的设定值限制,PHP通常限定为2MB。
4.参数格式
所谓参数就是传递给服务器的具体数据,如登录的账号和密码。GET请求的URL需要拼接参数,格式要求如下。
(1)资源路径末尾添加一个“?”(问号),表示追加参数。
(2)每一个变量和值按照“变量名=变量值”方式设定,中间不能包含空格或者中文,如果要包含中文或者空格等,需要添加百分号转义。
(3)多个参数之间需要使用“&”连接。
下面是一个带有参数的URL示例。
http://ww.test.com/login?username=123&pwd=234&type=JSON
对于POST请求而言,参数被包装成二进制的数据体,格式与上面基本一致,只是不包含“?”。
综上所述,GET和POST方法各有所长,根据不同的使用场合,选取合适的方法即可。如果要传递大量的数据,只能使用POST请求;如果是要传递包含机密或者敏感的信息,建议使用POST请求;如果仅只是索取数据,建议使用GET请求;如果需要增加、修改、删除数据,建议使用POST请求。
注意:
默认情况下,HTTP请求使用的是GET方法。
2.4.3 实战演练——模拟POST用户登录
在移动互联网开发中,几乎所有的应用都会希望更多的用户加入,因此,用户登录是一个应用不可缺少的环节。接下来,本节将通过POST方法实现用户登录的逻辑,具体步骤如下。
1.准备工作
在2.3节中我们搭建了一个服务器,找到Sites文件夹,将login.html和login.php这两个文件拖曳到该文件夹下,其中,login.html是用于让用户输入的脚本,login.php是用于处理用户登录的脚本。打开Safari中的login.html文件,如图2-38所示。
图2-38 Safari打开的login.html
从图2-38中可以看出,上半部分是一个GET登录的页面,下半部分是一个POST登录的页面。只要输入姓名“zhangsan”,密码“zhang”,单击“提交”按钮,就会跳转到如图2-39所示的页面。
图2-39 Safari打开的login.php
需要注意的是,如果姓名文本框和密码文本框为空,单击“提交”按钮,会提示“没有输入用户名或密码”;如果姓名或者密码文本框内容输入有误,单击“提交”按钮,会提示“账号密码不正确”。
2.创建工程,设计界面
(1)新建一个Single View Application应用,命名为05-用户登录。
(2)进入Main.storyboard,从对象库拖曳1个View、2个Text Field、1个Button到程序界面,其中,2个Text Field分别用于输入用户名和密码,Button表示登录按钮,View表示容器视图,其他控件均是View的子控件。
(3)选中View,设置其高度、宽度、距离顶部的距离固定,并且水平方向居中,添加这4个约束,设计好的页面如图2-40所示。
图2-40 设计完成的页面
3.创建控件对象的关联
(1)单击Xcode 6.1界面右上角的图标,进入控件与代码的关联界面。依次选中两个Text Field,分别添加表示用户名和密码文本框的属性。
(2)同样的方式,选中Button,添加一个Touch Up Inside事件,命名为login。
4.通过代码实现用户登录的功能
按照文本框的提示,输入用户名和密码后,单击“登录”按钮,如果登录成功,将用户登录信息保存到沙盒,再次运行程序,将沙盒获取到的用户名或者密码显示到对应的文本框位置,具体步骤如下。
(1)保存和加载用户信息
定义两个属性,分别表示用户名和密码。定义两个方法,分别用于保存用户的偏好设置和读取用户的偏好设置,代码如下:
1 #import "ViewController.h"
2 @interface ViewController () <UITextFieldDelegate>
3 @property (nonatomic, copy) NSString *username; // 用户名
4 @property (nonatomic, copy) NSString *password; // 密码
5 @property (weak, nonatomic) IBOutlet UITextField *userNameText;
6 @property (weak, nonatomic) IBOutlet UITextField *passwordText;
7 @end
8 @implementation ViewController
9 #pragma mark - 保存和加载用户信息
10 #define UserNameKey @"UserNameKey"
11 #define PasswordKey @"PasswordKey"
12 - (void)saveUserInfo
13 {
14 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
15 [defaults setObject:self.username forKey:UserNameKey];
16 [defaults setObject:self.password forKey:PasswordKey];
17 }
18 - (void)loadUserInfo
19 {
20 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
21 self.userNameText.text = [defaults stringForKey:UserNameKey];
22 self.passwordText.text = [defaults stringForKey:PasswordKey];
23 }
24 @end
(2)POST登录
定义一个方法,通过POST方法实现用户登录,如果登录成功,将登录的用户信息保存到偏好设置,代码如下:
1 - (void)postLogin
2 {
3 // 1.url
4 NSURL *url = [NSURL URLWithString:@"http://localhost/login.php"];
5 // 2.请求
6 NSMutableURLRequest *request = [NSMutableURLRequest
7 requestWithURL:url];
8 // 2.1 请求方法
9 request.HTTPMethod = @"POST";
10 // 2.2 请求体
11 NSString *bodyStr = [NSString
12 stringWithFormat:@"username=%@&password=%@",self.username,
13 self.password];
14 request.HTTPBody = [bodyStr dataUsingEncoding:NSUTF8StringEncoding];
15 // 3.发送请求
16 [NSURLConnection sendAsynchronousRequest:request
17 queue:[NSOperationQueue mainQueue] completionHandler:
18 ^(NSURLResponse *response, NSData *data, NSError *connectionError){
19 // 反序列化数据
20 NSDictionary *result = [NSJSONSerialization
21 JSONObjectWithData:data options:0 error:NULL];
22 NSLog(@"%@", result);
23 // 判断是否登录成功
24 if ([result[@"userId"] intValue] > 0) {
25 [self saveUserInfo];
26 }
27 }];
28 }
在上述代码中,第9行代码设置HTTPMethod属性为POST,第14行代码通过dataUsing Encoding:方法将字符串转换为NSData类型,并赋值给HTTPBody属性,第20~21行代码将JSON文档转换为NSDictionary类型。值得一提的是,一个请求默认采用的是GET方法。
(3)实现登录方法
单击“登录”按钮,设置用户名和密码,实现POST登录,代码如下:
1 - (IBAction)login {
2 // 设置用户名和密码
3 self.username = self.userNameText.text;
4 self.password = self.passwordText.text;
5 // 登录
6 [self postLogin];
7 }
(4)加载偏好设置的用户信息
一旦将用户信息存储到偏好设置后,只要运行程序,就会将用户名或者密码显示到对应的文本框内,代码如下:
1 - (void)viewDidLoad {
2 [super viewDidLoad];
3 [self loadUserInfo];
4 }
(5)处理多个文本框的逻辑
单击用户名文本框,屏幕弹出键盘,单击“return”键切换到密码文本框,再次单击“return”键,直接用户登录。通过拖曳的方式,设置View Controller为两个Text Field的代理,并遵守UITextFieldDelegate协议,实现该协议的相应的方法,代码如下:
1 #pragma mark - UITextFieldDelegate
2 - (BOOL)textFieldShouldReturn:(UITextField *)textField
3 {
4 if (textField == self.userNameText) { // 切换到密码
5 [self.passwordText becomeFirstResponder];
6 } else {
7 [self login]; // 登录
8 }
9 return YES;
10 }
5.运行程序
(1)单击“运行”按钮运行程序,程序运行成功后,在模拟器的文本框输入“itcast”和“123”,如图2-41所示。
单击“登录”按钮,这时的用户信息是错误的,一旦判断信息有误,控制台会输出错误对应的提示信息,如图2-42所示。
图2-41 用户名和密码错误
图2-42 控制台输出错误信息
(2)再次在文本框中输入正确的用户名和密码,单击“登录”按钮后,控制台会输出相应的提示信息,如图2-43所示。
图2-43 控制台输出正确信息
2.4.4 数据安全——MD5算法
用户安全登录有两个原则,一是不能在网络上传输用户隐私数据的明文,另一个是不能在本地存储用户隐私数据的明文。试想密码以明文的形式保存在沙盒中,一旦泄露是极其危险的。对于数据安全方面提出了多种解决方案,其中MD5使用最为广泛。
消息摘要算法第5版(Message-Digest Algorithm 5,MD5)是计算机安全领域广泛使用的一种散列函数,用于提供消息的完整性保护。通过对任意一个二进制数据抽取特征码,得到一个32个字符的定长字符串,故MD5存在以下两个特点:
- 相同的字符串,使用相同的算法,每次加密的结果是固定的;
- 根据最终输出的值,无法得到原始的明文,即过程是不可逆的。
要想使用MD5,需要引用一个分类NSString+Hash,它已经封装了关于MD5加密的方法。接下来,通过多个方案循序渐进的方式,深入剖析如何通过MD5,让同一个密码的加密结果不同,详细内容如下。
方案一:直接使用MD5
直接调用md5String方法,实现密码字符串的加密,可通过如下代码实现:
password = password.md5String;
网络上推出了破解MD5算法的工具,因此,现在的MD5算法不是绝对安全的。
方案二:MD5加盐
为了增加解密的难度,提供了加盐的方式。所谓加盐,就是在明文密码的固定位置插入一个随机字符串,再直接调用md5String方法,可通过如下代码实现:
static NSString salt = @"ABCabc123!@#";
password= [passwordstringByAppendingString:salt].md5String;
值得一提的是,salt字符串一定要够复杂,否则会失去意义,这种方法近几年用得相对而言比较少。
方案三:HMAC
直接调用hmacMD5StringWithKey:方法,该方法需要传入一个NSString类型的Key,底层使用这个Key对密码加密,再调用md5String方法,重复执行一次这个步骤,可通过如下代码实现:
password = [password hmacMD5StringWithKey:@"itcast"];
使用itcast与password拼接,即对password加盐,再对拼接字符串进行MD5加密。重复前面的步骤,对加密后的数据再次拼接itcast字符串,即再次加盐,再对拼接字符串进行MD5加密。相较于前面的方案而言,安全级别高很多。但是,对于同一个字符串,每次的结果是一样的,这样会存在暴力破解的潜在风险。
方案四:时间戳密码
为了让同一个字符串的加密结果不同,可以拼接一个当前时间的字符串,可通过如下代码实现:
- (NSString *)timePassword {
// 1. 生成key
NSString *key = @"itcast".md5String;
// 2. 对密码进行 hmac 加密
NSString *pwd = [self.password hmacMD5StringWithKey:key];
// 3. 获取当前系统时间
NSDateFormatter *fmt = [[NSDateFormatter alloc] init];
fmt.locale = [NSLocale localeWithLocaleIdentifier:@"zh"];// 指定时区
fmt.dateFormat = @"yyyy-MM-dd HH:mm";
NSString *dateStr = [fmt stringFromDate:[NSDate date]];
// 4. 将系统时间拼接在第一次加密密码的末尾
pwd = [pwd stringByAppendingString:dateStr];
// 5. 返回再次 hmac的结果
return [pwd hmacMD5StringWithKey:key];
}
为了大家更好地理解,按照上述代码的思路,讲述客户端与服务器端对接的思路,如图2-44所示。
图2-44 客户端和服务端对接示意图
从图2-44中可以看出,若要让客户端与服务器端实现对接,大致流程如下。
(1)用户注册时,客户端输入用户名zhangsan和密码zhang,由于服务器端不会明文记住用户的密码,故用户提交时的密码会采用HMAC方式加密,服务器端的数据库会记录加密后的信息。
(2)用户登录时,客户端的密码依旧是zhang,为了与服务器端密码保持一致,客户端首先用HMAC加密得到zhang.hmac,之后让zhang.hmac拼接客户端的系统时间,得到一个新字符串,最后对该字符串再次采用HMAC加密,最终记录的结果为“(zhang.hmac + “2015-06-08 15:59”).hmac”。
(3)服务器端首先根据用户名取出用户口令zhang.hmac,然后让zhang.hmac字符串拼接服务器端的系统时间,再次采用HMAC加密,最终记录的结果为“(zhang.hmac + “系统时间”).hmac”。
(4)只要时间的分钟发生变化,密码可能就会失效,例如,客户端的时间是15:59:59,服务器端的时间是16:00:01。为此,服务器端需要再记录一次比系统时间少一分钟的情况,结果为“(zhang.hmac +“系统时间-1”).hmac”。这样,服务器端会依据这两次的结果进行比较。
综上所述,第4种方案的安全级别更高,目前使用比较广泛。但是第4种方案需要服务器脚本的支持,而且客户端的时间与服务器端的时间是不同步的。
方案五:服务器时间戳密码
为了解决客户端与服务器端时间不同步的问题,需更改时间戳的代码,将获取当前系统时间的代码修改为获取服务器时间的代码,可通过如下代码实现:
/// 生成时间戳密码
- (NSString *)timePassword:(NSString *)pwd {
// 1. 以itcast.md5 作为 hmac key
NSString *key = @"itcast".md5String;
// 2. 对密码进行 HAMC加密
NSString *pwd = [self.pwd hmacMD5StringWithKey:key];
// 3. 获取服务器的时间
NSData *data = [NSData dataWithContentsOfURL:[NSURL
URLWithString:@"http://localhost/hmackey.php"]];
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data
options:0 error:NULL];
NSString *dateStr = dict[@"key"];
// 4. 拼接时间字符串
pwd = [pwd stringByAppendingString:dateStr];
// 5. 再次使用HMAC散列密码
return [pwd hmacMD5StringWithKey:key];
}
其中,hmackey.php是用于获取服务器时间的脚本。客户端通过获取服务器端的时间,解决了时间不同步的问题,这个方案是最好的选择。
2.4.5 钥匙串访问
MD5保存在本地的密码是不可逆的,用户若要从本地文件获取用户信息,显而易见,密码只能获取到加密后的,影响用户的体验。为此,苹果在iOS 7.0.3版本加入了iCloud钥匙串功能。
钥匙串访问采用256位AES加密技术,保证了用户密码的安全,并且是可逆的,能返回用户的原始密码,增强用户体验。钥匙串访问的接口是纯C语言的,代码不易于阅读,针对这种情况,建议使用一个第三方框架sskeychain,官网地址为https://github.com/soffes/sskeychain,该框架提供了几个常用的方法,如表2-8所示。
表2-8 SSKeychain类的常用方法
属性声明 |
功能描述 |
---|---|
+ (NSArray *)allAccounts; |
获取所有的账户 |
+ (NSArray *)accountsForService:(NSString *)serviceName; |
获取所有的账户信息 |
+ (NSString *)passwordForService:(NSString *)serviceName account: (NSString *)account; |
获取账户的密码 |
+ (BOOL)deletePasswordForService:(NSString *)serviceName account: (NSString *)account; |
删除账户的密码 |
+ (BOOL)setPassword:(NSString *)password forService:(NSString *) serviceName account:(NSString *)account; |
将账户密码保存在钥匙串 |
从表2-7中可以看出,后4种方法都带有一个NSString类型的参数serviceName,表示服务名称,建议使用bundleId,可通过如下代码获取:
NSString *bundleId = [NSBundle mainBundle].bundleIdentifier;
2.4.6 实战演练——模拟用户安全登录
前面介绍了安全登录的一些技巧,为了大家更好地理解,接下来,更改用户登录案例的部分内容,将密码采用MD5算法进行加密,增加用户登录的安全性,具体步骤如下。
1.准备工作
前面搭建了一个服务器,找到其对应的Sites文件夹,将loginhmac.php和hmackey.php这两个文件拖曳到给该目录下,其中,loginhmac.php是用于用户安全登录的脚本,hmackey.php是用于取出当前系统时间的脚本。打开hmackey.php文件,页面如图2-45所示。
图2-45 Safari中打开hmackey.php文件
从图2-45中可以看出,它是一个JSON文档,该文档中只有一个属性,Key为“key”,Value为当前获取的系统时间。
2.创建工程,设计界面
(1)新建一个Single View Application应用,命名为06-用户安全登录。将Main.storyboard的名称修改为Login.storyboard,进入Login.storyboard,搭建一个如图2-40所示的登录界面。
(2)使用N快捷键,新建一个Storyboard,命名为Home。进入Home.storyboard,从对象库拖曳一个Navigation Controller到程序界面,默认带有一个Table View Controller的根视图控制器。从对象库拖曳一个Bar Button Item到导航条的右侧,双击输入按钮标题为“注销”,并设置该控制器的标题为“主页”,如图2-46所示。
图2-46 添加完成的Home.Storyboard
(3)选中根节点的项目,在对应的编辑窗口中找到“Deployment Info”选项,该选项包含一个Main Interface,默认是Main.storyboard,删除后面文本框的内容。
3.创建控件对象的关联
(1)选中Login.storyboard,单击右上角的图标,进入控件与代码的关联界面。依次选中两个Text Field,分别添加表示用户名和密码文本框的属性。
(2)同样的方式,选中Button,添加一个Touch Up Inside事件,命名为login。
4.封装网络工具类
在应用程序开发中,通常要建立一个网络请求管理器的单例,用于将用户登录的细节屏蔽起来,具体步骤如下。
(1)新建一个网络工具类NetworkTools,继承自NSObject。在NetworkTools.h文件中,定义一个供外界访问的类方法,代码如例2-8所示。
【例2-8】NetworkTools.h
1 #import <Foundation/Foundation.h>
2 @interface NetworkTools : NSObject
3 /**
4 * 全局的访问点
5 */
6 + (instancetype)sharedTools;
7 // 用户登录
8 - (void)userLoginFailed:(void(^)())failed;
9 // 用户名
10 @property (nonatomic, copy) NSString *username;
11 // 密码
12 @property (nonatomic, copy) NSString *password;
13 @end
在上述代码中,第8行代码定义了一个用户登录的方法,并指定了一个登录失败回调的block。
(2)导入NSString+Hash分类,引入NSString+Hash.h头文件。在NetworkTools.m文件中,定义一个方法,用于生成带有服务器时间戳的密码,代码如下:
1 /**
2 * 生成带时间戳记的密码
3 */
4 - (NSString *)timePassword
5 {
6 // 1.key
7 NSString *key = @"itheima".md5String;
8 // 2.用key对密码进行hmac
9 NSString *password = [self.password hmacMD5StringWithKey:key];
10 // 3.获取当前服务器的系统时间
11 NSURL *url = [NSURL URLWithString:@"http://localhost/hmackey.php"];
12 // 3.1 使用同步获取时间
13 NSData *data = [NSData dataWithContentsOfURL:url];
14 // 3.2 反序列化数据
15 NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data
16 options:0 error:NULL];
17 // 3.3 取出时间字符串
18 NSString *dateStr = result[@"key"];
19 // 4.组合密码和时间
20 password = [password stringByAppendingString:dateStr];
21 return [password hmacMD5StringWithKey:key];
22 }
在上述代码中,第11~18行代码通过hmackey.php脚本获取了服务器的系统时间。
(3)添加第三方框架SSKeychain到项目中,导入SSKeychain.h头文件,定义一个方法,将用户名保存到沙盒,将密码保存到钥匙串;定义另一个方法,用于访问沙盒中的用户名和钥匙串中的密码,代码如下:
1 #pragma mark - 保存和加载用户信息
2 #define UserNameKey @"UserNameKey"
3 - (void)saveUserInfo
4 {
5 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
6 [defaults setObject:self.username forKey:UserNameKey];
7 // 保存到钥匙串
8 NSString *bundleId = [NSBundle mainBundle].bundleIdentifier;
9 [SSKeychain setPassword:self.password forService:bundleId
10 account:self.username];
11 }
12 - (void)loadUserInfo
13 {
14 NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
15 self.username = [defaults stringForKey:UserNameKey];
16 // 从钥匙串访问密码
17 NSString *bundleId = [NSBundle mainBundle].bundleIdentifier;
18 self.password = [SSKeychain passwordForService:bundleId
19 account:self.username];
20 }
(4)实现供全局访问的类方法,保证NetworkTools类的实例仅有一个,代码如下:
1 // 实际工作中,单例只写这一个方法即可
2 + (instancetype)sharedTools
3 {
4 static id instance;
5 static dispatch_once_t onceToken;
6 dispatch_once(&onceToken, ^{
7 instance = [[self alloc] init];
8 });
9 return instance;
10 }
(5)重写init方法,在该方法中加载用户信息,代码如下:
1 - (instancetype)init
2 {
3 if (self = [super init]) {
4 // 加载用户信息
5 [self loadUserInfo];
6 }
7 return self;
8 }
(6)实现用户登录的方法,第1次登录时判断用户名或者密码是否存在,如果存在,通过POST方法加载数据,代码如下:
1 /**
2 * 用户登录
3 */
4 - (void)userLoginFailed:(void (^)())failed
5 {
6 NSAssert(failed != nil, @"必须传入回调");
7 // 1.判断用户名或者密码是否存在
8 if (!(self.username.length > 0 && self.password.length > 0)) {
9 failed();
10 return;
11 }
12 // 2.对密码进行MD5处理
13 NSString *pwd = [self timePassword];
14 // 3.url
15 NSURL *url = [NSURL URLWithString:@"http://localhost/loginhmac.php"];
16 // 4.请求
17 NSMutableURLRequest *request = [NSMutableURLRequest
18 requestWithURL:url];
19 // 4.1 请求方法
20 request.HTTPMethod = @"POST";
21 // 4.2 请求体
22 NSString *bodyStr = [NSString stringWithFormat:
23 @"username=%@&password=%@",self.username, pwd];
24 request.HTTPBody = [bodyStr dataUsingEncoding:NSUTF8StringEncoding];
25 // 5.发送请求
26 [NSURLConnection sendAsynchronousRequest:request
27 queue:[NSOperationQueue mainQueue] completionHandler:
28 ^(NSURLResponse *response, NSData *data, NSError *connectionError) {
29 // 反序列化数据
30 NSDictionary *result = [NSJSONSerialization JSONObjectWithData:data
31 options:0 error:NULL];
32 // 判断是否登录成功
33 if ([result[@"userId"] intValue] > 0) {
34 [self saveUserInfo];
35 }else{ // 登录失败回调
36 failed();
37 }
38 }];
39 }
在上述代码中,第33~37行代码对登录结果进行逻辑判断,如果userId的值大于0,表示登录成功,这时需要保存用户的信息;如果userId的值小于0,表示登录失败,这时调用failed代码块,以方便向外界传递“登录失败”的信息。
5.用户登录
(1)单击“登录”按钮,通过网络工具单例类实现登录的功能。在ViewController.m文件中,实现login方法,代码如下:
1 #import "ViewController.h"
2 #import "NetworkTools.h"
3 @interface ViewController () <UITextFieldDelegate>
4 @property (weak, nonatomic) IBOutlet UITextField *userNameText;
5 @property (weak, nonatomic) IBOutlet UITextField *passwordText;
6 @end
7 @implementation ViewController
8 - (IBAction)login {
9 NetworkTools *tools = [NetworkTools sharedTools];
10 // 设置用户名和密码
11 tools.username = self.userNameText.text;
12 tools.password = self.passwordText.text;
13 // 登录
14 [tools userLoginFailed:^{
15 // 提示用户
16 UIAlertView *alertView = [[UIAlertView alloc]
17 initWithTitle:@"提示" message:@"用户名或密码错误" delegate:nil
18 cancelButtonTitle:@"OK" otherButtonTitles:nil, nil];
19 [alertView show];
20 }];
21 }
在上述代码中,第9行代码获取了NetworkTools单例,第11~12行代码将文本框输入的用户信息传递给该单例,用于记录用户名和密码,第14行代码调用userLoginFailed:方法安全登录,当登录失败后,提示“用户名或密码错误”的信息。
(2)程序启动后,默认会根据NetworkTools单例的内容进行自动登录,代码如下:
1 #pragma mark - 保存和加载用户信息
2 - (void)loadUserInfo
3 {
4 // 从单例获取用户信息
5 NetworkTools *tools = [NetworkTools sharedTools];
6 self.userNameText.text = tools.username;
7 // 从钥匙串访问密码
8 self.passwordText.text = tools.password;
9 }
10 - (void)viewDidLoad {
11 [super viewDidLoad];
12 [self loadUserInfo];
13 }
(3)处理多个文本框的逻辑,单击“return”键切换到下一个文本框,直到最后一个文本框切换到登录按钮。通过拖曳的方式设置ViewController为Text Field的代理,ViewController需要遵守UITextFieldDelegate协议,并实现相应的代理方法,代码如下:
1 #pragma mark - UITextFieldDelegate
2 - (BOOL)textFieldShouldReturn:(UITextField *)textField
3 {
4 if (textField == self.userNameText) { // 切换到密码
5 [self.passwordText becomeFirstResponder];
6 } else {
7 [self login]; // 登录
8 }
9 return YES;
10 }
6.切换Storyboard
程序首次启动后,显示登录页面,输入正确的用户信息,切换到主页;当用户再次启动后,根据沙盒和钥匙串保存的用户信息,自动登录到主页页面。这个过程需要通过网络工具类传递登录信息,且需要在两个页面切换,通过Window对象切换,采用通知实现,具体步骤如下。
(1)在NetworkTools.h文件中,定义一个表示通知名称的宏,代码如下:
1 #define CZUserLoginStatusChangedNotification
2 @"CZUserLoginStatusChangedNotification"
(2)在NetworkTools.m文件中,在登录成功的部分,插入如下代码:
1 // 发送通知
2 [[NSNotificationCenter defaultCenter] postNotificationName:
3 CZUserLoginStatusChangedNotification object:@"Main"];
在上述代码中,第2~3行代码获取了NSNotificationCenter单例,调用postNotification Name: object:方法发送通知。
(3)在AppDelegate.m文件中,由于没有启动的Storyboard,需要手动创建UIWindow,代码如下:
1 - (BOOL)application:(UIApplication *)application
2 didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
3 // 如果没有启动的StoryBoard,需要手动实例化window
4 self.window = [[UIWindow alloc] initWithFrame:
5 [UIScreen mainScreen].bounds];
6 self.window.backgroundColor = [UIColor whiteColor];
7 [self.window makeKeyAndVisible];
8 // 注册通知
9 [[NSNotificationCenter defaultCenter] addObserver:self
10 selector:@selector(switchStoryboard:)
11 name:CZUserLoginStatusChangedNotification object:nil];
12 // 用户登录
13 [[NetworkTools sharedTools] userLoginFailed:^{
14 [self switchStoryboard:nil];
15 }];
16 return YES;
17 }
当程序启动后,首先调用如上方法。在上述代码中,第9行代码注册了一个监听器,并指定了监听方法switchStoryboard:。
(4)实现switchStoryboard:方法,根据notification的object属性来判断使用哪个Storyboard的控制器,代码如下:
1 - (void)switchStoryboard:(NSNotification *)notification
2 {
3 NSString *sbName = notification.object != nil ? notification.object : @"Login";
4 // 显示主界面
5 UIStoryboard *sb = [UIStoryboard storyboardWithName:sbName bundle:nil];
6 // 切换视图控制器
7 self.window.rootViewController = sb.instantiateInitialViewController;
8 }
在上述代码中,第3行代码定义了一个三目运算符,根据sbName判断,若sbName没值,直接显示登录页面,反之则显示主页面。
7.注销按钮
(1)新建一个表示主页的类HomeTableViewController,继承自UITableViewController,并设置该类为Home.storyboard的关联类。
(2)通过拖曳的方式给“注销”按钮绑定一个单击事件,命名为loginout:方法。
(3)在HomeTableViewController.m文件中,实现loginout:方法,代码如下:
1 - (IBAction)loginout:(UIBarButtonItem *)sender {
2 // 利用通知注销
3 [[NSNotificationCenter defaultCenter] postNotificationName:
4 CZUserLoginStatusChangedNotification object:@"Login"];
5 }
8.运行程序
(1)单击“运行”按钮,运行程序,程序运行成功后,输入“zhangsan”和“zhang”,单击“登录”按钮,切换到主页界面,如图2-47所示。
图2-47 程序的运行结果
(2)再次运行程序,程序直接进入主页界面,实现自动登录的效果。单击主页右上角的“注销”按钮,程序成功地切换到登录页面,实现退出登录的功能。
2.5 文件的上传与下载
在iOS开发中,经常会涉及文件的上传和下载功能,前面已经简单介绍了NSURLConnection的下载,但是容易出现瞬间内存峰值,为此,iOS 7推出了一个NSURLSession类。本节将针对文件上传和下载的内容进行详细的介绍。
2.5.1 上传文件的原理
要想上传文件,需要依赖于POST请求,通过将上传的文件编码到POST请求体中,将该请求体一并发送到服务器。接下来,安装一个Firefox(火狐浏览器),该浏览器可以安装多个插件,方便开发人员调试,通过该浏览器来跟踪POST请求的信息,可分为上传单个文件和多个文件,详细介绍如下。
1.上传单个文件
(1)安装火狐浏览器
双击Firefox安装包,按照提示逐步完成。打开Firefox浏览器,将一个名称为firebug.xpi的插件拖曳到该浏览器,弹出一个提示安装的窗口,如图2-48所示。
图2-48 安装插件的提示窗口
单击图2-48中的“安装”按钮,这时,浏览器右上角位置增加了一个“小虫子”图标,用于剖析Web内部的细节,而且能清晰地查看网站的源代码。
(2)准备资料
在Finder中打开Sites文件夹,将准备好的post文件夹复制到Sites文件夹,其中,upload.html是用于上传文件的脚本,upload.php是用于处理上传功能的脚本。另外,111.txt是用于测试的上传文件。刷新本地服务器,打开upload.html文件,如图2-49所示。
图2-49 Firefox打开upload.html文件
值得一提的是,单击“浏览”按钮,选择任意一个来源的文件,单击“上传”按钮,文件默认会上传到abc文件夹。
(3)跟踪头信息
选中“小虫子”图标,单击“浏览”按钮,选取111.txt,单击“上传”按钮,浏览器底部的窗口展示了跟踪的全部信息,单击菜单“头信息”,它展示了响应头、请求头、上传的请求头的信息,如图2-50所示。
在请求头中,最为重要的就是Content-Type,它对应的值可分为两个部分,前半部分表示内容的类型,后半部分是边界boundary,用于分隔表单中不同部分的数据,后面的一串数字是由浏览器自动生成的,它的格式是不固定的,可以是任意字符。
(4)Post信息
单击菜单“Post”,它展示了发送的请求体的内容,源代码如图2-51所示。和头信息中的boundary部分进行对比不难发现,boundary的内容和请求体的数据部分前的字符串相比少了两个“--”。
图2-50 头信息
图2-51 Post菜单
由图2-51可知,上传的请求体有着严格的格式要求,任何一点错误都会导致上传失败。其中,Content-Disposition指定了表单元素的name属性和文件名称,Content-Type用于告知服务器上传文件的文件类型,一旦指定为application/octet-stream,就表示可以上传任意类型的文件。后面是请求体中最重要的数据部分,该部分就是二进制字符串。依据这几部分的顺序组成的源代码结构如图2-52所示。
图2-52 源代码组成示意图
需要注意的是,请求头与请求体中的Content-Type表示不同的概念,请不要混淆。每行的末尾需添加一个“\r\n”,苹果的上传操作十分麻烦,需要拼接好所需要的字符串格式,才能实现上传文件,另外还要加上头部的Content-Type信息。
2.上传多个文件
(1)准备资料
在Firefox中打开本地服务器,单击“post”文件夹,该目录下还有upload-m.html和upload-m.php两个文件,其中,upload-m.html是用于上传多个文件的脚本,upload-m.php是用于处理上传多个文件的脚本。另外,Finder中有111.txt和222.txt两个用于测试的文件。打开upload-m.html文件,如图2-53所示。
图2-53 Firefox打开upload-m.html文件
从图2-53中可以看出,该脚本有两个“选取文件”的按钮,另外还有一个用于输入文字的文本框,作为上传的文本信息。
(2)跟踪头信息和Post信息
选中“小虫子”图标,单击第一个“选取文件”按钮,选取Finder中的111.txt文件,以同样的方式选取Finder中的222.txt文件,在文本框中输入“嘚瑟”文本内容,单击“上传”按钮,浏览器底部的窗口展示了跟踪请求的全部信息,Post菜单的信息与上传单个文件的信息稍有差异,示意图如图2-54所示。
图2-54 多文件源代码示意图
从图2-54中可以看出,该请求体主要分为3个部分,每一个部分都以边界boundary分开。需要注意的是,多个字段上传,name对应的值中需要有一个“[ ]”标志。
2.5.2 实战演练——上传单个文件
为了大家更好地理解,接下来,通过一个案例演示最原始的上传001.png文件。新建一个Single View Application应用,命名为07-上传单个文件,在ViewController.m文件中,实现相应的逻辑,具体内容如下。
1.获取请求体
定义一个用于获取请求体的方法,该方法封装了拼接上传源代码的功能,外界只需要调用这个方法,就可以直接获取拼接好的请求体的二进制数据,代码如下:
1 #define boundary @"itheima-upload"
2 - (NSData *)formData:(NSData *)fileData fieldName:(NSString *)fieldName
3 fileName:(NSString *)fileName {
4 // 可变Data,用于拼接二进制数据
5 NSMutableData *dataM = [NSMutableData data];
6 // 可变String,用于拼接字符串
7 NSMutableString *strM = [NSMutableString string];
8 [strM appendFormat:@"--%@\r\n", boundary];
9 [strM appendFormat:@"Content-Disposition: form-data; name=\"%@\";
10 filename=\"%@\"\r\n", fieldName, fileName];
11 [strM appendString:@"Content-Type: application/octet-stream\r\n\r\n"];
12 // 先插入 strM
13 [dataM appendData:[strM dataUsingEncoding:NSUTF8StringEncoding]];
14 // 插入文件数据
15 [dataM appendData:fileData];
16 NSString *tail = [NSString stringWithFormat:@"\r\n--%@--", boundary];
17 [dataM appendData:[tail dataUsingEncoding:NSUTF8StringEncoding]];
18 return dataM.copy;
19 }
在上述代码中,获取请求体的方法为formData:fieldName:filename:,该方法需要传递3个参数,其中,fileData表示用于上传文件的二进制数据,fieldName表示服务器的字段名,即name,fileName表示保存到服务器的文件名称,这是HTTP官方要求的格式。
2.上传文件的功能
定义一个用于上传文件的方法,该方法封装了上传文件的功能,外界只需要调用这个方法,就可以实现上传文件的功能,代码如下:
1 #define boundary @"itheima-upload"
2 - (void)uploadFile:(NSData *)fileData fieldName:(NSString *)fieldName
3 fileName:(NSString *)fileName {
4 // 1. url——负责上传文件的脚本
5 NSURL *url = [NSURL URLWithString:
6 @"http://192.168.13.85/post/upload.php"];
7 // 2. request
8 NSMutableURLRequest *request = [NSMutableURLRequest
9 requestWithURL:url];
10 request.HTTPMethod = @"POST";
11 // 2.1 设置 content-type
12 NSString *type = [NSString stringWithFormat:@"multipart/form-data;
13 boundary=%@", boundary];
14 [request setValue:type forHTTPHeaderField:@"Content-Type"];
15 // 2.2 设置数据体
16 request.HTTPBody = [self formData:fileData fieldName:fieldName
17 fileName:fileName];
18 // 3. connection
19 [NSURLConnection sendAsynchronousRequest:request
20 queue:[NSOperationQueue mainQueue] completionHandler:
21 ^(NSURLResponse *response, NSData *data, NSError *connectionError) {
22 NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data
23 options:0 error:NULL]);
24 }];
25 }
在上述代码中,上传文件的方法名为uploadFile:fieldName:filename:该方法需要传递3个参数,这3个参数与前面方法的参数一致。其中,第12~14行代码调用setValue:forHTTPHeaderField:方法给Content-type赋值。
3.单击屏幕,上传文件
导入001.png资源,从mainBundle中获取该图片的路径,将其转换为二进制数据,调用上传的方法,将其进行上传,代码如下:
1 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
2 // 1.data
3 NSString *path = [[NSBundle mainBundle] pathForResource:@"001.png"
4 ofType:nil];
5 NSData *fileData = [NSData dataWithContentsOfFile:path];
6 [self uploadFile:fileData fieldName:@"userfile" fileName:@"xxx.png"];
7 }
4.运行程序
单击“运行”按钮运行程序,程序运行成功后,单击模拟器的屏幕,abc目录添加了一个xxx.png文件,如图2-55所示。
图2-55 上传成功的xxx.png
2.5.3 实战演练——上传多个文件
为了大家更好地理解,接下来,通过一个案例演示如何通过最原始的方式上传001.png和demo.jpg文件。新建一个Single View Application应用,命名为08-上传多个文件,在ViewController.m文件中,实现相应的逻辑,具体内容如下。
1.获取请求体
定义一个用于获取请求体的方法,该方法封装了拼接源代码字符串的功能,外界只需要调用这个方法,就可以直接获取拼接好的请求体的二进制数据,代码如下:
1 - (NSData *)formData:(NSDictionary *)fileDict
2 fieldName:(NSString *)fieldName params:(NSDictionary *)params {
3 NSMutableData *dataM = [NSMutableData data];
4 // 1. 上传文件 - 遍历字典
5 [fileDict enumerateKeysAndObjectsUsingBlock:^(NSString *fileName,
6 NSData *fileData, BOOL *stop) {
7 // 可变字符串
8 NSMutableString *strM = [NSMutableString string];
9 [strM appendFormat:@"--%@\r\n", boundary];
10 [strM appendFormat:@"Content-Disposition: form-data; name=\"%@\";
11 filename=\"%@\"\r\n", fieldName, fileName];
12 [strM appendString:@"Content-Type:
13 application/octet-stream\r\n\r\n"];
14 // 先插入 strM
15 [dataM appendData:[strM dataUsingEncoding:NSUTF8StringEncoding]];
16 // 插入文件数据
17 [dataM appendData:fileData];
18 [dataM appendData:[@"\r\n"
19 dataUsingEncoding:NSUTF8StringEncoding]];
20 }];
21 // 2. 拼接数据参数 - 遍历字典
22 [params enumerateKeysAndObjectsUsingBlock:
23 ^(id key, id obj, BOOL *stop) {
24 NSMutableString *strM = [NSMutableString string];
25 [strM appendFormat:@"--%@\r\n", boundary];
26 [strM appendFormat:@"Content-Disposition: form-data;
27 name=\"%@\"\r\n\r\n", key];
28 [strM appendFormat:@"%@\r\n", obj];
29 // 添加到 dataM
30 [dataM appendData:[strM dataUsingEncoding:NSUTF8StringEncoding]];
31 }];
32 // 3. 末尾字符串
33 NSString *tail = [NSString stringWithFormat:@"--%@--", boundary];
34 [dataM appendData:[tail dataUsingEncoding:NSUTF8StringEncoding]];
35 return dataM.copy;
36 }
在上述代码中,获取请求体的方法为formData:fieldName:filename:,该方法需要传递3个参数,其中,fileDict是一个字典,用于保存多个文件,fieldName表示服务器的字段名,params表示输入的文字。
2.上传多个文件的功能
定义一个用于上传多个文件的方法,该方法封装了上传文件的功能,外界只需要调用这个方法,就可以实现上传多个文件,代码如下:
1 - (void)uploadFile:(NSDictionary *)fileDict fieldName:(NSString *)fieldName
2 params:(NSDictionary *)params {
3 // 1. url -负责上传文件的脚本
4 NSURL *url = [NSURL URLWithString:
5 @"http://192.168.13.85/post/upload-m.php"];
6 // 2. request
7 NSMutableURLRequest *request = [NSMutableURLRequest
8 requestWithURL:url];
9 request.HTTPMethod = @"POST";
10 // 2.1 设置 content-type
11 NSString *type = [NSString stringWithFormat:@"multipart/form-data;
12 boundary=%@", boundary];
13 [request setValue:type forHTTPHeaderField:@"Content-Type"];
14 // 2.2 设置数据体
15 request.HTTPBody = [self formData:fileDict fieldName:fieldName
16 params:params];
17 // 3. connection
18 [NSURLConnection sendAsynchronousRequest:request
19 queue:[NSOperationQueue mainQueue] completionHandler:
20 ^(NSURLResponse *response, NSData *data, NSError *connectionError) {
21 NSLog(@"%@", [NSJSONSerialization JSONObjectWithData:data
22 options:0 error:NULL]);
23 }];
24 }
在上述代码中,上传多个文件的方法名为uploadFile:fieldName:filename:该方法同样需要传递3个参数,这3个参数与上一个方法的参数保持一致。
3.单击屏幕,上传多个文件
导入001.png和demo.jpg资源,导入一个“NSArray+Log“分类,用于输出中文。从mainBundle中获取该图片的路径,将其转换为二进制数据,调用上传的方法,将多个文件进行上传,代码如下:
1 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
2 // 1. data
3 NSString *path1 = [[NSBundle mainBundle] pathForResource:@"001.png"
4 ofType:nil];
5 NSData *fileData1 = [NSData dataWithContentsOfFile:path1];
6 // 2. data
7 NSString *path2 = [[NSBundle mainBundle] pathForResource:@"demo.jpg"
8 ofType:nil];
9 NSData *fileData2 = [NSData dataWithContentsOfFile:path2];
10 // 通过字典传递参数
11 NSDictionary *fileDict = @{@"abc.png": fileData1, @"abc.jpg": fileData2};
12 // 数据参数
13 NSDictionary *params = @{@"status": @"嘚瑟"};
14 // 多个文件上传,字段名需要包含 []
15 [self uploadFile:fileDict fieldName:@"userfile[]" params:params];
16 }
4.运行程序
单击“运行”按钮运行程序,程序运行成功后,单击模拟器的屏幕,abc目录添加了abc.png和abc.jpg两个文件,如图2-56所示。
图2-56 上传成功的abc.png和abc.jpg
与此同时,控制台的输出如图2-57所示。
图2-57 程序的输出结果
2.5.4 NSURLConnection下载
所谓下载,就是把服务器的内容存放到本地,前面已经简单了解了NSURLConnection的使用,而针对异步下载大文件,它存在着以下两个缺陷:
(1)缺少文件跟进进度;
(2)出现瞬间内存峰值,造成应用程序闪退。
首先解决下载进度跟进的问题,iOS提供了一个NSURLConnectionDataDelegate协议,只要遵守了该协议的对象,就能够监听关于文件下载的整个过程。NSURLConnection类提供了3个初始化方法,只用于建立连接,而且可以指定代理对象,定义格式如下:
- (instancetype)initWithRequest:(NSURLRequest *)request
delegate:(id)delegate startImmediately:(BOOL)startImmediately;
- (instancetype)initWithRequest:(NSURLRequest *)request
delegate:(id)delegate;
+ (NSURLConnection*)connectionWithRequest:(NSURLRequest *)request
delegate:(id)delegate;
从上述定义可以看出,第1个方法比第2个方法多一个参数startImmediately,该参数表示是否立即下载数据,YES代表立即下载,并把connection加入到当前的Run Loop中;NO代表只建立连接,不要下载数据,需要手动调用start方法来下载数据。
指定了代理对象后,该对象需要遵守NSURLConnectionDataDelegate协议,该协议定义了几个常用的方法,定义格式如下:
// 开始接收到服务器的响应时调用
- (void)connection:(NSURLConnection *)connection
didReceiveResponse:(NSURLResponse *)response;
// 接收到服务器返回的数据时调用,若数据比较大时会调用多次
- (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data;
//服务器返回的数据完全接收后调用
- (void)connectionDidFinishLoading:(NSURLConnection *)connection;
//请求出错时调用,如请求超时
- (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error;
当一个文件比较大时,会多次调用接收数据的方法,根据该方法来累加每次下载文件的大小,实现文件下载进度的跟进,可通过如下代码实现:
1 // 1. 接收到服务器响应(状态行/响应头)
2 - (void)connection:(NSURLConnection *)connection
3 didReceiveResponse:(NSURLResponse *)response {
4 self.expectedContentLength = response.expectedContentLength;
5 self.fileSize = 0;
6 }
7 // 2. 接收到二进制数据(可能会多次)
8 - (void)connection:(NSURLConnection *)connection
9 didReceiveData:(NSData *)data {
10 self.fileSize += data.length;
11 float progress = (float)self.fileSize / self.expectedContentLength;
12 NSLog(@"%f", progress);
13 // 拼接数据
14 [self.fileData appendData:data];
15 }
16 // 3. 网络请求结束(断开连接)
17 - (void)connectionDidFinishLoading:(NSURLConnection *)connection {
18 // 写入磁盘
19 [self.fileData writeToFile:@"/Users/apple/Desktop/aaa.mp4"
20 atomically:YES];
21 // 释放数据
22 self.fileData = nil;
23 }
24 // 4. 网络连接错误,任何的网络访问都有可能出错
25 - (void)connection:(NSURLConnection *)connection
26 didFailWithError:(NSError *)error {
27 NSLog(@"%@", error);
28 }
expectedContentLength、fileSize、fileData依次表示文件的总大小、当前接收的大小、文件数据。在上述代码中,第2~6行代码代表接收到服务器的响应调用的方法,在该方法中确定文件的最终大小和当前接收的初始大小;第8~15行代码代表接收到服务器返回的二进制数据调用的方法,在该方法内拼接当前已经接收的数据大小和接收的数据;第17~23行代码代表断开连接调用的方法,在该方法中将全部接收完的数据写入到指定文件,运行结果如图2-58所示。
图2-58 程序的运行效果
经历以上过程,就实现了下载进度的跟进。最后来解决内存峰值的问题,关于出现这个问题的原因,主要是每次接收到的部分数据累积后,在断开连接时实现整个文件的写入造成的。假设每一次接收到的部分数据都写入文件,就解决了内存峰值的问题。iOS提供了一个NSFileHandle类,用于对文件的内容进行读取和写入操作,该类也提供了一些常用的方法,定义格式如下:
// 打开一个文件准备写入
+ (instancetype)fileHandleForWritingAtPath:(NSString *)path;
// 跳到文件的末尾
- (unsigned long long)seekToEndOfFile;
// 写入数据
- (void)writeData:(NSData *)data;
// 关闭文件
- (void)closeFile;
每次打开文件,指向文件的指针都会在头部位置,指针的位置决定了写入数据的位置,要想实现追加数据,每次写入数据之前,将指针的位置移动到末尾,以实现追加数据的效果,seekToEndOfFile方法是用于改变指针位置的。
每次接收到数据后,单独将数据写入磁盘,修改上面进度跟进的部分代码,修改后的代码如下:
1 // 1. 接收到服务器响应(状态行/响应头)
2 - (void)connection:(NSURLConnection *)connection
3 didReceiveResponse:(NSURLResponse *)response {
4 self.expectedContentLength = response.expectedContentLength;
5 self.fileSize = 0;
6 }
7 // 2. 接收到二进制数据(可能会多次)
8 - (void)connection:(NSURLConnection *)connection
9 didReceiveData:(NSData *)data {
10 self.fileSize += data.length;
11 float progress = (float)self.fileSize / self.expectedContentLength;
12 NSLog(@"%f", progress);
13 // 拼接数据
14 [self writeData:data];
15 }
16 - (void)writeData:(NSData *)data {
17 // 如果文件不存在,fp == nil
18 NSFileHandle *fp = [NSFileHandle
19 fileHandleForWritingAtPath:@"/Users/apple/Desktop/aaa.mp4"];
20 if (fp == nil) {
21 // 单独将数据写入磁盘
22 [data writeToFile:@"/Users/apple/Desktop/aaa.mp4" atomically:YES];
23 } else {
24 //将文件指针挪动到后面
25 [fp seekToEndOfFile];
26 //写入数据
27 [fp writeData:data];
28 //关闭文件(在文件操作时,一定记住,打开关闭要成对出现)
29 [fp closeFile];
30 }
31 }
32 // 3. 网络连接错误,任何的网络访问都有可能出错
33 - (void)connection:(NSURLConnection *)connection
34 didFailWithError:(NSError *)error {
35 NSLog(@"%@", error);
36 }
在上述代码中,第16~31行代码是自定义的一个方法,用于追加数据。其中,第20~23行代码使用if else语句进行判断,如果fp不存在,第1次写入到aaa.mp4文件;如果fp存在,移动文件指针到末尾位置,追加数据并关闭文件。
经历以上过程,下载进度跟进和内存峰值的问题就解决了。
2.5.5 NSURLSession介绍
NSURLSession是iOS7中新的网络接口,它与NSURLConnection是并列的关系,当程序在前台时,NSURLSession与NSURLConnection可以互为替代工作,而且NSURLSession支持后台网络操作,除非用户将其强行关闭。接下来,大家看一下NSURLSession的结构图,如图2-59所示。
图2-59 NSURLSession的结构图
由图2-59可知,NSURLSession从字面上表示会话的意思,每一个NSURLSession对象都是根据一个NSURLSessionConfiguration初始化的,该类用于定义和配置会话,如Cookie、安全性、缓存策略等。NSURLSessionConfiguration类有3个类构造方法,代表着不同的工作模式,定义格式如下:
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
+ (NSURLSessionConfiguration *)
backgroundSessionConfigurationWithIdentifier:(NSString *)identifier;
针对它们的详细介绍如下:
(1)defaultSessionConfiguration:默认会话配置,类似于NSURLConnection的标准配置,使用的是基于磁盘缓存的持久化策略,使用用户keychain中保存的证书进行认证授权。
(2)ephemeralSessionConfiguration:临时会话配置,该配置不会使用磁盘保存任何数据,所有与会话相关的Caches、证书、Cookies等只会被保存在内存中。因此,当程序使会话无效时,这些缓存的数据就会被自动清空。
(3)backgroundSessionConfigurationWithIdentifier::后台会话配置,该配置会在后台完成上传和下载,创建NSURLSessionConfiguration对象时需要提供一个ID,用于标识完成工作的后台会话。
另外,NSURLSession的另一个重要组成部分就是NSURLSessionTask,主要负责处理数据的加载,以及客户端与服务器端之间的文件和数据的上传或者下载任务。NSURLSessionTask类是一个抽象类,它有3 个具体的子类是可以直接使用的,如 图2-60所示。
图2-60 NSURLSessionTask的继承体系
图2-60介绍了NSURLSessionTask类的3个子类,这3个类封装了现代应用程序的3个基本网络任务,针对它们的详细介绍如下。
(1)NSURLSessionDataTask:用于处理一般的NSData类型的数据,如通过GET或者POST方法从服务器获取的JSON或者XML,但是该类不支持后台获取。
(2)NSURLSessionUploadTask:用于PUT方法上传文件,而且支持后台上传。
(3)NSURLSessionDownloadTask:用于下载文件,而且支持后台下载。
需要注意的是,默认情况下任务是挂起的,通过调用resume方法继续执行任务。前面介绍了NSURLSession的两个主要组成部分,要想使用NSURLSession,大致需要如下两个步骤。
1.使用NSURLSessionConfiguration配置NSURLSession对象
要想创建一个NSURLSession对象,iOS提供了3个类方法,这3个方法的定义格式如下:
+ (NSURLSession *)sharedSession;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration;
+ (NSURLSession *)sessionWithConfiguration:(NSURLSessionConfiguration *)configuration
delegate:(id <NSURLSessionDelegate>)delegate delegateQueue:(NSOperationQueue *)queue;
在上述代码中,第1个方法是一个静态的方法,该类使用共享的会话,该会话使用全局的Cache、Cookie和证书;第2个方法是创建对应配置的会话,与NSURLSession Configuration对象配合使用;第3个方法是可以定制会话的类型,而且还可以指定会话的委托和该委托所处的队列。
当不再需要连接时,可以调用invalidateAndCancel方法直接关闭,或者调用finishTasks AndInvalidate方法等待当前的任务结束后关闭。这时,delegate会收到URLSession: didBecome InvalidWithError:消息,会被解引用。
2.使用NSURLSession对象来启动一个NSURLSessionTask对象
所有的任务都是由NSURLSession对象发起的,为此,NSURLSession类提供了4个方法,用于启动一个任务,具体定义格式如下:
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url;
- (NSURLSessionDataTask *)dataTaskWithRequest:(NSURLRequest *)request
completionHandler:(void (^)(NSData *data, NSURLResponse *response,
NSError *error))completionHandler;
- (NSURLSessionDataTask *)dataTaskWithURL:(NSURL *)url
completionHandler:(void (^)(NSData *data, NSURLResponse *response,
NSError *error))completionHandler;
在上述代码中,前两个方法只有1个参数,通过传入一个request或url创建一个任务;后面两个方法多了1个参数,通过该参数指定回调的代码块,而且这两个方法回调默认是异步执行的。
2.5.6 实战演练——使用NSURLSession实现下载功能
针对下载功能这部分,iOS提供了一个NSURLSessionDownloadTask子类,用于处理下载方面的功能。同时,NSURLSession类也提供了几个方法来处理下载任务,定义格式如下:
// 通过一个request创建下载任务
- (NSURLSessionDownloadTask *)downloadTaskWithRequest:(NSURLRequest *)
request completionHandler:(void (^)(NSURL *location,
NSURLResponse *response, NSError *error))completionHandler;
//通过一个url创建下载任务
- (NSURLSessionDownloadTask *)downloadTaskWithURL:(NSURL *)url
completionHandler:(void (^)(NSURL *location, NSURLResponse *response,
NSError *error))completionHandler;
//实现断点续传
- (NSURLSessionDownloadTask *)downloadTaskWithResumeData:(NSData *)
resumeData completionHandler:(void (^)(NSURL *location,
NSURLResponse *response, NSError *error))completionHandler;
在上述代码中,这3个方法都有一个回调的代码块completionHandler,该代码块有3个参数,location是一个NSURL类型的值,表示下载的临时文件目录,如果要保存文件,需要将文件保存至沙盒。
除此之外,为了能够跟进文件下载的进度,NSURLSession类定义了一个NSURLSession DownloadDelegate协议,该协议提供了3个方法供外界监听,定义格式如下:
@protocol NSURLSessionDownloadDelegate <NSURLSessionTaskDelegate>
// 下载完成,该方法必须实现
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didFinishDownloadingToURL:(NSURL *)location;
@optional
// 每下载完一部分数据时就会调用该方法,可能会调用多次
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didWriteData:(int64_t)bytesWritten
totalBytesWritten:(int64_t)totalBytesWritten
totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite;
// 断点续传的方法
- (void)URLSession:(NSURLSession *)session
downloadTask:(NSURLSessionDownloadTask *)downloadTask
didResumeAtOffset:(int64_t)fileOffset
expectedTotalBytes:(int64_t)expectedTotalBytes;
@end
在上述代码中,第1个方法是必须要实现的,第2个方法有5个参数,其中,bytesWritten表示本次下载的字节数,totalBytesWritten表示已经下载的字节数,totalBytesExpectedToWrite表示下载文件的总大小。需要注意的是,iOS 7中后两个方法也是必须要实现的。
为了大家更好地理解,接下来,通过一个下载视频的案例来讲解如何使用NSURLSession启动下载任务,跟进文件下载的进度,详细介绍如下。
1.创建工程,设计界面
(1)在Finder中打开 Sites 文件夹,将之前准备好的321.mp4测试文件复制到该目录下。
(2)新建一个Single View Application应用,命名为09- NSURLSession下载。
(3)进入Main.storyboard,从对象库拖曳1个Progress View、2个Label到程序界面,其中,Progress View用于展示下载的进度视图,设置该视图的Style为Bar,Progress的值为0,设计好的页面如图2-61所示。
图2-61 设计好的程序界面
2.创建控件对象的关联
单击Xcode 6.1界面右上角的图标,进入控件与代码的关联界面。依次选中Progress View和右侧的Label,分别添加表示进度视图和进度提示标签的属性。
3.通过代码实现下载的功能
从服务器端下载资源,并跟进下载的进度,通过文字和图片的方式展示到界面,详细介绍如下。
(1)定义一个表示会话的属性,并通过懒加载的方式进行初始化,具体代码如下:
1 #import "ViewController.h"
2 @interface ViewController () <NSURLSessionDownloadDelegate>
3 @property (nonatomic, strong) NSURLSession *session;
4 @property (weak, nonatomic) IBOutlet UIProgressView *progressView;
5 @property (weak, nonatomic) IBOutlet UILabel *mesLabel;
6 @end
7 @implementation ViewController
8 #pragma mark - 懒加载
9 - (NSURLSession *)session
10 {
11 if(_session == nil){
12 // 默认会话配置
13 NSURLSessionConfiguration *config = [NSURLSessionConfiguration
14 defaultSessionConfiguration];
15 // 创建会话,并指定代理
16 _session = [NSURLSession sessionWithConfiguration:config
17 delegate:self delegateQueue:nil];
18 }
19 return _session;
20 }
21 @end
在上述代码中,第16~17行代码调用sessionWithConfiguration:delegate:delegateQueue:方法创建了一个带有默认配置的会话,并且指定了代理对象和代理工作的队列。代理工作的队列与下载没有任何关系,它仅仅只是对其进行指定。下载本身是有一个独立的线程“顺序”完成的。无论选择什么队列,都不会影响主线程。
(2)单击屏幕,根据指定的路径,从服务器端访问视频资源,代码如下:
1 - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
2 {
3 // 1.确定url
4 NSURL *url = [NSURL URLWithString:@"http://localhost/321.mp4"];
5 // 2.下载
6 NSURLSessionDownloadTask *task = [self.session
7 downloadTaskWithURL:url];
8 // 3.继续任务
9 [task resume];
10 }
在上述代码中,要想使用NSURLSession完成下载功能仅仅需要3个步骤,只要指定访问的路径,根据该路径由NSURLSession对象启动下载任务,最后调用resume方法将挂起的任务继续执行,就完成了下载文件的操作。
(3)为了跟进文件的下载进度,需要遵守NSURLSessionDownloadDelegate协议,实现相应的方法,具体代码如下:
1 #pragma mark -NSURLSessionDownloadDelegate
2 // 完成下载
3 - (void)URLSession:(NSURLSession *)session
4 downloadTask:(NSURLSessionDownloadTask *)downloadTask
5 didFinishDownloadingToURL:(NSURL *)location
6 {
7 NSLog(@"%@", location);
8 }
9 // 下载进度
10 - (void)URLSession:(NSURLSession *)session
11 downloadTask:(NSURLSessionDownloadTask *)downloadTask
12 didWriteData:(int64_t)bytesWritten
13 totalBytesWritten:(int64_t)totalBytesWritten
14 totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite
15 {
16 float progress=(float)totalBytesWritten / totalBytesExpectedToWrite;
17 dispatch_async(dispatch_get_main_queue(), ^{
18 self.progressView.progress = progress;
19 self.mesLabel.text = [NSString stringWithFormat:
20 @"%0.2f%%", progress * 100];
21 });
22 }
23 // 断点续传
24 - (void)URLSession:(NSURLSession *)session
25 downloadTask:(NSURLSessionDownloadTask *)downloadTask
26 didResumeAtOffset:(int64_t)fileOffset
27 expectedTotalBytes:(int64_t)expectedTotalBytes
28 {
29 NSLog(@"%s", __FUNCTION__);
30 }
在上述代码中,第10~22行代码是跟踪下载进度的方法,其中,第17行代码获取了主队列,第18~19行代码在主队列中设置了对界面的相关操作。
4.运行程序
单击“运行”按钮运行程序,程序运行成功后,单击屏幕,视频下载的进度以图片和数字的效果动态地展现,如图2-62所示。
图2-62 程序的运行结果
2.6 第三方框架
所谓第三方框架,就是网络高手编写的框架程序,针对某一个具体的技术问题,提供完善的解决方案,它具有功能强大、良好的错误处理能力、可持续升级维护的特点。
在iOS开发中,不可避免地需要用到一些第三方框架,这些框架提供了很多的功能,既提高了开发的效率,又可以从公开的源代码中受益。最常用到的框架就是SDWebImage和AFNetworking,分别用于不同的场合。接下来,本节将针对SDWebImage和AFNetworking这两个框架进行详细的讲解。
2.6.1 SDWebImage介绍
SDWebImage是一个特别厉害的网络图片处理框架,该类库提供了一个UIImageView的分类,支持加载来自网络的远程图片,具有缓存管理、异步下载、同一个URL下载次数的控制和优化、支持gif动态图等特征。接下来,针对该框架实现的重要功能进行详细的介绍,具体内容如下。
1.类库的下载
Git是一个分布式的版本控制系统,用于高效地处理任何大小的项目。GitHub是一个基于版本控制的社交网站,作为开源的代码库和版本控制系统,它拥有了越来越多的用户,已经成为了管理软件开发及发现已有代码的首选方法。
(1)打开GitHub的官方网站(https://github.com),如图2-63所示。
(2)在图2-63所示页面的顶部输入搜索的文字“SDWebImage”,单击“return”键,跳入搜索完成的界面,如图2-64所示。
从图2-64中可以看出,第1个选项“rs/SDWebImage”对应的小星数量最多,代表着它的口碑极好。
(3)单击“rs/ SDWebImage”选项,切换到该框架的详细介绍和下载页面,如图2-65所示。
图2-63 GitHub的官方网站
图2-64 搜索SDWebImage完成的界面
图2-65 SDWebImage的详细文档
(4)单击图2-65中的“Download ZIP”按钮,下载源码到Finder中的“下载”文件夹,在该目录中打开刚刚下载的文件夹,这时会看到“SDWebImage”文件夹,双击打开该文件夹,如图2-66所示。
图2-66 SDWebImage目录
从图2-66中可以看出,SDWebImage目录下包含了该框架的所有源代码,如果要使用该框架的方法,只要将该目录添加到项目中,并且导入相应的头文件即可。需要注意的是,由于绝大多数第三方框架可能会对其他框架有所依赖,为了避免错误,导入框架后需要运行程序来检查是否编译通过。
2.UITableView使用UIImageView+WebCache类
要想使用UIImageView+WebCache分类,前提是要#import导入UIImageView+Web Cache.h头文件,在tableview的tableView:cellForRowAtIndexPath:方法中调用sd_setImageWith URL:placeholderImage:方法,从异步下载到缓存管理一步到位,示例代码如下:
1 - (UITableViewCell *)tableView:(UITableView *)tableView
2 cellForRowAtIndexPath:(NSIndexPath *)indexPath
3 {
4 static NSString *MyIdentifier = @"MyIdentifier";
5 UITableViewCell *cell = [tableView
6 dequeueReusableCellWithIdentifier:MyIdentifier];
7 if (cell == nil){
8 cell = [[[UITableViewCell alloc]
9 initWithStyle:UITableViewCellStyleDefault
10 reuseIdentifier:MyIdentifier] autorelease];
11 }
12 [cell.imageView sd_setImageWithURL:[NSURL
13 URLWithString:@"http://www.domain.com/path/to/image.jpg"]
14 placeholderImage:[UIImage imageNamed:@"placeholder.png"]];
15 cell.textLabel.text = @"My Text";
16 return cell;
17 }
在上述代码中,第12~14行代码调用sd_setImageWithURL:placeholderImage:方法下载图片,第1个参数传入图片的URL路径,第2个参数表示图像未下载完成时设定的占位图片。
除此之外,还可以使用回调block,不管图像检索是否成功完成,都可以被通知到有关图像下载的进展,示例代码如下:
1 [cell.imageView sd_setImageWithURL:[NSURL URLWithString:
2 @"http://www.domain.com/image.jpg"]placeholderImage:
3 [UIImage imageNamed:@"placeholder.png"]
4 completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType,
5 NSURL *imageURL) {
6 ... completion code here ...
7 }];
在上述代码中,该方法多了一个回调的block。需要注意的是,如果图像请求在完成之前被取消,成功和失败的块都无法被调用。
3.使用SDWebImageManager类进行异步加载的工作
UIImageView+WebCache分类后面要介绍SDWebImageManager类,它能够与图像缓存池异步下载技术相结合,除了UIView类以外,还能够直接使用该类在其他环境中进行网络图片的下载和缓存,示例代码如下:
1 // 创建SDWebImageManager单例
2 SDWebImageManager *manager = [SDWebImageManager sharedManager];
3 // 将需要缓存的图片加载进来
4 UIImage *cachedImage = [manager imageWithURL:url];
5 if (cachedImage) {
6 // 如果Cache命中,则直接利用缓存的图片进行有关操作
7 // Use the cached image immediatly
8 } else {
9 // 如果Cache没有命中,则去下载指定网络位置的图片,并且给出一个委托方法
10 // Start an async download
11 [manager downloadWithURL:url delegate:self];
12 }
在上述代码中,第11行代码调用了downloadWithURL: delegate:方法,设置了代理属性,该代理对象需要遵守SDWebImageManagerDelegate协议,并且实现协议中的webImageManager:didFinishWithImage:方法,用于对下载完成的图片进行的操作,示例代码如下:
1 // 当下载完成后,调用回调方法,使下载的图片显示
2 - (void)webImageManager:(SDWebImageManager *)imageManager
3 didFinishWithImage:(UIImage *)image {
4 // Do something with the downloaded image
5 }
4.独立的异步图像下载
有时可能会单独用到异步图片下载,此时则一定要用downloaderWithURL:delegate:方法来建立一个SDWebImageDownloader实例,示例代码如下:
downloader = [SDWebImageDownloader downloaderWithURL:url delegate:self];
这样,SDWebImageDownloaderDelegate协议的imageDownloader:didFinishWithImage:方法被调用时,下载会立即开始并完成。
5.独立的异步图像缓存
SDImageCache类提供一个创建空缓存的实例,并通过imageForKey:方法来寻找当前缓存,示例代码如下:
UIImage *myCachedImage = [[SDImageCache sharedImageCache]
imageFromKey:myCacheKey];
另外,要想存储一个图像到缓存中,需要使用storeImage: forKey:方法,示例代码如下:
[[SDImageCache sharedImageCache] storeImage:myImage forKey:myCacheKey];
默认情况下,图像将被存储在内存缓存和磁盘缓存中,如果仅是想保存在内存缓存中,要使用storeImage:forKey:toDisk:方法的第3个参数带一负值来替代。
总而言之,SDWebImage用法简单,功能却极其强大,大大地提高了网络图片的处理效率。
2.6.2 AFNetworking和ASIHTTPRequest框架
ASIHTTPRequest底层是基于纯C语言的CFNetwork框架,该框架提供了一个更加方便的HTTP网络传输的封装,它是最早设计的框架,功能非常强大,只是不支持ARC,而且现在已经停止更新。
AFNetworking是在ASIHTTPRequest之后出现的框架,该框架的底层是基于OC的NSURLConnection和NSURLSession实现的,目前使用比较广泛,它既提供了丰富的API,又提供了完善的错误解决方案,使用起来更加简单。这两个框架的结构,如图2-67所示。
图2-67 AFN与ASI的结构
要想使用AFNetworking第三方框架,需要到GitHub网站下载,按照前面介绍的步骤,下载AFNetworking源代码,将AFNetworking文件夹添加到项目中,并使用#import导入AFNetworking.h头文件。接下来,针对AFNetworking框架的使用进行详细讲解。
1.AFHTTPRequestOperationManager
AFHTTPRequestOperationManager表示请求管理者,它封装了通过HTTP与Web应用程序进行通信的常用方法,包括创建请求、响应序列化、网络连接监控、数据安全等。要想创建一个请求管理者,可通过如下方法:
+ (instancetype)manager;
从方法的定义可以看出,该方法是一个类方法,用于创建一个共享的请求管理者实例,供全局使用。
2.GET请求
AFHTTPRequestOperationManager类提供了一个用于发送GET请求的方法,该方法的定义格式如下:
- (AFHTTPRequestOperation *)GET:(NSString *)URLStringparameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
从方法的定义可以看出,该方法需要传递多个参数,URLString表示路径字符串;parameters表示设置请求的参数;success表示请求成功后回调的代码块,用于处理返回的数据;failure表示请求失败后回调的代码块,用于处理error,示例代码如下:
1 AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager
2 manager];
3 [manager GET:@"http://example.com/resources.json" parameters:nil
4 success:^(AFHTTPRequestOperation *operation, id responseObject) {
5 NSLog(@"JSON: %@", responseObject);
6 } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
7 NSLog(@"Error: %@", error);
8 }];
该方法仅需要传递一个表示URL的字符串,无需再关心URL或者URLRequest的概念,最重要的是返回的二进制数据,默认会反序列化为NSDictionary和NSArray类型,无需再做任何反序列化的处理。完成回调的线程是主线程,无需再考虑线程间通信的问题。
3.POST请求
AFHTTPRequestOperationManager类提供了一个用于发送POST请求的方法,该方法的定义格式如下:
- (AFHTTPRequestOperation *)POST:(NSString *)URLStringparameters:(id)parameters
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
从方法的定义可以看出,该方法同样需要传递4个参数,并且这4个参数表示的意义与上面的方法都一样,只是请求的方法不同,而且请求体一般通过parameters参数传递,示例代码如下:
1 AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager
2 manager];
3 NSDictionary *parameters = @{@"foo": @"bar"};
4 [manager POST:@"http://example.com/resources.json" parameters:parameters
5 success:^(AFHTTPRequestOperation *operation, id responseObject) {
6 NSLog(@"JSON: %@", responseObject);
7 } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
8 NSLog(@"Error: %@", error);
9 }];
在上述方法中,第3行代码定义了一个NSDictionary包装的参数,用于设置请求参数,这样,开发者就不用再关注URL的格式了,也不必再设置请求方法和请求体。需要注意的是,针对参数中的特殊字符或者中文字符,不必再考虑百分号转义。
除此之外,该框架还提供了一个采用POST上传的方法,相比于上一个方法,本方法多了一个block参数,定义格式如下:
- (AFHTTPRequestOperation *)POST:(NSString *)URLString parameters:(id)parameters
constructingBodyWithBlock:(void (^)(id <AFMultipartFormData> formData))block
success:(void (^)(AFHTTPRequestOperation *operation, id responseObject))success
failure:(void (^)(AFHTTPRequestOperation *operation, NSError *error))failure;
在上述定义中,本方法多了一个回调的block块,该代码块会返回一个formData参数,用于将数据追加到请求体中,该参数是一个默认遵守了AFMultipartFormData协议的对象,该协议定义了多个方法来追加上传的数据,示例代码如下:
1 AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager
2 manager];
3 NSDictionary *parameters = @{@"foo": @"bar"};
4 NSURL *filePath = [NSURL fileURLWithPath:@"file://path/to/image.png"];
5 [manager POST:@"http://example.com/resources.json" parameters:parameters
6 constructingBodyWithBlock:^(id<AFMultipartFormData> formData) {
7 [formData appendPartWithFileURL:filePath name:@"image" error:nil];
8 } success:^(AFHTTPRequestOperation *operation, id responseObject) {
9 NSLog(@"Success: %@", responseObject);
10 } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
11 NSLog(@"Error: %@", error);
12 }];
2.7 本章小结
本章主要介绍了关于网络编程的内容,首先介绍了网络编程的基本概念,包括URL、TCP/IP、Socket等,接着简单地介绍了原生网络框架NSURLConnection的使用,并结合Web视图加载了百度网页,然后介绍了数据解析的内容,特别是XML文档和JSON文档的解析,再接着介绍了POST和GET方法,着重讲解了数据安全的内容,接着介绍了POST的上传和NSURLConnection和NSURLSession的下载,最后讲解了最常用的第三方框架。在实际开发中,公司都使用第三方框架开发,但是掌握网络编程的原理内容也尤为重要,有助于更好地理解。
【思考题】
1.简述HTTP和HTTPS协议的区别。
2.简述GET方法和POST方法的区别。