iOS开发笔记 - 网络篇

Posted 骆昊

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了iOS开发笔记 - 网络篇相关的知识,希望对你有一定的参考价值。

计算机网络基础

??计算机网络是多台独立自主的计算机互联而成的系统的总称,最初建立计算机网络的目的是实现信息传递和资源共享。

??如果说计算机是第二次世界大战的产物,那么计算机网络则是美苏冷战的产物。20世纪60年代初期,美国国防部领导的ARPA提出研究一种崭新的、能够适应现代战争的、生存性很强的通信系统并藉此来应对苏联核攻击的威胁,这个决定促使了分组交换网的诞生,也奠定今天计算机网络的原型,这是计算机网络发展史上第一个里程碑式的事件。

??第二个里程碑式的事件是20世纪80年代初,国际标准化组织(ISO)提出了OSI/RM(开放系统互联参考模型),该模型定义了计算机网络的分层体系结构,虽然该模型并没有成为网络设备制造商遵循的国际标准,但用分层的思想解决复杂系统设计问题的做法已经深入人心。成为事实标准(de facto standard)的是TCP/IP模型,而TCP/IP协议簇(协议簇通常指彼此相关联的一系列协议的总称)也是构成今天的Internet的基石。不同于OSI/RM的七层结构,TCP/IP模型是一个四层模型,从上到下依次是应用层、传输层、网络层、物理链路层。值得一提的是,传输层可以使用两种不同的协议,一个是面向连接的传输控制协议(TCP),另一个是无连接的用户数据报协议(UDP),我们耳熟能详的的协议如HTTP、FTP、Telnet、POP3、DHCP、DNS、ICQ等都属于应用层的协议,它们要么构建在TCP之上,要么构建在UDP之上。

技术分享

??计算机网络发展史上第三个里程碑事件应该是浏览器的问世。20世纪90年代初,英国人Timothy John Berners-Lee发明了浏览器,浏览器通过超文本传输协议(HTTP)跟服务器交换超文本数据,通过图形用户界面显示从服务器获得的超文本数据,这一切都让使用Internet变得无比简单,于是计算机网络的用户数量开始爆炸式的增长。

基于HTTP协议联网

??在ios开发中,如果应用程序需要的数据不在本地,而是通过网络获取的文字、图片、音视频等资源,那么我们的应用程序就需要联网,对于这种场景通常可以直接使用HTTP(Hyper-Text Transfer Protocol)向提供资源的服务器发出请求即可。HTTP协议对于很多人来说都不陌生,我们使用浏览器访问Web服务器的时候使用的基本上都是使用HTTP协议(有些服务器需要使用HTTPS,它是在HTTP下层添加SSL[Secure Socket Layer],用于安全的传输HTTP协议数据)。目前越来越多的应用已经从浏览器延伸到移动客户端,但是服务器端并不需要做出任何改变,iOS和android的应用程序也可以通过HTTP协议和服务器通信。

??我们先来解释一下什么是协议以及HTTP到底是一个怎样的协议。我们将任何可发送或接收信息的硬件或程序称之为实体,而协议则是控制两个对等实体进行通信的规则的集合。简单的说,协议就是通信双方必须遵循的对话的标准和规范。HTTP是构建在TCP之上的协议,之所以选择TCP作为底层传输协议是因为TCP除了可以保证可靠通信之外,还具备流量控制和拥塞控制的能力,如果这一点不能理解也不要紧,我么只需要知道HTTP需要可靠的传输层协议的支持就够了。

??HTTP有两种类型的报文:请求报文和响应报文。请求报文和响应报文都是由三个部分组成的。我们可以用抓包工具截取请求和响应报文来看看它们的结构。

技术分享

??请求报文是由请求行、请求头和消息体构成的。请求行包含了命令(通常是GET或POST)、资源和协议版本;请求头是键值对映射形式的和请求相关的信息,如客户端使用的语言、使用的浏览器等信息;消息体是客户端发给服务器的数据;在请求头和消息体之间有一个空行。

