第2章 网络编程

第2章 网络编程

学习目标

0  1

  • 了解网络的基本概念,包括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可知,客户端要想访问数据,首先要提交一个请求,用于告知服务器想要的数据。服务器接收到请求后,根据这个请求到数据库查找相应的资源,无论服务器是否成功拿到资源,都会将结果返回给客户端,这个过程就是响应。

..\tu\0201.tif

图2-1 网络编程的示意图

值得一提的是,网络上所有的数据都是二进制数据,并且以二进制流的形式从一个节点到另一个节点。

2.1.2 URL介绍

URL的全称是Uniform Resource Locator,即统一资源定位符,通过一个URL可以找到互联网上唯一的资源,类似于计算机上一个文件的路径。为了大家更好地理解,接下来,通过图2-2来描述。

C:\Documents and Settings\Administrator\桌面\15-0315改1.14重画3个\0202.tif

图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)。

..\tu\0203.tif

图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确认。

..\tu\0204.tif

图2-4 连接建立示意图

..\tu\0205.tif

图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所示。

C:\Documents and Settings\Administrator\桌面\15-0315改1.14重画3个\0206.tif

图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所示的窗口。

..\tu\0207.tif

图2-7 添加约束的窗口

..\tu\0208.tif

图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右上角的图片 20图标,进入控件与代码关联的界面,依次给3个Text Field和1个Label添加4个属性,分别命名为hostText、portText、msgText、recvLabel,用于表示主机名、端口号、发送的信息、回复的信息。

(2)依次选中2个Button,以同样的方式,分别添加两个单击事件,命名为conn、send。

3.实现Socket聊天

按照实现Socket通信的3个步骤,模拟完成一个客户端与服务器端聊天的功能,详细步骤介绍如下。

图片 9

图2-9 给View添加约束

图片 10

图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所示。

图片 5

图2-11 程序运行的部分场景图

图片 4

图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所示。

..\tu\0213.tif

图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所示。

图片 1

图2-15 设计完成的界面

2.创建控件对象的关联

单击Xcode右上角的图片 4图标,进入控件与代码关联的界面,给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

图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所示。

图片 1

图2-18 “更改密码”窗口

图片 2

图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所示。

未命名:Users:sunshine:Desktop:无标题3.png

图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所示。

未命名:Users:sunshine:Desktop:无标题4.png

图2-23 提示错误信息

值得一提的是,提示图2-23所示的错误是正常的,若提示其他错误则表示不正常。

4.验证

配置工作完成之后,可以通过如下方式进行验证。打开Safari,在地址栏中输入“localhost”,单击“return”键,出现的页面如图2-24所示。

图2-24展示的页面是一个文件列表,这个目录对应着“/sunshine/Sites”路径。如果要在该页面中添加内容,只要在Finder中找到Sites文件夹,将要添加进去的文件拖曳到该文件夹目录下,单击图2-24中所示的“刷新”按钮即可。

图片 4

图2-24 配置成功的服务器

注意:

(1)每次启动计算机后,Apache服务器默认是不自动启动的,故需要打开终端,输入如下命名:

$ sudo apachectl -k start

(2)在使用终端进行操作之前,需要注意如下几个事项:

  • 关闭中文输入法;
  • 命令与参数之间需要有空格;
  • 修改系统文件一定记住输入sudo命令,否则会没有权限;
  • 目录一定要在/Users/sunshine(当前用户名)下。

2.3.2 XML文档结构

可扩展标记语言(Extensible Markup Language,XML),是一种用于标记电子文件使其具有结构性的标记语言,用于传输和存储数据。下面通过图2-25来描述XML文档的结构。

图片 3

图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所示。

..\tu\0226.tif

图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所示。

图片 5

图2-27 Safari打开的videos.xml文件

由图2-27可知,videos实质上是一个数组集合,其内部有多个video模型,每个video模型有多个属性,一个开始标签和一个结束标签将该属性对应的值括起来,解析XML文档按照从上至下的原则。接下来,通过一张图来分析该文件对应的解析思维,如图2-28所示。

..\tu\0228.tif

图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

图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

未命名:Users:sunshine:Desktop:无标题5.png

图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所示。

..\tu\0232.tif

图2-32 JSON对象的语法表

下面是一个JSON对象的例子:

{
  "name" : "Jay",
  "age" : 30,
  "sex" : ture
}

在上述代码中,JSON对象类似于字典类型,可读性更好。它是一个无序的集合,key必须使用双引号,值之间使用逗号隔开,value可以是数值、字符串、数组、对象等几种类型。

2.数组

数组表示为“[ ]”(中括号)括起来的内容,数据结构为“[value, value, value,…]”。它是值的有序集合,取值方式与其他语言一样,根据索引获取即可。JSON数组的语法表如图2-33所示。

..\tu\0233.tif

图2-33 JSON数组的语法表

下面是一个JSON数组的例子:

["it","cast","itcast"]

在上述代码中,每个条目之间同样使用逗号隔开,value可以是双引号括起来的字符串、数值、true、false、null、对象或者数组,而且这些结构可以嵌套,如图2-34所示。

..\tu\0234.tif

图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所示。

图片 1

图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所示。

..\tu\0236.tif

图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所示。

C:\Documents and Settings\Administrator\桌面\15-0315改1.20\0237a.tif

