ios 网络请求封装放在哪

本帖子已过去太久远了,不再提供回复功能。iOS开发网络篇—发送GET和POST请求(使用NSURLSession)
1 .该文主要介绍如何使用NSURLSession来发送GET请求和POST请求
2 .本文将不再讲解NSURLConnection的使用,如有需要了解NSURLConnection如何发送请求。
3 . 本文示例代码发送的请求均为http请求,已经对info.plist文件进行配置。
如何配置请参考:
4 .本文示例代码,可以在下面的地址获取:
点击链接:
一、简单说明
  在iOS9.0之后,以前使用的NSURLConnection过期,苹果推荐使用NSURLSession来替换NSURLConnection完成网路请求相关操作。
  NSURLSession的使用非常简单,先根据会话对象创建一个请求Task,然后执行该Task即可。
  NSURLSessionTask本身是一个抽象类,在使用的时候,通常是根据具体的需求使用它的几个子类。关系如下:
二、发送GET请求
  使用NSURLSession发送GET请求的方法和NSURLConnection类似,整个过程如下:
1 .确定请求路径(一般由公司的后台开发人员以接口文档的方式提供),GET请求参数直接跟在URL后面
2 .创建请求对象(默认包含了请求头和请求方法【GET】),此步骤可以省略
3 .创建会话对象(NSURLSession)
4 .根据会话对象创建请求任务(NSURLSessionDataTask)
5 .执行Task
6 .当得到服务器返回的响应后,解析数据(XML|JSON|HTTP)
示例代码一:
-(void)get1
//对请求路径的说明
//https://120.25.226.186:32812/login?username=520it&pwd=520&type=JSON
//协议头+主机地址+接口名称+?+参数1&参数2&参数3
//协议头(https://)+主机地址(120.25.226.186:32812)+接口名称(login)+?+参数1(username=520it)&参数2(pwd=520)&参数3(type=JSON)
//GET请求,直接把请求参数跟在URL的后面以?隔开,多个参数之间以&符号拼接
//1.确定请求路径
NSURL *url = [NSURL URLWithString:@&https://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON&];
//2.创建请求对象
//请求对象内部默认已经包含了请求头和请求方法(GET)
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.获得会话对象
NSURLSession *session = [NSURLSession sharedSession];
//4.根据会话对象创建一个Task(发送请求)
第一个参数:请求对象
第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
data:响应体信息(期望的数据)
response:响应头信息,主要是对服务器端的描述
error:错误信息,如果请求失败,则error有值
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error == nil) {
//6.解析服务器返回的数据
//说明:(此处返回的数据是JSON格式的,因此使用NSJSONSerialization进行反序列化处理)
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@&%@&,dict);
//5.执行任务
[dataTask resume];
示例代码二:
-(void)get2
//1.确定请求路径
NSURL *url = [NSURL
URLWithString:@&https://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON&];
//2.获得会话对象
NSURLSession *session = [NSURLSession sharedSession];
//3.根据会话对象创建一个Task(发送请求)
第一个参数:请求路径
第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
data:响应体信息(期望的数据)
response:响应头信息,主要是对服务器端的描述
error:错误信息,如果请求失败,则error有值
1)该方法内部会自动将请求路径包装成一个请求对象,该请求对象默认包含了请求头信息和请求方法(GET)
2)如果要发送的是POST请求,则不能使用该方法
NSURLSessionDataTask *dataTask = [session dataTaskWithURL:url completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//5.解析数据
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@&%@&,dict);
//4.执行任务
[dataTask resume];
执行结果:
此处打印的值是一个字典,字典中success这个key对应的value打印出来为Unicode编码的,如果想输出中文,可以为NSDictionary提供一个分类,重写系统中的方法。
#import &NSDictionary+Log.h&
@implementation NSDictionary (Log)
-(NSString *)descriptionWithLocale:(id)locale indent:(NSUInteger)level
//初始化可变字符串
NSMutableString *string = [NSMutableString string];
//拼接开头[
[string appendString:@&[&];
//拼接字典中所有的键值对
[self enumerateKeysAndObjectsUsingBlock:^(id
_Nonnull key, id
_Nonnull obj, BOOL * _Nonnull stop) {
[string appendFormat:@&%@:&,key];
[string appendFormat:@&%@&,obj];
//拼接结尾]
[string appendString:@&]&];
执行结果:  
  vcD4NCjxoMyBpZD0="三发送post请求">三、发送POST请求
 使用NSURLSession发送POST请求的方法和NSURLConnection类似,整个过程如下:
1 .确定请求路径(一般由公司的后台开发人员以接口文档的方式提供)
2 . 创建可变的请求对象(因为需要修改),此步骤不可以省略
3 . 修改请求方法为POST
4 . 设置请求体,把参数转换为二进制数据并设置请求体
5 . 创建会话对象(NSURLSession)
6 . 根据会话对象创建请求任务(NSURLSessionDataTask)
7 . 执行Task
8 . 当得到服务器返回的响应后,解析数据(XML|JSON|HTTP)
示例代码:
-(void)post
//对请求路径的说明
//https://120.25.226.186:32812/login
//协议头+主机地址+接口名称
//协议头(https://)+主机地址(120.25.226.186:32812)+接口名称(login)
//POST请求需要修改请求方法为POST,并把参数转换为二进制数据设置为请求体
//1.创建会话对象
NSURLSession *session = [NSURLSession sharedSession];
//2.根据会话对象创建task
NSURL *url = [NSURL URLWithString:@&https://120.25.226.186:32812/login&];
//3.创建可变的请求对象
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
//4.修改请求方法为POST
request.HTTPMethod = @&POST&;
//5.设置请求体
request.HTTPBody = [@&username=520it&pwd=520it&type=JSON& dataUsingEncoding:NSUTF8StringEncoding];
//6.根据会话对象创建一个Task(发送请求)
第一个参数:请求对象
第二个参数:completionHandler回调(请求完成【成功|失败】的回调)
data:响应体信息(期望的数据)
response:响应头信息,主要是对服务器端的描述
error:错误信息,如果请求失败,则error有值
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//8.解析数据
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:nil];
NSLog(@&%@&,dict);
//7.执行任务
[dataTask resume];
*四、NSURLSession代理方法简单介绍
  有的时候,我们可能需要监听网络请求的过程(如下载文件需监听文件下载进度),那么就需要用到代理方法。
  接下来通过代码简单说明NSURLSession中普通网络请求会涉及代理方法的使用
#import &ViewController.h&
@interface ViewController ()
@property (nonatomic, strong) NSMutableData *responseD
@implementation ViewController
-(NSMutableData *)responseData
if (_responseData == nil) {
_responseData = [NSMutableData data];
return _responseD
//当点击控制器View的时候会调用该方法
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
[self delegateTest];
//发送请求,代理方法
-(void)delegateTest
//1.确定请求路径
NSURL *url = [NSURL URLWithString:@&https://120.25.226.186:32812/login?username=520it&pwd=520it&type=JSON&];
//2.创建请求对象
//请求对象内部默认已经包含了请求头和请求方法(GET)
NSURLRequest *request = [NSURLRequest requestWithURL:url];
//3.获得会话对象,并设置代理
第一个参数:会话对象的配置信息defaultSessionConfiguration 表示默认配置
第二个参数:谁成为代理,此处为控制器本身即self
第三个参数:队列,该队列决定代理方法在哪个线程中调用,可以传主队列|非主队列
[NSOperationQueue mainQueue]
代理方法在主线程中调用
[[NSOperationQueue alloc]init] 非主队列: 代理方法在子线程中调用
NSURLSession *session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:[NSOperationQueue mainQueue]];
//4.根据会话对象创建一个Task(发送请求)
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:request];
//5.执行任务
[dataTask resume];
//1.接收到服务器响应的时候调用该方法
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
//在该方法中可以得到响应头信息,即response
NSLog(@&didReceiveResponse--%@&,[NSThread currentThread]);
//注意:需要使用completionHandler回调告诉应该如何处理服务器返回的数据
//默认是取消的
NSURLSessionResponseCancel = 0,
默认的处理方式,取消
NSURLSessionResponseAllow = 1,
接收服务器返回的数据
NSURLSessionResponseBecomeDownload = 2,变成一个下载请求
NSURLSessionResponseBecomeStream
变成一个流
completionHandler(NSURLSessionResponseAllow);
//2.接收到服务器返回数据的时候会调用该方法,如果数据较大那么该方法可能会调用多次
-(void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveData:(NSData *)data
NSLog(@&didReceiveData--%@&,[NSThread currentThread]);
//拼接服务器返回的数据
[self.responseData appendData:data];
//3.当请求完成(成功|失败)的时候会调用该方法,如果请求失败,则error有值
-(void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error
NSLog(@&didCompleteWithError--%@&,[NSThread currentThread]);
if(error == nil)
//解析数据,JSON解析请参考
/wendingding/p/3815303.html
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:self.responseData options:kNilOptions error:nil];
NSLog(@&%@&,dict);
执行代码结果:在 SegmentFault,解决技术问题
每个月,我们帮助 1000 万的开发者解决各种各样的技术问题。并助力他们在技术能力、职业生涯、影响力上获得提升。
一线的工程师、著名开源项目的作者们,都在这里:
获取验证码
已有账号?
问题对人有帮助,内容完整,我也想知道答案
问题没有实际价值,缺少关键内容,没有改进余地
现在做一个社交的APP。用的AFNetworking来做的网络。
我是通过AFHTTPRequestOperation,然后将operation添加到AFHTTPRequestOperationManager的operationQueue里面通过队列的方式进行网络请求的调度的。
我现在比较担心就是在网络比较慢的情况下,用户在ViewController1当中进行了读取数据操作,然后等了一段时间后没有读到数据,希望直接退出ViewController1。当退出后,之前读取数据的操作仍然在operationQueue里面进行网络访问。
有没有比较好的设计方式能够避免这种界面已经被Pop或dismiss掉之后,其网络请求仍然占据网络资源的问题
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
AFHTTPRequestOperationManager 有个 operationQueue 属性,它是 NSOperationQueue 类的实例,它有一个方法 cancelAllOperations。
文档:This method calls the cancel method on all operations currently in the queue.
在 viewWillDisappear 方法中调用就行。
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
重写viewdiddisappear方法
答案对人有帮助,有参考价值
答案没帮助,是错误的答案,答非所问
“仍然占据网络资源”这个描述就不太准确。。。这么说吧,一个网络请求发出去了,比如数据经过TCP、IP层发到路由器等网路上了你是取消不了的,之后服务器的数据返回路径你也是控制不了的,楼上说cancel掉,NSOperation的cancel只是把isCanceled标志位置位而已,做不了其他的呀,所以你看SDWebImage里的网络回调的block都有判断当前operation的isCanceled是否为YES了,为NO才回调呢
同步到新浪微博
分享到微博?
关闭理由:
删除理由:
忽略理由:
推广(招聘、广告、SEO 等)方面的内容
与已有问题重复(请编辑该提问指向已有相同问题)
答非所问,不符合答题要求
宜作评论而非答案
带有人身攻击、辱骂、仇恨等违反条款的内容
无法获得确切结果的问题
非开发直接相关的问题
非技术提问的讨论型问题
其他原因(请补充说明)
我要该,理由是:iOS网络请求在Controller退出后是否应该被取消?a year agosudo tcpdump -i rvi0 -AAlnn src 23.22.14.18 or dst 23.22.14.18
下图是tcpdump获取的结果:图中标号1,2,3表示的tcp三次握手。标号4是客户端发送Http Get,标号5是Server Ack。标号6是Server Response。后面还有几个断开tcp连接的包被我省去了,前面这6个packet足以说明问题了。上述三步中,前两步消耗的是用户上行的流量,第三步用的下行的流量。国内移动运营商对于上行和下行的流量都算在包月的总流量当中的,所以如果用户在第六个或者之后的Packet没有返回之前退出Controller的话,后续的Packet依然会通过连接,走下行通道,抵达我们的客户端,耗费用户的流量,Response往往还是一个请求的流量大头。如果我们在请求返回之前,调用Cancel:[dataTask cancel];
取消请求的话,客户端会发送如下一个Packet断开当前的Http连接,阻止后续的流量产生:所以结论是:如果不Cancel,请求完成之后通过回调找到delegate,如果是weak引用,Controller被释放,delegate变为nil,业务流程被中断,代码还算安全。但是会的的确确浪费一些用户流量。养成好习惯,自己产生的垃圾自己清理哦。欢迎关注公众号:MrPeakTech赞赏1 人赞赏32收藏分享举报文章被以下专栏收录不定期iOS深度技术文章分享推荐阅读{&debug&:false,&apiRoot&:&&,&paySDK&:&https:\u002F\\u002Fapi\u002Fjs&,&wechatConfigAPI&:&\u002Fapi\u002Fwechat\u002Fjssdkconfig&,&name&:&production&,&instance&:&column&,&tokens&:{&X-XSRF-TOKEN&:null,&X-UDID&:null,&Authorization&:&oauth c3cef7c66aa9e6a1e3160e20&}}{&database&:{&Post&:{&&:{&isPending&:false,&contributes&:[{&sourceColumn&:{&lastUpdated&:,&description&:&不定期更新一些iOS相关的深度技术文章,来自工作当中的技术积累和心得,文章均同步于个人技术博客。也欢迎通过二维码关注个人公众号。&,&permission&:&COLUMN_PUBLIC&,&memberId&:2685152,&contributePermission&:&COLUMN_PUBLIC&,&translatedCommentPermission&:&all&,&canManage&:true,&intro&:&不定期iOS深度技术文章分享&,&urlToken&:&mrpeak&,&id&:21134,&imagePath&:&v2-d4bf75b366effc9.jpg&,&slug&:&mrpeak&,&applyReason&:&0&,&name&:&MrPeak杂货铺&,&title&:&MrPeak杂货铺&,&url&:&https:\u002F\\u002Fmrpeak&,&commentPermission&:&COLUMN_ALL_CAN_COMMENT&,&canPost&:true,&created&:,&state&:&COLUMN_NORMAL&,&followers&:946,&avatar&:{&id&:&v2-d4bf75b366effc9&,&template&:&https:\u002F\\u002F{id}_{size}.jpg&},&activateAuthorRequested&:false,&following&:false,&imageUrl&:&https:\u002F\\u002Fv2-d4bf75b366effc9_l.jpg&,&articlesCount&:49},&state&:&accepted&,&targetPost&:{&titleImage&:&https:\u002F\\u002Fv2-1dbaa905853_r.png&,&lastUpdated&:,&imagePath&:&v2-1dbaa905853.png&,&permission&:&ARTICLE_PUBLIC&,&topics&:[1724],&summary&:&一个编写iOS代码的经典场景:用户进入某个Controller,发起Http网络请求从Server获取数据,在数据返回之前用户退出了Controller。此时是否需要Cancel之前发出的网络请求呢?如果请求的数据只在当前Controller产生内容,结论当然是需要Cancel,虽然我知道不…&,&copyPermission&:&ARTICLE_COPYABLE&,&translatedCommentPermission&:&all&,&likes&:0,&origAuthorId&:0,&publishedTime&:&T21:55:34+08:00&,&sourceUrl&:&&,&urlToken&:,&id&:1467262,&withContent&:false,&slug&:,&bigTitleImage&:false,&title&:&iOS网络请求在Controller退出后是否应该被取消?&,&url&:&\u002Fp\u002F&,&commentPermission&:&ARTICLE_ALL_CAN_COMMENT&,&snapshotUrl&:&&,&created&:,&comments&:0,&columnId&:21134,&content&:&&,&parentId&:0,&state&:&ARTICLE_PUBLISHED&,&imageUrl&:&https:\u002F\\u002Fv2-1dbaa905853_r.png&,&author&:{&bio&:&iOS 技术博客&,&isFollowing&:false,&hash&:&9b13af9f7&,&uid&:08,&isOrg&:false,&slug&:&mrpeak&,&isFollowed&:false,&description&:&公众号:MrPeakTech&,&name&:&MrPeak&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fmrpeak&,&avatar&:{&id&:&v2-79fbcdd188e&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&memberId&:2685152,&excerptTitle&:&&,&voteType&:&ARTICLE_VOTE_CLEAR&},&id&:457481}],&title&:&iOS网络请求在Controller退出后是否应该被取消?&,&author&:&mrpeak&,&content&:&\u003Cp\u003E一个编写iOS代码的经典场景:用户进入某个Controller,发起Http网络请求从Server获取数据,在数据返回之前用户退出了Controller。此时是否需要Cancel之前发出的网络请求呢?\u003C\u002Fp\u003E\u003Cp\u003E如果请求的数据只在当前Controller产生内容,结论当然是需要Cancel,虽然我知道不少iOS程序员因为偷懒而忘了取消。我们用工程的思维,深入本质,一起看下这背后都发生了什么,如果不Cancel会有哪些副作用。\u003C\u002Fp\u003E\u003Cp\u003E我们可以把一个Http请求分为这几步:\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E第一步:三次握手\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E这一步会有三个packet产生,sync,sync+ack,ack。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E第二步:客户端发送Http的request\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E此处根据请求类型产生的packet数量会有差异。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E第三步:客户端接收Server的Http Response\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E根据Response的大小,产生的packet数量不相同,一般一个packet在1.5KB左右。\u003C\u002Fp\u003E\u003Ch4\u003E通过代码查看测试样本\u003C\u002Fh4\u003E\u003Cp\u003E我们在Demo项目中运行如下代码,再配合tcpdump抓包看看背后究竟发生了什么?\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];\n
AFURLSessionManager *manager = [[AFURLSessionManager alloc] initWithSessionConfiguration:configuration];\n\n
NSURL *URL = [NSURL URLWithString:@\&http:\u002F\u002Fhttpbin.org\u002Fget\&];\n
NSURLRequest *request = [NSURLRequest requestWithURL:URL];\n\n
NSURLSessionDataTask *dataTask = [manager dataTaskWithRequest:request completionHandler:^(NSURLResponse *response, id responseObject, NSError *error) {\n
if (error) {\n
NSLog(@\&Error: %@\&, error);\n
} else {\n
NSLog(@\&%@ %@\&, response, responseObject);\n
[dataTask resume];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Esudo tcpdump -i rvi0 -AAlnn src 23.22.14.18 or dst 23.22.14.18\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E下图是tcpdump获取的结果:\u003C\u002Fp\u003E\u003Cnoscript\u003E\u003Cimg src=\&https:\u002F\\u002Fv2-efd103d398fd65c6c187fdc_b.png\& data-rawwidth=\&2880\& data-rawheight=\&1800\& class=\&origin_image zh-lightbox-thumb\& width=\&2880\& data-original=\&https:\u002F\\u002Fv2-efd103d398fd65c6c187fdc_r.png\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='2880'%20height='1800'&&\u002Fsvg&\& data-rawwidth=\&2880\& data-rawheight=\&1800\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&2880\& data-original=\&https:\u002F\\u002Fv2-efd103d398fd65c6c187fdc_r.png\& data-actualsrc=\&https:\u002F\\u002Fv2-efd103d398fd65c6c187fdc_b.png\&\u003E\u003Cul\u003E\u003Cli\u003E图中标号1,2,3表示的tcp三次握手。\u003C\u002Fli\u003E\u003Cli\u003E标号4是客户端发送Http Get,标号5是Server Ack。\u003C\u002Fli\u003E\u003Cli\u003E标号6是Server Response。\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E后面还有几个断开tcp连接的包被我省去了,前面这6个packet足以说明问题了。\u003C\u002Fp\u003E\u003Cp\u003E上述三步中,前两步消耗的是用户上行的流量,第三步用的下行的流量。国内移动运营商对于上行和下行的流量都算在包月的总流量当中的,所以如果用户在第六个或者之后的Packet没有返回之前退出Controller的话,后续的Packet依然会通过连接,走下行通道,抵达我们的客户端,耗费用户的流量,Response往往还是一个请求的流量大头。\u003C\u002Fp\u003E\u003Cp\u003E如果我们在请求返回之前,调用Cancel:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E[dataTask cancel];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E取消请求的话,客户端会发送如下一个Packet断开当前的Http连接,阻止后续的流量产生:\u003C\u002Fp\u003E\u003Cnoscript\u003E\u003Cimg src=\&https:\u002F\\u002Fv2-3c3a6b93dfdea6515e6d_b.png\& data-rawwidth=\&1450\& data-rawheight=\&228\& class=\&origin_image zh-lightbox-thumb\& width=\&1450\& data-original=\&https:\u002F\\u002Fv2-3c3a6b93dfdea6515e6d_r.png\&\u003E\u003C\u002Fnoscript\u003E\u003Cimg src=\&data:image\u002Fsvg+utf8,&svg%20xmlns='http:\u002F\u002Fwww.w3.org\u002FFsvg'%20width='1450'%20height='228'&&\u002Fsvg&\& data-rawwidth=\&1450\& data-rawheight=\&228\& class=\&origin_image zh-lightbox-thumb lazy\& width=\&1450\& data-original=\&https:\u002F\\u002Fv2-3c3a6b93dfdea6515e6d_r.png\& data-actualsrc=\&https:\u002F\\u002Fv2-3c3a6b93dfdea6515e6d_b.png\&\u003E\u003Cp\u003E所以结论是:如果不Cancel,请求完成之后通过回调找到delegate,如果是weak引用,Controller被释放,delegate变为nil,业务流程被中断,代码还算安全。但是会的的确确浪费一些用户流量。养成好习惯,自己产生的垃圾自己清理哦。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E欢迎关注公众号:MrPeakTech\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cbr\u003E&,&updated&:new Date(&T13:55:34.000Z&),&canComment&:false,&commentPermission&:&anyone&,&commentCount&:13,&collapsedCount&:0,&likeCount&:32,&state&:&published&,&isLiked&:false,&slug&:&&,&lastestTipjarors&:[{&isFollowed&:false,&name&:&章北海&,&headline&:&嗯&,&avatarUrl&:&https:\u002F\\u002F50\u002Fv2-99fb3ee4edda862cb3cf2b19f5ae8157_s.jpg&,&isFollowing&:false,&type&:&people&,&slug&:&jianjiandandan01&,&bio&:&简单,宁静,珍惜&,&hash&:&ab77b31321c9dbd080b62e4&,&uid&:770000,&isOrg&:false,&description&:&嗯&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fjianjiandandan01&,&avatar&:{&id&:&v2-99fb3ee4edda862cb3cf2b19f5ae8157&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false}],&isTitleImageFullScreen&:false,&rating&:&none&,&titleImage&:&https:\u002F\\u002Fv2-1dbaa905853_r.png&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&reviewers&:[],&topics&:[{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&iOS 开发&}],&adminClosedComment&:false,&titleImageSize&:{&width&:1642,&height&:1010},&href&:&\u002Fapi\u002Fposts\u002F&,&excerptTitle&:&&,&column&:{&slug&:&mrpeak&,&name&:&MrPeak杂货铺&},&tipjarState&:&activated&,&tipjarTagLine&:&真诚赞赏,手留余香&,&sourceUrl&:&&,&pageCommentsCount&:13,&tipjarorCount&:1,&annotationAction&:[],&hasPublishingDraft&:false,&snapshotUrl&:&&,&publishedTime&:&T21:55:34+08:00&,&url&:&\u002Fp\u002F&,&lastestLikers&:[{&bio&:&说能做的,做说过的。&,&isFollowing&:false,&hash&:&0aead3a7b8ac3ccc213e463e57047d4a&,&uid&:12,&isOrg&:false,&slug&:&shuai.huang&,&isFollowed&:false,&description&:&&,&name&:&黄帅&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fshuai.huang&,&avatar&:{&id&:&82e43b50fac3ce6b6a77f6f0fd5ab042&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&吃过猪肉,见过猪跑。友善度为负&,&isFollowing&:false,&hash&:&38f03ebae11f6aa3977aca5&,&uid&:622200,&isOrg&:false,&slug&:&zhang-hua-jian-87&,&isFollowed&:false,&description&:&吃过猪肉,见过猪跑。&,&name&:&茶包子&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fzhang-hua-jian-87&,&avatar&:{&id&:&da8e974dc&,&template&:&https:\u002F\\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:null,&isFollowing&:false,&hash&:&7a210fa5fa84b1305d8c&,&uid&:889660,&isOrg&:false,&slug&:&jcccZzzz&,&isFollowed&:false,&description&:&&,&name&:&JcccZ&,&profileUrl&:&https:\u002F\\u002Fpeople\u002FjcccZzzz&,&avatar&:{&id&:&v2-1c4f01dfe4c7a55cb900c&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&我会遇见你&,&isFollowing&:false,&hash&:&c0ff4bdda0&,&uid&:315000,&isOrg&:false,&slug&:&sun-zhe-73-91&,&isFollowed&:false,&description&:&&,&name&:&收藏温柔&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fsun-zhe-73-91&,&avatar&:{&id&:&v2-04a4cde376e7fa1621c0&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},{&bio&:&写代码的&,&isFollowing&:false,&hash&:&4db039ecd10ffd714e049214&,&uid&:424700,&isOrg&:false,&slug&:&zzZzz1&,&isFollowed&:false,&description&:&&,&name&:&张冲&,&profileUrl&:&https:\u002F\\u002Fpeople\u002FzzZzz1&,&avatar&:{&id&:&8eebcc7b5ba&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false}],&summary&:&\u003Cimg src=\&https:\u002F\\u002F50\u002Fv2-efd103d398fd65c6c187fdc_200x112.png\& data-rawwidth=\&2880\& data-rawheight=\&1800\& class=\&origin_image inline-img zh-lightbox-thumb\& data-original=\&https:\u002F\\u002F50\u002Fv2-efd103d398fd65c6c187fdc_r.png\&\u003E一个编写iOS代码的经典场景:用户进入某个Controller,发起Http网络请求从Server获取数据,在数据返回之前用户退出了Controller。此时是否需要Cancel之前发出的网络请求呢?如果请求的数据只在当前Controller产生内容,结论当然是需要Cancel,虽然我知道不…&,&reviewingCommentsCount&:0,&meta&:{&previous&:{&isTitleImageFullScreen&:false,&rating&:&none&,&titleImage&:&https:\u002F\\u002F50\u002Fv2-9823f100_xl.jpg&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&topics&:[{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&iOS 开发&}],&adminClosedComment&:false,&href&:&\u002Fapi\u002Fposts\u002F&,&excerptTitle&:&&,&author&:{&bio&:&iOS 技术博客&,&isFollowing&:false,&hash&:&9b13af9f7&,&uid&:08,&isOrg&:false,&slug&:&mrpeak&,&isFollowed&:false,&description&:&公众号:MrPeakTech&,&name&:&MrPeak&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fmrpeak&,&avatar&:{&id&:&v2-79fbcdd188e&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&column&:{&slug&:&mrpeak&,&name&:&MrPeak杂货铺&},&content&:&\u003Cp\u003EUIViewController是iOS应用的基础业务单位,每个iOS程序员都写过无数的Controller。今天和大家一起来深度解剖Controller,看看怎么来做一次深度的重构。\u003C\u002Fp\u003E\u003Ch3\u003E\u003Cb\u003E重构的前提\u003C\u002Fb\u003E\u003C\u002Fh3\u003E\u003Cp\u003E我们应该谨慎的去重构我们的代码。iOS系统提供的UIViewController一定程度上可以很好的应付简单的页面单位,对于复杂的页面,我们也可以采用市面上主流的MV(X)系列模式,比如MVP,MVVM等。但随着单个Controller内业务进一步增长,我们需要更细粒度的重构,或者说对MV(X)做进一步的定制。\u003C\u002Fp\u003E\u003Cp\u003E以下图映客App两个页面为例。\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-4534f6adac2d5f717bcc7_b.png\& data-rawwidth=\&1500\& data-rawheight=\&1334\& class=\&origin_image zh-lightbox-thumb\& width=\&1500\& data-original=\&http:\u002F\\u002Fv2-4534f6adac2d5f717bcc7_r.png\&\u003E\u003Cbr\u003E\u003Cp\u003E左边页面元素少且静态,一个TableView基本上就能应付,右边的直播页面则元素多且动态,传统的MV(X)也会显得粒度太粗,这类复杂页面虽然不常遇到,但往往体现一个App的核心功能,合理的搭建或者重构这类Controller十分重要。\u003C\u002Fp\u003E\u003Ch3\u003E\u003Cb\u003E重构的本质\u003C\u002Fb\u003E\u003C\u002Fh3\u003E\u003Cp\u003E如何去定义重构,以我的理解可以归纳为两个关键词:分解,连接。\u003C\u002Fp\u003E\u003Cp\u003E重构的前提是复杂,臃肿,不直观,重构的手段是分解之后再连接。以映客的直播界面为例,UI元素,用户事件,服务器交互等基础元素都非常之多,以一个简单的MVP去归类代码犹嫌不足,我们还需要进一步的分解成view1,view2...viewN,presenter1,presenter2...presenterN,model1,model2...modelN,第二个问题是如何把这一个个的类文件或者说功能单位合理组织连接起来。完成上述两步我们就完成了一次重构,每一次将代码打散再串联就是一次重构。\u003C\u002Fp\u003E\u003Ch3\u003E\u003Cb\u003E分解UIViewController\u003C\u002Fb\u003E\u003C\u002Fh3\u003E\u003Cp\u003E写了那么多Controller,让你来说下一个Controller都细分为哪些更小的功能单位,你能随口说出来么?只有做过足够多的业务,才能慢慢对Controller的构成有自己的理解。\u003C\u002Fp\u003E\u003Cp\u003E当然可以回答说MVC或者MVP,但这个答案粒度太粗,一个Controller内部会发生哪些事可以说的更细,我们看下VIPER的答案:\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E\u003Cstrong\u003EView:\u003C\u002Fstrong\u003E displays what it is told to by the Presenter and relays user input back to the Presenter.\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Cstrong\u003EInteractor:\u003C\u002Fstrong\u003E contains the business logic as specified by a use case.\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Cstrong\u003EPresenter:\u003C\u002Fstrong\u003E contains view logic for preparing content for display (as received from the Interactor) and for reacting to user inputs (by requesting new data from the Interactor).\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Cstrong\u003EEntity:\u003C\u002Fstrong\u003E contains basic model objects used by the Interactor.\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E\u003Cstrong\u003ERouting:\u003C\u002Fstrong\u003E contains navigation logic for describing which screens are shown in which order\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003EView不用多说,可以分解成更多的子View,最后合成一个树形结构。\u003C\u002Fp\u003E\u003Cp\u003EEntity自然是代表Model。\u003C\u002Fp\u003E\u003Cp\u003EMVC当中的C,MVP当中的P,被细分成了Interactor,Presenter,和Routing。这三个角色各自负责什么职责呢?\u003C\u002Fp\u003E\u003Cp\u003ERouting比较清楚,处理页面之间的跳转。我见过的项目代码里,很少有把这一部分单独拎出来的,但其实很有意义,这部分代表的是不同Controller之间耦合依赖的方式,无论是从类关系描述的角度还是Debug的角度,都能帮助我们快速定位代码。\u003C\u002Fp\u003E\u003Cp\u003EInteractor和Presenter初看起来很类似,似乎都是在处理业务逻辑。但业务逻辑其实是个大的归类,可以描述任何一种业务场景和行为。Interactor当中有个很重要的术语:use case,这个术语很多技术文章中都会遇见,它代表的是一个完整的,独立的,细分过后的业务流程,比如我们App当中的登录模块,它是一个业务单位,但它其实可以进一步的细分为很多的use case:\u003C\u002Fp\u003E\u003Cp\u003Euse case 1: 验证邮箱长度\u003C\u002Fp\u003E\u003Cp\u003Euse case 2: 密码强度检验\u003C\u002Fp\u003E\u003Cp\u003Euse case 3: 从Server查询user name是否可用\u003C\u002Fp\u003E\u003Cp\u003E...\u003C\u002Fp\u003E\u003Cp\u003Euser case N\u003C\u002Fp\u003E\u003Cp\u003E定义use case有什么好处呢?\u003C\u002Fp\u003E\u003Cp\u003E好处当然是分门别类,结构清晰。你把100本书堆一堆,或者放书架上按类别摆放,下次找书的时候那种方式你更舒服?独立出一个个的use case还有一个好处是方便unit test,如果项目对每一个use case都有写对应的unit test,每次遇到“前一发动全身“的业务更改,可以边杯茶边写代码。\u003C\u002Fp\u003E\u003Cp\u003E我见过不少代码都体现不出use case的分类,可以回头看下自己当前项目的登录模块,上面我提到的这些case有没有在类文件当中合理摆放,还是都搅在一起?\u003C\u002Fp\u003E\u003Cp\u003E所以VIPER当中interactor的说法是强化大家写单独的use case的意识,打开interactor.m,看到一个函数代表一个use case,同一类的use case再用#pragma mark 归在一块,别人看你代码时能不赏心悦目吗?\u003C\u002Fp\u003E\u003Cp\u003E再说到Presenter,Presenter可以看做是上面一个个use case的使用者和响应者。使用者将各个use case串联起来描述一个完整详细的业务流程,比如我们的登录模块,每次用户点击按钮注册的时候,会触发一系列的use case,从检验用户输入合法性,设备网络状态,服务器资源是否可用,到最后处理结果并展示,这就是一个完整的业务流程,这个流程由Presenter来描述。响应者表示Presenter在接收到服务器反馈之后进一步改变本地的状态,比如view的展示,新的数据修改等,甚至会调用Routing发生页面跳转。\u003C\u002Fp\u003E\u003Cp\u003E说到这里就比较明了了,interactor和routing都是服务的提供方,presenter是服务的使用和集成方。VIPER说白了不过是对传统的MVC当中的C做了进一步细分。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E能不能分的更细呢?\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E当然可以,VIPER的分法是一种通用的做法,我们还可以从业务的角度去做细分。拿映客的直播界面做例子,比如Presenter当中包含了很多完整的业务流程:\u003C\u002Fp\u003E\u003Cul\u003E\u003Cli\u003E\u003Cp\u003E收到用户消息并展示\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E收到礼品消息并展示\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E收到弹幕消息并展示\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E收到用户进出房间的事件,处理并展示\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003Cli\u003E\u003Cp\u003E收到XXX,处理并展示\u003C\u002Fp\u003E\u003C\u002Fli\u003E\u003C\u002Ful\u003E\u003Cp\u003E以Objective C语言的特性,我们可以生成更多的Presenter Category来安置这些流程,比如LivePresenter+Message, LivePresenter+Gift, LivePresenter+Danmu, LivePresenter+Room, LivePresenter+XXX。\u003C\u002Fp\u003E\u003Cp\u003E不要觉得上面几个业务流程很简单,一个presenter处理绰绰有余,我前段时间刚好做过一个直播项目,Presenter类超过1000行代码很轻松。\u003C\u002Fp\u003E\u003Cp\u003E还可以进一步细分,一个功能复杂繁多的页面基本上离不开UITableView,而tableview的代码量主要在于delegate和datasource。这两个职责当然可以放在presenter当中,或者我们向Android学习,把它们也独立出来放到单独的类文件中去处理,比如叫做Adapter,用代码来说就是:\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-objective-c\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003E_tableView\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E.\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003Edelegate\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E=\u003C\u002Fspan\u003E \u003Cspan class=\&nb\&\u003Eself\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E.\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003Eadapter\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E;\u003C\u002Fspan\u003E\n\u003Cspan class=\&n\&\u003E_tableView\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E.\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003EdataSource\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E=\u003C\u002Fspan\u003E \u003Cspan class=\&nb\&\u003Eself\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E.\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003Eadapter\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E;\u003C\u002Fspan\u003E\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E和tableView相关的这些代码都搬到了adapter当中:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-objective-c\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u003Cspan class=\&k\&\u003E@protocol\u003C\u002Fspan\u003E \u003Cspan class=\&bp\&\u003EUITableViewDelegate\u003C\u002Fspan\u003E\n\n\u003Cspan class=\&p\&\u003E-\u003C\u002Fspan\u003E \u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003ECGFloat\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nf\&\u003EtableView:\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003EUITableView\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E*\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nv\&\u003EtableView\u003C\u002Fspan\u003E \u003Cspan class=\&nf\&\u003EheightForRowAtIndexPath:\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003ENSIndexPath\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E*\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nv\&\u003EindexPath\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E;\u003C\u002Fspan\u003E\n\u003Cspan class=\&p\&\u003E-\u003C\u002Fspan\u003E \u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003ECGFloat\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nf\&\u003EtableView:\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003EUITableView\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E*\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nv\&\u003EtableView\u003C\u002Fspan\u003E \u003Cspan class=\&nf\&\u003EheightForHeaderInSection:\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003ENSInteger\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nv\&\u003Esection\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E;\u003C\u002Fspan\u003E\n\u003Cspan class=\&p\&\u003E-\u003C\u002Fspan\u003E \u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003ECGFloat\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nf\&\u003EtableView:\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003EUITableView\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E*\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nv\&\u003EtableView\u003C\u002Fspan\u003E \u003Cspan class=\&nf\&\u003EheightForFooterInSection:\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003ENSInteger\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nv\&\u003Esection\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E;\u003C\u002Fspan\u003E\n\n\u003Cspan class=\&k\&\u003E@end\u003C\u002Fspan\u003E\n\n\u003Cspan class=\&k\&\u003E@protocol\u003C\u002Fspan\u003E \u003Cspan class=\&bp\&\u003EUITableViewDataSource\u003C\u002Fspan\u003E\u003Cspan class=\&o\&\u003E&\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003ENSObject\u003C\u002Fspan\u003E\u003Cspan class=\&o\&\u003E&\u003C\u002Fspan\u003E\n\n\u003Cspan class=\&k\&\u003E@required\u003C\u002Fspan\u003E\n\u003Cspan class=\&o\&\u003E-\u003C\u002Fspan\u003E \u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003ENSInteger\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nl\&\u003EtableView\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E:(\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003EUITableView\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E*\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003EtableView\u003C\u002Fspan\u003E \u003Cspan class=\&nl\&\u003EnumberOfRowsInSection\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E:(\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003ENSInteger\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&n\&\u003Esection\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E;\u003C\u002Fspan\u003E\n\u003Cspan class=\&p\&\u003E-\u003C\u002Fspan\u003E \u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003EUITableViewCell\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E*\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nf\&\u003EtableView:\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003EUITableView\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E*\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nv\&\u003EtableView\u003C\u002Fspan\u003E \u003Cspan class=\&nf\&\u003EcellForRowAtIndexPath:\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E(\u003C\u002Fspan\u003E\u003Cspan class=\&bp\&\u003ENSIndexPath\u003C\u002Fspan\u003E \u003Cspan class=\&o\&\u003E*\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E)\u003C\u002Fspan\u003E\u003Cspan class=\&nv\&\u003EindexPath\u003C\u002Fspan\u003E\u003Cspan class=\&p\&\u003E;\u003C\u002Fspan\u003E\n\n\u003Cspan class=\&k\&\u003E@end\u003C\u002Fspan\u003E\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E我们的Presenter就变得更加干净了,看起来和刚大扫除过的房间一样令人愉悦。\u003C\u002Fp\u003E\u003Cp\u003E好了,到这里我们盘子里的牛排已经被切成很多小块了,可以开始享用这些美味的代码了,继续我们的第二步工作:连接。\u003C\u002Fp\u003E\u003Ch3\u003E\u003Cb\u003E连接\u003C\u002Fb\u003E\u003C\u002Fh3\u003E\u003Cp\u003E先看下我们分解之后有哪些元素:\u003C\u002Fp\u003E\u003Cp\u003Eview(1…N), model(1…N), interactor, presenter(1…N), routing, adapter。看着应该粒度够细了,对于复杂的Controller,我个人习惯的做法和VIPER相近,但略有不同,Interactor当中的use case通过分层的架构被我放到server layer,分层的架构是另一个话题,这里不做细述。其他元素基本一致。\u003C\u002Fp\u003E\u003Cp\u003E至于怎么连接,手段无非就是OC的几种类交互机制:\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003EDelegate, Target-Action, Block, Notification, KVO\u003C\u002Fstrong\u003E。\u003C\u002Fp\u003E\u003Cp\u003E这几者之间的差异可以参考\u003Ca href=\&http:\u002F\\u002F?target=https%3A\u002F\u002Fwww.objc.io\u002Fissues\u002F7-foundation\u002Fcommunication-patterns\u002F\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003Eobjc.io的一篇经典文章\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E。选择不同对耦合度,开发便捷性,调试是否方便等都会产生影响,如何应用不同的机制将各个单位串联起来就看架构师自己的积累和理解了,任何一个选择都有其优势和局限性。\u003C\u002Fp\u003E\u003Cp\u003E如果拿捏不准选哪个好的时候,我个人建议就使用delegate,朴素可靠且直观。delegate需要在不同的元素之间传递,代码量会偏多一些,但优点在protocol定义清晰,耦合在哪里一目了然,记得要注意循环引用的问题。\u003C\u002Fp\u003E\u003Cp\u003E我早些时候其他几种机制都在实际项目中做过尝试,最后综合比较还是倾向于选择delegate,再后来经过一番脑洞(主要是为了解决传递delegate所带来的额外代码量),利用runtime特性,做了一个CDD机制来自动串联各个功能单位。\u003Ca href=\&http:\u002F\\u002F?target=http%3A\\u002Fblog\u002Fcdd\u002F\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003ECDD的详细介绍\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E在之前的博客中有,这里也不细述了,其本质或者说最终目的还是在于连接。\u003C\u002Fp\u003E\u003Cp\u003E说完了分解和连接,Controller的重构完成了大半,还剩下一个至关重要的概念:状态分享。\u003C\u002Fp\u003E\u003Ch3\u003E\u003Cb\u003E尽量避免跨类,跨模块或跨层共享状态\u003C\u002Fb\u003E\u003C\u002Fh3\u003E\u003Cp\u003E我之前在一篇博客中谈到过对于\u003Ca href=\&http:\u002F\\u002F?target=http%3A\\u002Fblog\u002Fstate\u002F\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E程序状态的维护\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E。状态是否维护得好对于程序的整体稳定性很有影响,对于Controller当中的状态维护我有一个简单的建议:\u003C\u002Fp\u003E\u003Cblockquote\u003E\u003Cp\u003E\u003Cstrong\u003E传递状态的时候尽可能Copy\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003C\u002Fblockquote\u003E\u003Cp\u003E之前流行的函数式编程其实就很强调无状态性,无状态不是让大家不定义状态变量,而是避免函数之间的状态共享,具体到OC当中,就是不要在不同的功能单位里使用指向同一块内存拷贝的地址,为什么共享状态是一件危险的事,我在之前的文章中也介绍过。\u003C\u002Fp\u003E\u003Cp\u003E一般来说,我们从Model Layer或者说数据层拿到的model实例,扔给Controller使用的时候应该是一份新的拷贝,在不同的类单位里共享NSMutableString或者NSMutableArray,NSMutableDictionary很容易让你的代码变得不稳定,而且这类不稳定性一般很难调试,debug填坑的时候经常按下葫芦浮起瓢。\u003C\u002Fp\u003E\u003Cp\u003E在controller内部传递model或者state的时候,我们应该也尽量使用copy行为,任何state你一旦暴露出去就不再安全,自己创建,自己修改,自己销毁才是正途。说到\u003C\u002Fp\u003E\u003Cp\u003E我之前介绍Facebook架构的时候就提到过,Facebook当中的model layer是由一个单独开发团队维护的,应用层开发人员(Controller开发人员)获取到的都是新的拷贝,要修改某个属性不一定有接口,甚至要向model的维护团队提交增加接口的申请,对于state维护的谨慎度可见一斑。\u003C\u002Fp\u003E\u003Ch3\u003E\u003Cb\u003E使用脚本生成原型代码\u003C\u002Fb\u003E\u003C\u002Fh3\u003E\u003Cp\u003E说了这么多,Controller重构的关键点都说完了。最后再提个小Tip,一旦Controller做深度细分之后,团队成员需要对Controller的分法和构成有一致的认识,写出来的代码应该保持一致,我的做法是通过脚本的方式生成Controller各个相关的类文件,比如我的Controller是如下结构:\u003C\u002Fp\u003E\u003Cimg src=\&http:\u002F\\u002Fv2-ceac712a6fed5639b4bff6_b.png\& data-rawwidth=\&512\& data-rawheight=\&486\& class=\&origin_image zh-lightbox-thumb\& width=\&512\& data-original=\&http:\u002F\\u002Fv2-ceac712a6fed5639b4bff6_r.png\&\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cp\u003E通过脚本将文件名和文件内容当中的Template全部替换成目标Controller的名字,就省去了很多重复代码的体力劳动,也达到了代码风格一致的目的。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cb\u003E在公众号回复:Template ,可以下载脚本和UIViewController模板文件。\u003C\u002Fb\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E欢迎关注公众号:MrPeakTech\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E&,&state&:&published&,&sourceUrl&:&&,&pageCommentsCount&:0,&canComment&:false,&snapshotUrl&:&&,&slug&:,&publishedTime&:&T09:50:22+08:00&,&url&:&\u002Fp\u002F&,&title&:&深度重构UIViewController&,&summary&:&UIViewController是iOS应用的基础业务单位,每个iOS程序员都写过无数的Controller。今天和大家一起来深度解剖Controller,看看怎么来做一次深度的重构。\u003Cb\u003E重构的前提\u003C\u002Fb\u003E我们应该谨慎的去重构我们的代码。iOS系统提供的UIViewController一定程度上可以很好的应付…&,&reviewingCommentsCount&:0,&meta&:{&previous&:null,&next&:null},&commentPermission&:&anyone&,&commentsCount&:16,&likesCount&:44},&next&:{&isTitleImageFullScreen&:false,&rating&:&none&,&titleImage&:&https:\u002F\\u002F50\u002Fv2-cb2eafe9a5e745fddb607a78fc46da11_xl.jpg&,&links&:{&comments&:&\u002Fapi\u002Fposts\u002F2Fcomments&},&topics&:[{&url&:&https:\u002F\\u002Ftopic\u002F&,&id&:&&,&name&:&iOS 开发&}],&adminClosedComment&:false,&href&:&\u002Fapi\u002Fposts\u002F&,&excerptTitle&:&&,&author&:{&bio&:&iOS 技术博客&,&isFollowing&:false,&hash&:&9b13af9f7&,&uid&:08,&isOrg&:false,&slug&:&mrpeak&,&isFollowed&:false,&description&:&公众号:MrPeakTech&,&name&:&MrPeak&,&profileUrl&:&https:\u002F\\u002Fpeople\u002Fmrpeak&,&avatar&:{&id&:&v2-79fbcdd188e&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false},&column&:{&slug&:&mrpeak&,&name&:&MrPeak杂货铺&},&content&:&\u003Cp\u003E之前详细谈过不少关于\u003Ca href=\&https:\u002F\\u002F?target=http%3A\u002F\u002FHTTP%E7%259A%%%25E4%25BA%259B%25E4%25BA%258B%2520-\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003EHTTP协议\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E的知识点,TCP\u002FIP也通过\u003Ca href=\&https:\u002F\\u002F?target=http%3A\u002F\u002FiOS%2CAndroid%25E7%25BD%%25BB%259C%25E6%258A%%258C%%E7%25A8%258B%25E4%25B9%258Btcpdump%2520-\& class=\& wrap external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003Etcpdump\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E做过简单的介绍,但网络协议的本质其实是连接,设备或者端之间连接的方式有多种,常见的http或者基于tcp的socket只是森林一叶,还有些不那么常见的协议比如蓝牙。适当脑洞,也能玩出不少新花样来。\u003C\u002Fp\u003E\u003Ch3\u003E关键概念\u003C\u002Fh3\u003E\u003Cp\u003E谈到蓝牙,很容易让人联想到蓝牙穿戴设备,好像听起来更靠近硬件层一些。苹果其实对iOS和OSX上的蓝牙已做了一层很好的封装,看过CoreBluetooth Framework的大致API之后,基本上就将其流程明白个大概。难点在于理解其工作模式和理清一些关键概念,比如Peripehral, Central, Service, characteristics等等,不要被这些陌生的单词吓到,网络协议的应用大多脱不了CS的架构模型,这里和大家一起对照传统的Client\u002FServer架构来梳理下iOS和OSX上CoreBluetooth的重要知识点。我画了一张图,方便大家一目了然的明白CoreBluetooth的工作原理。\u003C\u002Fp\u003E\u003Cimg src=\&https:\u002F\\u002F50\u002Fv2-32d9d982b167deee58a313b087d2a23d_b.png\& data-rawwidth=\&2204\& data-rawheight=\&1386\& class=\&origin_image zh-lightbox-thumb\& width=\&2204\& data-original=\&https:\u002F\\u002F50\u002Fv2-32d9d982b167deee58a313b087d2a23d_r.png\&\u003E\u003Cbr\u003E\u003Cp\u003E我们只需要把Peripehral, Central, Service, characteristics几个概念理清,再各自对应到我们之前关于CS的知识体系之中就可以轻松的做一层自己的封装了。\u003C\u002Fp\u003E\u003Cp\u003E初次查看CoreBluetooth文档的时候,很容易把Central理解成Server,其实刚好相反,Peripheral才是我们的Server。正如上图所示,Peripheral和Central之间建立的是一对多的关系。每个Peripheral会以广播的模式告诉外界自己能提供哪些Service,这里Service的概念和我们传统CS架构当中的Service基本是一致的,每个PeriPheral可以提供多个Service,而每个Service呢,会包含多个characteristic,characteristic是个陌生但十分关键的概念,可以把characteristic理解成一个Service模块具体提供哪些服务,比如一个心率监测Service同时包含心率测量characteristic和地理位置定位characteristic。\u003C\u002Fp\u003E\u003Cp\u003EPeripheral作为Server,Central作为Client,Peripheral广播自己的Service和characteristic,Central订阅某一个具体的characteristic,Peripheral就和Central之间通过characteristic建立了一个双向的数据通道,整个模型非常简洁而且符合我们CS的架构体系。接下来具体看下CoreBluetooth的相关API。\u003C\u002Fp\u003E\u003Ch3\u003E优雅的CoreBluetooth\u003C\u002Fh3\u003E\u003Cp\u003E首先值得开心一把的是iOS和OSX使用的是同一套API封装,都是基于CoreBluetooth Framework,只在极细小的地方有些差异,完全可以做一层library的封装在两个平台上无缝衔接使用。\u003C\u002Fp\u003E\u003Cp\u003E在具体搭建基于CoreBluetooth应用之前,要先确立到底哪一方作为Peripheral,哪一方又是Central。Macbook,iPhone,iPad都能成为Peripheral或者Central。我们通过代码的方式再看一遍上面的架构流程。\u003C\u002Fp\u003E\u003Ch4\u003EServer端\u003C\u002Fh4\u003E\u003Cp\u003E\u003Cstrong\u003E创建Peripheral,也就是我们的Server:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E_peripheral = [[CBPeripheralManager alloc] initWithDelegate:self queue:nil];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cstrong\u003E生成Service以备添加到Peripheral当中:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003ECBMutableService *transferService = [[CBMutableService alloc] initWithType:[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID] primary:YES];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cstrong\u003E生成characteristics以备添加到Service当中:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003Eself.transferCharacteristic = [[CBMutableCharacteristic alloc] initWithType:[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]\n
properties:CBCharacteristicPropertyNotify|CBCharacteristicPropertyWrite\n
value:nil\n
permissions:CBAttributePermissionsReadable|CBAttributePermissionsWriteable];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cstrong\u003E建立Peripheral,Server,characteristics三者之间的关系并开始广播服务:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002F建立关系\ntransferService.characteristics = @[self.transferCharacteristic];\n[self.peripheral addService:transferService];\n\u002F\u002F开始广播\n[self.peripheral startAdvertising:@{ CBAdvertisementDataServiceUUIDsKey : @[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]] }];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Ch4\u003EClient端\u003C\u002Fh4\u003E\u003Cp\u003E\u003Cstrong\u003E创建我们的Central,也就是client:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E_central = [[CBCentralManager alloc] initWithDelegate:self queue:nil];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cstrong\u003E扫描可用的Peripheral:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E[self.central scanForPeripheralsWithServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]]\n
options:@{ CBCentralManagerScanOptionAllowDuplicatesKey : @YES }];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cstrong\u003E扫描到Peripheral之后连接:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E[self.central connectPeripheral:targetPeripheral options:nil];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cstrong\u003E连接成功之后查找可用的Service:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E[peripheral discoverServices:@[[CBUUID UUIDWithString:TRANSFER_SERVICE_UUID]]];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cstrong\u003E找到Service之后,进一步查找可用的Characteristics并订阅:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002F查找Characteristics\n[peripheral discoverCharacteristics:@[[CBUUID UUIDWithString:TRANSFER_CHARACTERISTIC_UUID]] forService:service];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E\u003Cstrong\u003E查找到Characteristics订阅:\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E\u002F\u002F订阅\n[peripheral setNotifyValue:YES forCharacteristic:characteristic];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E订阅之后Central和Peripheral之间就建立了一个双向的数据通道,后续二者之间的数据传输就可以通过characteristic来完成了。\u003C\u002Fp\u003E\u003Ch3\u003E数据传输\u003C\u002Fh3\u003E\u003Cp\u003E有了数据通道,接下来就是如何传输数据了。说到数据传输就免不了要确定应用层的协议,类似平时我们使用socket实现游戏的网络模块时,需要自定义应用层协议才能实现业务数据的交换,协议的设计这里就不展开说了,之前有过相关经验的童鞋完全可以把协议层迁移过来。\u003C\u002Fp\u003E\u003Cp\u003E再看下Peripheral是如何向Central发送数据的,首先Peripheral会向自己的characteristic写数据:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E[self.peripheral updateValue:chunk forCharacteristic:self.transferCharacteristic onSubscribedCentrals:@[self.central]];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003ECentral那一端会通过如下回调收到来自Peripheral的数据流:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E- (void)peripheral:(CBPeripheral *)peripheral didUpdateValueForCharacteristic:(CBCharacteristic *)characteristic error:(NSError *)\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E这里值得注意的是二者数据的发送与获取,是以二进制流的方式发送的,是NSData的形式封装的,Peripheral可以持续不停的发送二进制流,所以Central端收到的时候需要自己做协议的解析,根据自定义协议将整个流拆成一个个的业务Packet包。\u003C\u002Fp\u003E\u003Cp\u003E而Central发送的时候确是封装成了一个个的Request,比如Central端调用如下API发送数据:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E[self.discoveredPeripheral writeValue:data forCharacteristic:self.discoveredCharacterstic type:CBCharacteristicWriteWithoutResponse];\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003EPeripheral端会收到如下回调:\u003C\u002Fp\u003E\u003Cdiv class=\&highlight\&\u003E\u003Cpre\u003E\u003Ccode class=\&language-text\&\u003E\u003Cspan\u003E\u003C\u002Fspan\u003E- (void)peripheralManager:(CBPeripheralManager *)peripheral didReceiveWriteRequests:(NSArray&CBATTRequest *& *)requests\n\u003C\u002Fcode\u003E\u003C\u002Fpre\u003E\u003C\u002Fdiv\u003E\u003Cp\u003E数据被封装成了单独的CBATTRequest,直接去Request当中取value就可以获取到Central所发送过来的数据。\u003C\u002Fp\u003E\u003Ch3\u003E已知的坑\u003C\u002Fh3\u003E\u003Cp\u003E我之前测试协议的时候发现一个不大不小的坑,多个Central(比如A和B)端同时一个Peripheral发送数据的时候,Peripheral会收到多个CBATTRequest,奇怪的是每个CBATTRequest当中的Central都会指向最先建立连接的A,结果导致Peripheral端无法判断write请求的数据来自哪一个Central。\u003C\u002Fp\u003E\u003Ch3\u003E简单脑洞\u003C\u002Fh3\u003E\u003Cp\u003E蓝牙不仅仅能应用于穿戴式设备,还能做一些好玩的小众应用或者游戏,其本质是一个小型封闭的局域网,不用经过第三方的Server或者Cloud,很安全。\u003C\u002Fp\u003E\u003Cp\u003E比如两台iPhone设备之间通过基于蓝牙的IM App进行聊天(距离这么近,为什么不当面聊,黑人问号?)。\u003C\u002Fp\u003E\u003Cp\u003E比如一些基于蓝牙对战的小游戏。\u003C\u002Fp\u003E\u003Cp\u003E比如通过蓝牙在iPhone和Macbook之间做数据同步。\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E对细节代码感兴趣的童鞋可以在公众号回复bt,查看官方的demo。\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Cstrong\u003E欢迎关注公众号:MrPeakTech\u003C\u002Fstrong\u003E\u003C\u002Fp\u003E\u003Cp\u003E\u003Ca href=\&https:\u002F\\u002F?target=http%3A\u002F\\u002Fr\u002FjD-awuDECr11raSh92qe\& class=\& external\& target=\&_blank\& rel=\&nofollow noreferrer\&\u003E\u003Cspan class=\&invisible\&\u003Ehttp:\u002F\u002F\u003C\u002Fspan\u003E\u003Cspan class=\&visible\&\\u002Fr\u002FjD-awuD\u003C\u002Fspan\u003E\u003Cspan class=\&invisible\&\u003EECr11raSh92qe\u003C\u002Fspan\u003E\u003Cspan class=\&ellipsis\&\u003E\u003C\u002Fspan\u003E\u003Ci class=\&icon-external\&\u003E\u003C\u002Fi\u003E\u003C\u002Fa\u003E (二维码自动识别)\u003C\u002Fp\u003E\u003Cbr\u003E\u003Cbr\u003E\u003Cbr\u003E&,&state&:&published&,&sourceUrl&:&&,&pageCommentsCount&:0,&canComment&:false,&snapshotUrl&:&&,&slug&:,&publishedTime&:&T12:05:29+08:00&,&url&:&\u002Fp\u002F&,&title&:&iOS开发之玩转蓝牙&,&summary&:&之前详细谈过不少关于\u003Ca href=\&http:\u002F\u002FHTTP%202.0%E7%9A%84%E9%82%A3%E4%BA%9B%E4%BA%8B%20-\&\u003EHTTP协议\u003C\u002Fa\u003E的知识点,TCP\u002FIP也通过\u003Ca href=\&http:\u002F\u002FiOS,Android%E7%BD%91%E7%BB%9C%E6%8A%93%E5%8C%85%E6%95%99%E7%A8%8B%E4%B9%8Btcpdump%20-\&\u003Etcpdump\u003C\u002Fa\u003E做过简单的介绍,但网络协议的本质其实是连接,设备或者端之间连接的方式有多种,常见的http或者基于tcp的socket只是森林一叶,还有些不那么常见的协议比如蓝牙。适当脑洞,也能玩出不少新花样来…&,&reviewingCommentsCount&:0,&meta&:{&previous&:null,&next&:null},&commentPermission&:&anyone&,&commentsCount&:0,&likesCount&:15}},&annotationDetail&:null,&commentsCount&:13,&likesCount&:32,&FULLINFO&:true}},&User&:{&mrpeak&:{&isFollowed&:false,&name&:&MrPeak&,&headline&:&公众号:MrPeakTech&,&avatarUrl&:&https:\u002F\\u002F50\u002Fv2-79fbcdd188e_s.jpg&,&isFollowing&:false,&type&:&people&,&slug&:&mrpeak&,&bio&:&iOS 技术博客&,&hash&:&9b13af9f7&,&uid&:08,&isOrg&:false,&description&:&公众号:MrPeakTech&,&badge&:{&identity&:null,&bestAnswerer&:null},&profileUrl&:&https:\u002F\\u002Fpeople\u002Fmrpeak&,&avatar&:{&id&:&v2-79fbcdd188e&,&template&:&https:\u002F\\u002F50\u002F{id}_{size}.jpg&},&isOrgWhiteList&:false,&isBanned&:false}},&Comment&:{},&favlists&:{}},&me&:{},&global&:{&experimentFeatures&:{&ge3&:&ge3_9&,&ge2&:&ge2_1&,&nwebStickySidebar&:&sticky&,&androidPassThroughPush&:&all&,&newMore&:&new&,&liveReviewBuyBar&:&live_review_buy_bar_2&,&liveStore&:&ls_a2_b2_c1_f2&,&searchHybridTabs&:&without-tabs&,&isOffice&:&false&,&newLiveFeedMediacard&:&old&,&homeUi2&:&default&,&remixOneKeyPlayButton&:&headerButton&,&recommendationAbtest&:&old&,&marketTab&:&market_tab_old&,&qrcodeLogin&:&qrcode&,&recommendArticleNum&:&4&,&isShowUnicomFreeEntry&:&unicom_free_entry_off&,&newMobileColumnAppheader&:&new_header&,&androidDbRecommendAction&:&open&,&zcmLighting&:&zcm&,&favAct&:&default&,&appStoreRateDialog&:&close&,&mobileQaPageProxyHeifetz&:&m_qa_page_nweb&,&default&:&None&,&wechatShareModal&:&wechat_share_modal_show&,&qaStickySidebar&:&sticky_sidebar&,&androidProfilePanel&:&panel_b&,&nwebWriteAnswer&:&default&}},&columns&:{&next&:{},&mrpeak&:{&following&:false,&canManage&:false,&href&:&\u002Fapi\u002Fcolumns\u002Fmrpeak&,&name&:&MrPeak杂货铺&,&creator&:{&slug&:&mrpeak&},&url&:&\u002Fmrpeak&,&slug&:&mrpeak&,&avatar&:{&id&:&v2-d4bf75b366effc9&,&template&:&https:\u002F\\u002F{id}_{size}.jpg&}}},&columnPosts&:{},&columnSettings&:{&colomnAuthor&:[],&uploadAvatarDetails&:&&,&contributeRequests&:[],&contributeRequestsTotalCount&:0,&inviteAuthor&:&&},&postComments&:{},&postReviewComments&:{&comments&:[],&newComments&:[],&hasMore&:true},&favlistsByUser&:{},&favlistRelations&:{},&promotions&:{},&draft&:{&titleImage&:&&,&titleImageSize&:{},&isTitleImageFullScreen&:false,&canTitleImageFullScreen&:false,&title&:&&,&titleImageUploading&:false,&error&:&&,&content&:&&,&draftLoading&:false,&globalLoading&:false,&pendingVideo&:{&resource&:null,&error&:null}},&drafts&:{&draftsList&:[],&next&:{}},&config&:{&userNotBindPhoneTipString&:{}},&recommendPosts&:{&articleRecommendations&:[],&columnRecommendations&:[]},&env&:{&edition&:{&baidu&:false,&yidianzixun&:false,&qqnews&:false},&isAppView&:false,&appViewConfig&:{&content_padding_top&:128,&content_padding_bottom&:56,&content_padding_left&:16,&content_padding_right&:16,&title_font_size&:22,&body_font_size&:16,&is_dark_theme&:false,&can_auto_load_image&:true,&app_info&:&OS=iOS&},&isApp&:false,&userAgent&:{&ua&:&Mozilla\u002F5.0 (compatible, MSIE 11, Windows NT 6.3; Trident\u002F7.0; rv:11.0) like Gecko&,&browser&:{&name&:&IE&,&version&:&11&,&major&:&11&},&engine&:{&version&:&7.0&,&name&:&Trident&},&os&:{&name&:&Windows&,&version&:&8.1&},&device&:{},&cpu&:{}}},&message&:{&newCount&:0},&pushNotification&:{&newCount&:0}}}

我要回帖

更多关于 ios 同步网络请求 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信