技术分享

??响应报文是由响应行、响应头和消息体构成的。响应行包含了协议版本和状态码;响应头是键值对形式的和响应相关的信息,如服务器的软件版本、时间日期、缓存策略、响应内容类型等信息;消息体是服务器发给客户端的数据;在响应头和消息体之间有一个空行。

抓包工具

  • Charles

技术分享

??Charles是一个HTTP代理服务器,HTTP监视器,反转代理服务器,它允许一个开发者查看所有连接互联网的HTTP通信。很多iOS开发者都选择Charles作为抓包工具来获取和测试网络接口。通过下图所示的菜单项可以将Charles设置为Mac系统的HTTP代理,所有的HTTP数据都会被Charles截获。

技术分享

??当然,还可以将Charles设置为手机的代理,只要让安装了Charles的Mac系统和手机使用相同的网络,再将手机无线局域网的代理服务器设置为Mac系统的IP地址即可,这样手机上的HTTP数据也会被截获。

技术分享

技术分享

  • Wireshark

    ??Wireshark(原名Ethereal,1998年由美国Gerald Combs首创研发,由世界各国100多位网络专家和软件人员共同参与此软件的升级完善和维护,2006年5月更名为Wireshark)是一个非常专业的网络数据包截取和分析软件,它直接截获经过网卡的数据,并尽可能显示出最为详细的数据包信息,是协议分析的利器。Wireshark比Charles更底层更专业,但是如果只做HTTP数据分析,Charles用起来还是非常简单方便的。

技术分享

相关API

  • NSURL

    ??NSURL是代表统一资源定位符(Universal Resource Locator,URL)的类。URL是互联网上标准资源的地址,互联网上的每个资源都有一个唯一的与之对应的URL。

    ??URL的格式如下所示:

协议://域名或IP地址:端口号/路径/资源

??下面是百度logo的URL:

http://www.baidu.com:80/img/bd_logo1.png

说明:端口号是对IP地址的扩展。例如我们的服务器只有一个IP地址,但是我们可以在这台服务器上开设多个服务,如Web服务、邮件服务和数据库服务,当服务器收到一个请求时会根据端口号来区分到底请求的是Web服务还是邮件服务,或者是数据库服务。我们在浏览器中输入URL的时候通常都会省略端口号,因为HTTP协议默认使用80端口,也就是说除非你访问的Web服务器没有使用80端口,你才需要输入相应的端口号。

??下面的代码演示了如何在iOS应用中通过URL获取网络数据。

??Objective-C代码:

#import "ViewController.h"

#define CENTER_X CGRectGetWidth(self.view.bounds) / 2
#define CENTER_Y CGRectGetHeight(self.view.bounds) / 2

@interface ViewController ()

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:
        CGRectMake(0, 0, 320, 160)];
    imageView.center = CGPointMake(CENTER_X, CENTER_Y);
    [self.view addSubview:imageView];

    NSURL *url = [NSURL URLWithString:@"http://www.baidu.com/img/bd_logo1.png"];
    NSData *data = [NSData dataWithContentsOfURL:url];
    imageView.image = [UIImage imageWithData:data];
}

@end

??Swift代码:

import UIKit

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        let imageView = UIImageView(frame: CGRectMake(0, 0, 320, 160))
        imageView.center = CGPointMake(self.view.bounds.size.width / 2, 
            self.view.bounds.size.height / 2)
        self.view.addSubview(imageView)

        guard let url = NSURL(string: "http://www.baidu.com/img/bd_logo1.png") 
            else { return }
        guard let data = NSData(contentsOfURL: url) else  { return }
        imageView.image = UIImage(data: data)
    }

}