图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

图2-38 Safari打开的login.html

从图2-38中可以看出,上半部分是一个GET登录的页面,下半部分是一个POST登录的页面。只要输入姓名“zhangsan”,密码“zhang”,单击“提交”按钮,就会跳转到如图2-39所示的页面。

图片 3

图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所示。

图片 4

图2-40 设计完成的页面

3.创建控件对象的关联

(1)单击Xcode 6.1界面右上角的图片 47图标,进入控件与代码的关联界面。依次选中两个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所示。

图片 1

图2-41 用户名和密码错误

图片 2

图2-42 控制台输出错误信息

(2)再次在文本框中输入正确的用户名和密码,单击“登录”按钮后,控制台会输出相应的提示信息,如图2-43所示。

图片 4

图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所示。

C:\Documents and Settings\Administrator\桌面\Doc1.files\0244.tif

图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

图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

图2-46 添加完成的Home.Storyboard

(3)选中根节点的项目,在对应的编辑窗口中找到“Deployment Info”选项,该选项包含一个Main Interface,默认是Main.storyboard,删除后面文本框的内容。

3.创建控件对象的关联

(1)选中Login.storyboard,单击右上角的图片 56图标,进入控件与代码的关联界面。依次选中两个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所示。

图片 1

图2-48 安装插件的提示窗口

单击图2-48中的“安装”按钮,这时,浏览器右上角位置增加了一个“小虫子”图标,用于剖析Web内部的细节,而且能清晰地查看网站的源代码。

(2)准备资料

在Finder中打开Sites文件夹,将准备好的post文件夹复制到Sites文件夹,其中,upload.html是用于上传文件的脚本,upload.php是用于处理上传功能的脚本。另外,111.txt是用于测试的上传文件。刷新本地服务器,打开upload.html文件,如图2-49所示。

图片 3

图2-49 Firefox打开upload.html文件

值得一提的是,单击“浏览”按钮,选择任意一个来源的文件,单击“上传”按钮,文件默认会上传到abc文件夹。

(3)跟踪头信息

选中“小虫子”图标,单击“浏览”按钮,选取111.txt,单击“上传”按钮,浏览器底部的窗口展示了跟踪的全部信息,单击菜单“头信息”,它展示了响应头、请求头、上传的请求头的信息,如图2-50所示。

在请求头中,最为重要的就是Content-Type,它对应的值可分为两个部分,前半部分表示内容的类型,后半部分是边界boundary,用于分隔表单中不同部分的数据,后面的一串数字是由浏览器自动生成的,它的格式是不固定的,可以是任意字符。

(4)Post信息

单击菜单“Post”,它展示了发送的请求体的内容,源代码如图2-51所示。和头信息中的boundary部分进行对比不难发现,boundary的内容和请求体的数据部分前的字符串相比少了两个“--”。

C:\Documents and Settings\Administrator\桌面\Doc1.files\0250.tif

图2-50 头信息

C:\Documents and Settings\Administrator\桌面\Doc1.files\0251.tif

图2-51 Post菜单

由图2-51可知,上传的请求体有着严格的格式要求,任何一点错误都会导致上传失败。其中,Content-Disposition指定了表单元素的name属性和文件名称,Content-Type用于告知服务器上传文件的文件类型,一旦指定为application/octet-stream,就表示可以上传任意类型的文件。后面是请求体中最重要的数据部分,该部分就是二进制字符串。依据这几部分的顺序组成的源代码结构如图2-52所示。

图片 1

图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所示。

图片 1

图2-53 Firefox打开upload-m.html文件

从图2-53中可以看出,该脚本有两个“选取文件”的按钮,另外还有一个用于输入文字的文本框,作为上传的文本信息。

(2)跟踪头信息和Post信息

选中“小虫子”图标,单击第一个“选取文件”按钮,选取Finder中的111.txt文件,以同样的方式选取Finder中的222.txt文件,在文本框中输入“嘚瑟”文本内容,单击“上传”按钮,浏览器底部的窗口展示了跟踪请求的全部信息,Post菜单的信息与上传单个文件的信息稍有差异,示意图如图2-54所示。

图片 19

图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所示。

图片 8

图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所示。

图片 7

图2-56 上传成功的abc.png和abc.jpg

与此同时,控制台的输出如图2-57所示。

C:\Users\Administrator\Desktop\222.png

图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所示。

图片 1

图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所示。

..\tu\0259.tif

图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所示。

..\tu\0260.tif

图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

图2-61 设计好的程序界面

2.创建控件对象的关联

单击Xcode 6.1界面右上角的图片 63图标,进入控件与代码的关联界面。依次选中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所示。

图片 1

图2-63 GitHub的官方网站

图片 2

图2-64 搜索SDWebImage完成的界面

图片 3

图2-65 SDWebImage的详细文档

(4)单击图2-65中的“Download ZIP”按钮,下载源码到Finder中的“下载”文件夹,在该目录中打开刚刚下载的文件夹,这时会看到“SDWebImage”文件夹,双击打开该文件夹,如图2-66所示。

图片 4

图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所示。

..\tu\0267.tif

图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方法的区别。

图像说明文字

目录

推荐用户

同系列书

人邮微信
本地服务
教师服务
教师服务
读者服务
读者服务
返回顶部
返回顶部