提示:iOS 9出于安全方面的考虑,不允许使用非安全的HTTP协议联网,如果要用需要修改项目的Info.plist文件,添加“App Transport Security Settings”键,其类型是Dictionary;在“App Transport Security Settings”下添加一个子元素,键是“Allow Arbitrary Loads”,类型是Boolean,将其值设置为YES。

  • NSURLRequest / NSMutableURLRequest

    ??NSURLRequest / NSMutableURLRequest代表了客户端向服务器发送的HTTP请求。通过请求对象可以设置请求的方法、请求头、缓存策略、超时时间、消息体等。

  • NSURLResponse

    ??NSURLResponse代表了服务器发送给客户端的HTTP响应。

  • NSURLConnection

    ??在iOS 7以前,基于HTTP协议联网的操作最终都要由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 *connectionError))handler

提示:同步请求是阻塞式请求,这就意味着同步请求的方法在返回数据之前会一直阻塞;异步请求是非阻塞式请求,当服务器返回数据时可以回调的方式对数据进行处理。如果明白这一点,就很容易理解为什么上面的同步请求方法会返回NSData指针,而异步请求方法没有返回值但有一个Block类型的参数(Block最适合用来书写回调代码)。

  • NSURLSession

    ??2013的WWDC,苹果推出了NSURLConnection的继任者NSURLSession。与NSURLConnection相比,NSURLsession最直接的改进就是可以配置每个会话(session)的缓存、协议、cookie以及证书策略(credential policy)等,而且你可以跨程序共享这些信息。每个NSURLSession对象都由一个NSURLSessionConfiguration对象来进行初始化,NSURLSessionConfiguration对象代表了会话的配置以及一些用来增强移动设备上性能的新选项。

    ??可以通过NSURLSession创建NSURLSessionTask(会话任务),会话任务有三个子类对应不同的场景,分别是:NSURLSessionDataTask(获取数据的任务)、NSURLSessionDownloadTask(下载任务)和NSURLSessionUploadTask(上传任务),我们通过HTTP协议可以完成的操作都属于这三类任务之一。NSURLSessionTask主要有三个方法,分别是:resume(恢复任务)、suspend(挂起任务)和cancel(取消任务)。

  • NSURLSessionConfiguration

    ??如前面所述,NSURLSessionConfiguration代表了会话的配置,该类的三个创建对象的类方法很好的诠释了NSURLSession类设计时所考虑的不同的使用场景。

// 返回一个标准的配置,标准配置会使用默认的缓存策略、超时时间等
+ (NSURLSessionConfiguration *)defaultSessionConfiguration;
// 返回一个临时性的配置,这个配置中不会对缓存,Cookie和证书进行持久化存储
// 对于实现无痕浏览这种功能来说这种配置是非常理想的
+ (NSURLSessionConfiguration *)ephemeralSessionConfiguration;
// 返回一个后台配置
// 后台会话不同于普通的会话,它甚至可以在应用程序挂起,退出或者崩溃的情况下运行上传和下载任务
// 初始化时指定的标识符,被用于向任何可能在进程外恢复后台传输的守护进程(daemon)提供上下文
+ (NSURLSessionConfiguration *)backgroundSessionConfigurationWithIdentifier:
      (NSString *)identifier

数据解析

??通过HTTP从服务器获得的数据通常都是JSON格式或XML格式的,下面对这两种数据格式做一个简单的介绍。

  • XML

    ??XML全称可扩展标记语言(eXtensible Markup Language),被设计用来传输和存储数据。在JSON被广泛使用之前,XML是异构系统之间交换数据的事实标准,它是一种具有自我描述能力的传输数据的标记语言,如下所示。

<?xml version="1.0" encoding="ISO-8859-1"?>
<note>
    <to>Tove</to>
    <from>Jani</from>
    <heading>Reminder</heading>
    <body>Don‘t forget me this weekend!</body>
</note>

??XML文档形成一种树结构,它必须包含根元素。该元素是所有其他元素的父元素。这棵树从根部开始,并扩展到树的最底端,如下图所示。

技术分享

<bookstore>
    <book category="COOKING">
        <title lang="en">Everyday Italian</title>
        <author>Giada De Laurentiis</author>
        <year>2005</year>
        <price>30.00</price>
    </book>
    <book category="CHILDREN">
        <title lang="en">Harry Potter</title>
        <author>J K. Rowling</author>
        <year>2005</year>
        <price>29.99</price>
    </book>
    <book category="WEB">
        <title lang="en">Learning XML</title>
        <author>Erik T. Ray</author>
        <year>2003</year>
        <price>39.95</price>
    </book>
</bookstore>

??XML的语法规则跟其他标签语言(如html)基本一致,不过需要注意以下几条:

1. 所有的XML元素都必须有一个关闭标签
2. XML标签对大小写敏感
3. XML必须正确嵌套
4. XML文档必须有根元素
5. XML属性值必须加引号
6. XML中的特殊字符要使用实体引用
7. XML中的注释是<!-- -->

??在XML文档中查找信息可以使用XPath表达式,我们来看一个例子。

<?xml version="1.0" encoding="ISO-8859-1"?>
<bookstore>
    <book>
        <title lang="eng">Harry Potter</title>
        <price>29.99</price>
    </book>
    <book>
        <title lang="eng">Learning XML</title>
        <price>39.95</price>
    </book>
</bookstore>

XPath语法表

表达式 描述 例子 结果
nodename 选取此节点的所有子节点 bookstore 选取bookstore的所有子节点
/ 从根节点选取 /bookstore 选取根元素bookstore
// 从匹配选择的当前节点选择文档中的节点,而不考虑它们的位置 //book 选取所有 book 子元素,而不管它们在文档中的位置
. 选取当前节点
.. 选取当前节点的父节点
@ 选取属性 //@lang 选取名为lang的所有属性

XPath的例子

路径表达式 结果
/bookstore/book[1] 选取属于bookstore子元素的第一个book元素。
/bookstore/book[last()] 选取属于bookstore子元素的最后一个book元素。
/bookstore/book[last()-1] 选取属于bookstore子元素的倒数第二个book元素。
/bookstore/book[position()<3] 选取最前面的两个属于bookstore元素的子元素的 book元素。
//title[@lang] 选取所有拥有名为lang的属性的title元素。
//title[@lang=’eng’] 选取所有title元素,且这些元素拥有值为eng的lang属性。
/bookstore/book[price>35.00] 选取bookstore元素的所有book元素,且其中的 price元素的值须大于35.00。
/bookstore/book[price>35.00]/title 选取bookstore元素中的book元素的所有 title元素,且其中的price元素的值须大于35.00。

提示:如果对上面很多概念不理解或者想对XML有一个更全面的了解,建议访问RUNOOB.COM获得更多的信息。

??解析XML数据主要有两种方式:SAX和DOM。SAX解析属于事件驱动型的顺序解析,即从上至下解析XML文件,遇到标记、属性、注释、内容等都会引发事件回调,苹果原生的NSXMLParser就属于这种类型的解析,其优点在于速度快,内存占用少,但是操作比较复杂。DOM是文档对象模型的缩写,顾名思义就是将整个XML文档视为一个对象,DOM解析的原理是先根据XML文档的内容在内存中建立树结构,再对树结构进行解析,这种方式显然需要更多的内存,但操作简单且对XPath查询提供了很好的支持。 第三方库基本上都是用DOM解析,常用的有:GDataXML,KissXMLRaptureXMLXMLDictionary

??下面的代码演示了如何使用KissXML解析开源中国编号为44393的文章的相关链接。

Objective-C代码:

#import "ViewController.h"
#import "CDDetailViewController.h"
#import "CDRelativeNews.h"
#import "DDXML.h"

@interface ViewController () <UITableViewDataSource, UITableViewDelegate>

@end

@implementation ViewController {
    UITableView *myTableView;
    // iOS 9开始支持泛型容器(有类型限定的数组、字典等)
    // 可以在Xcode 7中使用这项新的语言特性
    NSMutableArray<CDRelativeNews *> *dataArray;
}

- (void)viewDidLoad {
    [super viewDidLoad];

    self.title = @"相关新闻链接";

    myTableView = [[UITableView alloc] initWithFrame:self.view.bounds style:UITableViewStylePlain];
    myTableView.dataSource = self;
    myTableView.delegate = self;
    [self.view addSubview:myTableView];

    [self loadDataModel];
}

- (void)loadDataModel {
    if (!dataArray) {
        dataArray = [NSMutableArray array];
    }

    // 创建统一资源定位符对象
    NSURL *url = [NSURL URLWithString:
        @"http://www.oschina.net/action/api/news_detail?id=44393"];
    // 通过统一资源定位符从服务器获得XML数据
    NSData *data = [NSData dataWithContentsOfURL:url];
    // 使用NSData对象创建XML文档对象 文档对象是将XML在内存中组织成一棵树
    DDXMLDocument *doc = [[DDXMLDocument alloc] 
        initWithData:data options:0 error:nil];
    // 使用XPath语法从文档对象模型中查找指定节点
    NSArray *array = [doc nodesForXPath:@"//relative" error:nil];
    // 循环取出节点并对节点下的子节点进行进一步解析
    for (DDXMLNode *node in array) {
        CDRelativeNews *model = [[CDRelativeNews alloc] init];
        // 取出当前节点的子节点并获取其对应的值
        model.title = [node.children[0] stringValue];
        model.url = [node.children[1] stringValue];
        // 将模型对象添加到数组中
        [dataArray addObject:model];
    }
    // 刷新表格视图
    [myTableView reloadData];
}

- (NSInteger) tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section {
    return dataArray.count;
}

- (UITableViewCell *) tableView:(UITableView *)tableView 
        cellForRowAtIndexPath:(NSIndexPath *)indexPath {
    UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:@"CELL"];
    if (!cell) {
        cell = [[UITableViewCell alloc] 
            initWithStyle:UITableViewCellStyleDefault reuseIdentifier:@"CELL"];
    }

    cell.textLabel.text = dataArray[indexPath.row].title;

    return cell;
}

- (void) tableView:(UITableView *)tableView 
        didSelectRowAtIndexPath:(NSIndexPath *)indexPath {
    CDRelativeNews *model = dataArray[indexPath.row];
    CDDetailViewController *detailVC = [[CDDetailViewController alloc] init];
    detailVC.urlStr = model.url;
    [self.navigationController pushViewController:detailVC animated:YES];
}

@end

??用这个例子顺便介绍一下如何在Swift中使用Objective-C实现两种语言的混编。首先还是向项目中添加KissXML第三方库,这个第三方库用是Objective-C书写的。在下面的例子中,我们创建了一个名为“bridge.h”的头文件,并在项目的“Build Settings”中找到“Objective-C Bridging Header”选项,将“bridge.h”头文件的路径添到此处。

技术分享

#ifndef bridge_h
#define bridge_h

#import "DDXML.h"

#endif /* bridge_h */
import UIKit

class ViewController: UIViewController, 
      UITableViewDataSource, UITableViewDelegate {

    var myTableView: UITableView?
    var dataArray = [RelativeNews]()

    override func viewDidLoad() {
        super.viewDidLoad()

        self.title = "相关新闻链接"

        myTableView = UITableView(frame: self.view.bounds, style: .Plain)
        myTableView!.dataSource = self
        myTableView!.delegate = self
        self.view.addSubview(myTableView!)

        self.loadDataModel()
    }

    func loadDataModel() {
        guard let url = NSURL(string: 
            "http://www.oschina.net/action/api/news_detail?id=44393") 
            else { return }
        guard let data = NSData(contentsOfURL: url) else { return }
        do {
            // 用通过URL获取的XML数据构造文档对象模型
            // 然后使用XPath语法全文查找relative节点
            for node in try DDXMLDocument(data: data, options: 0)
                    .nodesForXPath("//relative") {
                // 将数组中的元素类型转换为DDXMLNode
                if let relative = node as? DDXMLNode {
                    // 用children方法取DDXMLNode对象的子节点的数组
                    if let children = relative.children() as? [DDXMLNode] {
                        let model = RelativeNews()
                        model.title = children[0].stringValue()
                        model.url = children[1].stringValue()
                        dataArray.append(model)
                    }
                }
            }
            myTableView!.reloadData()
        }
        catch  {
            print("Error occured while handling XML")
        }
    }

    func tableView(tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return dataArray.count
    }

    func tableView(tableView: UITableView, cellForRowAtIndexPath indexPath: NSIndexPath) -> UITableViewCell {
        var cell = tableView.dequeueReusableCellWithIdentifier("CELL")
        if cell == nil {
            cell = UITableViewCell(style: .Default, reuseIdentifier: "CELL")
        }
        let model = dataArray[indexPath.row]
        cell?.textLabel?.text = model.title
        return cell!
    }

    func tableView(tableView: UITableView, didSelectRowAtIndexPath indexPath: NSIndexPath) {
        let model = dataArray[indexPath.row]
        let detailVC = DetailViewController()
        detailVC.urlStr = model.url
        self.navigationController?.pushViewController(detailVC, animated: true)
    }
}

说明:上面的代码中使用了Swift 2.x的异常处理机制,如果不了解可以看看简书上的这篇文章《Swift 2.0异常处理》

  • JSON

    ??JSON全称javascript对象表达式(JavaScript Object Notation),是目前最流行的存储和交换文本信息的语法,和XML相比,它更小、更快,更易解析,是一种轻量级的文本数据交换格式。

    ??JSON的语法规则可以简单的总结成以下几条:1. 数据在名/值对中;2. 数据由逗号分隔;3. 花括号保存对象;4. 方括号保存数组。

    ??例如:

{
  "name" : "骆昊",
  "age" : 35,
  "gender" : true,
  "car" : {
    "brand" : "Touareg",
    "maxSpeed" : 240
  },
  "favorites" : [
    "阅读",
    "旅游",
    "象棋"
  ],
  "mistress" : null
}

??JSON中的值可以是:

- 数字(整数或浮点数)
- 字符串(在双引号中)
- 逻辑值(true 或 false)
- 数组(在方括号中)
- 对象(在花括号中)
- null

??不难看出,JSON用键值对的方式描述了JavaScript中的对象,它的形态跟Objective-C的NSDictionary以及Swift中的Dictionary类型是完全一致的,可以通过NSJSONSerialization类的两个类方法实现JSON数据和字典或数组之间的相互转换。

// 将数据转换成对象(通常是数组或字典)
+ (id)JSONObjectWithData:(NSData *)data options:(NSJSONReadingOptions)opt error:(NSError **)error;
// 将数组或字典装换成JSON数据
+ (NSData *)dataWithJSONObject:(id)obj options:(NSJSONWritingOptions)opt error:(NSError **)error

??通过服务器获得JSON数据后,最终需要将它转换成我们程序中的对象。事实上,将JSON转换成模型对象的操作在开发网络应用中是很常见的,我们可以使用KVC(Key-Value Coding)的方式将一个字典赋值给一个对象的属性,代码如下所示。

说明:KVC通常翻译为键值编码,它允许开发者通过名字访问对象属性,而无需调用明确的存取方法,这样就可以实现在运行时而不是在编译时确定属性的绑定。这种间接访问能让代码变得更灵活和更具复用性。

Objective-C代码:

#import <Foundation/Foundation.h>

@interface CDPerson : NSObject

@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) NSUInteger age;
@property (nonatomic, copy) NSArray<NSString *> *friends;

@end
#import "CDPerson.h"

@implementation CDPerson

- (void)setValue:(id)value forUndefinedKey:(NSString *)key {
}

- (NSString *) description {
    NSMutableString *mStr = [NSMutableString string];
    for (NSString *friendsName in _friends) {
        [mStr appendString:friendsName];
        [mStr appendString:@" "];
    }
    return [NSString stringWithFormat:@"姓名: %@\n年龄: %ld\n朋友: %@", 
        _name, _age, mStr];
}

@end
#import <Foundation/Foundation.h>
#import "CDPerson.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSDictionary *dict = @{ @"name": @"骆昊", @"age":@(35), 
            @"friends":@[@"金庸", @"古龙", @"黄易"] };
        CDPerson *person = [[CDPerson alloc] init];
        [person setValuesForKeysWithDictionary:dict];

        NSLog(@"%@", person);
    }
    return 0;
}

Swift代码:

import Foundation

class Person: NSObject {
    var name: String = ""
    var age: UInt = 0
    var friends: [String] = []

    override func setValue(value: AnyObject?, forUndefinedKey key: String) {
    }

    override var description: String {
        get {
            var mStr = String()
            for friendName in friends {
                mStr.appendContentsOf("\(friendName) ")
            }
            return "姓名: \(name)\n年龄: \(age)\n朋友: \(mStr)"
        }
    }
}
var dict = [ "name": "骆昊", "age": 35, "friends": ["金庸", "古龙", "黄易"] ]
var person = Person()
person.setValuesForKeysWithDictionary(dict)
print(person.description)

??对于对象中关联了其他对象或者对象的属性跟字典中的键不完全匹配的场景,KVC就显得不那么方便了,但是已经有很多优秀的第三方库帮助我们实现了JSON和模型对象的双向转换,下面我们介绍这些第三库中非常有代表性的JSONModelYYModel

说明:JSONModel和YYModel都是用Objective-C开发的,下面我们也直接用Objective-C代码为大家介绍这些东西,不再提供双语版的讲解。

  • JSONModel
#import <Foundation/Foundation.h>
#import "JSONModel.h"

/**产品*/
@interface CDProduct: JSONModel

@property (nonatomic, assign) int id;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) double price;
@property (nonatomic, assign) int amount;

@end
#import "CDProduct.h"

@implementation CDProduct

- (NSString *)description {
    return [NSString stringWithFormat:@"商品编号: %d\n商品名称: %@\n商品价格: %.2f\n商品数量: %d", 
        _id, _name, _price, _amount];
}

@end
#import <Foundation/Foundation.h>
#import "JSONModel.h"

// 通过协议来限定数组中的元素类型
@protocol CDProduct <NSObject>
@end

/**订单*/
@interface CDOrder: JSONModel

@property (nonatomic, assign) int orderId;
@property (nonatomic, assign) double totalPrice;
@property (nonatomic, strong) NSArray<CDProduct> *products;

@end
#import "CDOrder.h"

@implementation CDOrder

// 该方法提供字典(JSON)中的键和对象属性之间的映射关系
+ (JSONKeyMapper *)keyMapper {
    return [[JSONKeyMapper alloc] initWithDictionary:@{
        @"order_id": @"orderId",
        @"order_price": @"totalPrice"
    }];
}

- (NSString *)description {
    return [NSString stringWithFormat:@"订单号: %d 总价: %.2f\n", 
        _orderId, _totalPrice];
}

@end
#import <Foundation/Foundation.h>
#import "CDOrder.h"
#import "CDProduct.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSDictionary *dict = @{
            @"order_id": @(104),
            @"order_price": @(108.85),
            @"products" : @[
                @{
                    @"id": @"123",
                    @"name": @"Product #1",
                    @"price": @(12.95),
                    @"amount": @(2)
                },
                @{
                    @"id": @"137",
                    @"name": @"Product #2",
                    @"price": @(82.95),
                    @"amount": @(1)
                }
            ]
        };

        CDOrder *model = [[CDOrder alloc] initWithDictionary:dict error:nil];
        NSLog(@"%@", model);
        for (CDProduct *product in model.products) {
            NSLog(@"%@", product);
        }
    }
    return 0;
}

??从上面的例子不难看出,JSONModel是有侵入性的,因为你的模型类必须继承JSONModel,这些对代码的复用和迁移多多少少会产生影响。基于这样的原因,更多的开发者在实现JSON和模型对象转换时更喜欢选择非侵入式的MJExtension,这里我们就不介绍MJExtension,其实它已经做得非常好了,但是当YYModel横空出世的时候,MJExtension瞬间就成了浮云。YYModel和MJExtension一样是没有侵入性的,你的模型类不要跟第三方库耦合在一起,而且YYModel提供了比MJExtension更优雅的配置方式,更强大的自动类型转化能力,当然在性能上YYModel也更优,而且跟MJExtension不在一个数量级上。我们还是用上面的例子来演示如何使用YYModel。

  • YYModel
#import <Foundation/Foundation.h>

@class CDProduct;

/**订单*/
@interface CDOrder: NSObject

@property (nonatomic, assign) int orderId;
@property (nonatomic, assign) double totalPrice;
@property (nonatomic, strong) NSArray<CDProduct *> *products;

@end
#import "CDOrder.h"

@implementation CDOrder
// 该方法提供属性名和字典(JSON)中的键的映射关系
+ (NSDictionary *) modelCustomPropertyMapper {
    return @{
        @"orderId": @"order_id",
        @"totalPrice": @"order_price"
    };
}

// 该方法提供容器属性中对象的类型
+ (NSDictionary *) modelContainerPropertyGenericClass {
    return @{
        @"products": NSClassFromString(@"CDProduct")
    };
}

- (NSString *)description {
    return [NSString stringWithFormat:@"订单号: %d 总价: %.2f\n",
            _orderId, _totalPrice];
}

@end
#import <Foundation/Foundation.h>

/**产品*/
@interface CDProduct: NSObject

@property (nonatomic, assign) int id;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) double price;
@property (nonatomic, assign) int amount;

@end
#import "CDProduct.h"

@implementation CDProduct

- (NSString *)description {
    return [NSString stringWithFormat:@"商品编号: %d\n商品名称: %@\n商品价格: %.2f\n商品数量: %d", 
        _id, _name, _price, _amount];
}

@end
#import <Foundation/Foundation.h>
#import "CDOrder.h"
#import "YYModel.h"

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSDictionary *dict = @{
           @"order_id": @(104),
           @"order_price": @(108.85),
           @"products": @[
               @{
                   @"id": @"123",
                   @"name": @"Product #1",
                   @"price": @(12.95),
                   @"amount": @(2)
                },
               @{
                   @"id": @"137",
                   @"name": @"Product #2",
                   @"price": @(82.95),
                   @"amount": @(1)
                }
            ]
        };

        CDOrder *order = [CDOrder yy_modelWithDictionary:dict];
        NSLog(@"%@", order);
        for (id product in order.products) {
            NSLog(@"%@", product);
        }
    }
    return 0;
}

第三方库

??如果要基于HTTP协议开发联网的iOS应用程序,可以使用优秀的第三方库来提升开发效率减少重复劳动,这些优秀的第三方库中的佼佼者当属AFNetworking

  • AFNetworking

    ??AFNetworking是基于URL加载系统的网络框架,很多App都使用它实现联网功能,它的2.x版本封装了基于NSURLConnection和NSURLSession的两套API。目前最新

以上是关于iOS开发笔记 - 网络篇的主要内容,如果未能解决你的问题,请参考以下文章

IOS开发之网络篇第一章:数据交换格式

iOS开发网络篇 —— OC加载HTML代码

iOS开发网络篇—文件的上传

iOS开发笔记--敏捷开发之Scrum扫盲篇

iOS开发网络篇—使用ASI框架进行文件下载

iOS开发网络篇之Web Service和XML数据解析