iOS网络架构讨论梳理
整理中。。。
其实如果没有APIManager这一层是没法使用delegate的,毕竟多个单页面多request的情况太普遍了,cancel request when dealloc补充了下思路,比RAC的swizz in dealloc温柔多了。至于block的问题extobjc基本上会是我的标配。Ping IP列表然后以NSURLProtocol改写URL这种方式怎么想都还是太暴力了…至少依赖OHHTTPStubs的测试框架就很难飞起了。SPDY/PipeLine希望能有更详细的介绍。希望增加针对不同网络环境的框架级优化。另,关于reformer,其实使用Model最大的好处应该是类型检查和可以围绕Model写部分fat model逻辑,例如Mantle或者MJExtension这样的repo,还提供了transform等机制,基本能做到reformer能干的活。当然,reform on call开销上,肯定是比reform要高,但若愿意,加一层proxy完全不是问题。
cas
其实我比较推荐对外采用离散的API调用方式,这种做法不光是delegate能变得可行,interceptor也能变得可行,interceptor也分内部interceptor和外部interceptor,所以如果集约的API调用方式,就很难处理这种场景了。具体的在PPT里面我有描述。另外,离散的API调用方式并不会影响单页面多request的需求,而且能够更好地完成业务需求。
ping IP列表只有在应用启动的时候才会做这个事情,并不会在每次发请求的时候都去做这个事情。关于NSURLProtocol这种方案,我觉得已经是属于相对比较好的了,这种做法对上层更加透明。或者,你觉得采用什么方式比较好?
看来后面我要针对SPDY和Pipeline写个补。关于不同网络环境的框架级优化貌似可做的事情不是很多,大部分是业务级的,比如断点续传,图片下载质量根据网络环境的切换等等,这个在框架级可以提供接口,但主要还是靠业务方去根据情况自己调度了。不过严格来说也算是优化的一部分,确实要写写。
类型检查其实是交给validator去做的,这个在我给出的代码里面有这个东西。关于model这里,另外一个要考虑的问题就是可维护性,我判断可维护性的其中一个标准就是,如果我把这个拿掉,换上别的逻辑,这个过程是否能够比较轻松。越轻松,可维护性就越好。reformer只做数据处理,APIManager只管理基础数据,等于是把fat model做了一个拆分,管理数据的专门管理数据,负责转化的专门负责转化,这样能够提高可维护性。
另外,我在上一篇文章里面是很推荐fat model的。但我推荐fat model的原因并不是因为一个model它只要fat就能比较好,而更多的是因为fat model比较好拆,把一个model弄成fat的最终目的是为了方便拆。这回的reformer就是拆了fat model,另外也解放了controller,因为数据转化的事情有的时候也会放到controller里面去做,不一定都会塞fat model里。
reformer嵌套
reformer这个主意很棒, 本质上能用json表示的数据都可以被NSDictionary再次表示一遍. reformer里面再来个标识物和嵌套就好了. 比如很多业务接口都有user_profile这种类型, 用一种统一的reformer来表示, 然后reformer嵌套reformer, 哈哈哈
cas
- 嵌套是支持的哈,但是支持的只是隐式嵌套~
- 就是指在reformer的转化方法里再调用reformer,因为它们的protocol都是一致的。
而不是在外面显式地嵌套调用不同的reformer。
我现在集约型用得这么爽,我为什么要用离散型?
其实你的代码应该是符合你的APP目前的业务需求的。但是考虑以下情况:
URL组装是有不同逻辑的,不同的API的URL分成了几种组装逻辑,这些API也是分别由不同的服务端团队去开发的。业务方只会传API的methodName,不同MethodName的签名逻辑也是不一样的。
APIMananger获得回来的数据是会有增量更新的。
不同API的请求策略也是不同的,具体的可以参考文中请求策略的部分。
取消请求时,需要精确取消某个请求。
如果你采用集约型API调用方式,上面这四种问题也还是能够解决的,但是手段就没有离散型的干净了。
如果用AFURLSessionManager的话,这个架构还合适吗?还是自己写NSURLSession呢?
这个架构的一大特点就是,它并不是针对某种调用方式而设计的。它是针对一个需求去设计的,这个需求就是API调用,是基于HTTP短链接的API调用而设计的。所以不管换成什么样的方式,只要还是这样的需求,这个架构就合适。
为何要使用以下的category动态设置请求的参数?
//
// NSURLRequest+CTNetworkingMethods.h
// RTNetworking
//
// Created by casa on 14-5-26.
// Copyright (c) 2014年 casatwy. All rights reserved.
//
#import
@interface NSURLRequest (CTNetworkingMethods)
@property (nonatomic, copy) NSDictionary *requestParams;
@end
//
// NSURLRequest+CTNetworkingMethods.m
// RTNetworking
//
// Created by casa on 14-5-26.
// Copyright (c) 2014年 casatwy. All rights reserved.
//
#import "NSURLRequest+CTNetworkingMethods.h"
#import
static void *CTNetworkingRequestParams;
@implementation NSURLRequest (CTNetworkingMethods)
- (void)setRequestParams:(NSDictionary *)requestParams
{
objc_setAssociatedObject(self, &CTNetworkingRequestParams, requestParams, OBJC_ASSOCIATION_COPY);
}
- (NSDictionary *)requestParams
{
return objc_getAssociatedObject(self, &CTNetworkingRequestParams);
}
@end
安居客不同的API在调用的时候,使用参数的方法都不同,有的是会把参数放在请求的header里面,有的会在body里面,有的会在URL里面。但是对于业务方来说,这些是透明的。框架在组装参数的时候会根据不同的API采用不同的组装方法,如果业务方要想看一下request的参数是什么,如果没有这个category,就很蛋疼了。
下拉刷新上拉加载的话,如何保存原始数据,如何来处理或维护APIBaseManager里的fetchedRawData,其实就是URLResponse里的content字段。
#import
#import "CTNetworkingConfiguration.h"
@interface CTURLResponse : NSObject
@property (nonatomic, assign, readonly) CTURLResponseStatus status;
@property (nonatomic, copy, readonly) NSString *contentString;
@property (nonatomic, copy, readonly) id content;
@property (nonatomic, assign, readonly) NSInteger requestId;
@property (nonatomic, copy, readonly) NSURLRequest *request;
@property (nonatomic, copy, readonly) NSData *responseData;
@property (nonatomic, copy) NSDictionary *requestParams;
@implementation CTURLResponse
#pragma mark - life cycle
- (instancetype)initWithResponseString:(NSString *)responseString requestId:(NSNumber *)requestId request:(NSURLRequest *)request responseData:(NSData *)responseData status:(CTURLResponseStatus)status
{
self = [super init];
if (self) {
self.contentString = responseString;
self.content = [NSJSONSerialization JSONObjectWithData:responseData options:NSJSONReadingMutableContainers error:NULL];
self.status = status;
self.requestId = [requestId integerValue];
self.request = request;
self.responseData = responseData;
self.requestParams = request.requestParams;
self.isCache = NO;
}
return self;
}
//在BasicAPIManager的successedOnCallingAPI:(CTURLResponse *)response中的代码片段
if (response.content) {
self.fetchedRawData = [response.content copy];
} else {
self.fetchedRawData = [response.responseData copy];
}
APIManager只负责保存从API过来的数据,如果上拉加载旧新闻的数据来源是从API来的,那也应该在每次成功调用API之后,把新的数据insert到原来的fetchedRawData里面。如果每次上拉加载旧新闻的数据是从本地来的,那么,就需要一个用于统筹的Mananger,来统筹管理LocalStorageMananger和ApiMananger,并由这个统筹Manager来交付数据(本地的和API的)。然后Controller直接操作这个统筹Manager,由这个统筹Mananger去根据需要来操作APIMananger或者LocalStorageManager。
现在有一个接口是通用的,只是传的参数(类似于网易新闻上面的分类,有N多分类,分类也是通过某个接口获取的)是不同的;我是创建N个APIManager呢?还是只创建一个APIManager呢,然后有个字段来区分是哪一类,在子类的paramsForApi:根据字段来动态的生成param呢?
APIService
demo中的APIService是神马作用呢?只是为了遵守协议返回apiBaseUrl吗?还是更深度的实现接口所需要的params?还有其他的用处吗?
最后一个就是关于reformer的问题了,按照我的理解reformer就是为了给viewController减负,或者对数据管理者减负;单独创建了reformor来处理数据,就像一个加工厂(像您在博客中提到的莲蓬头);不管进来的是神马数据,输出的都是view想要的东东。我理解的对吗?但有一个问题,如果这个reformer只处理一种数据,只输出一种数据,还有必要创建reformer了吗?如果创建的话,程序中会有越来越多的reformer。
跨层数据交流
其中说到“跨层数据交流”的定义或需求场景,如果是:A->B->C(这三个都是控制器,依次push)当C有数据变化需要回调给A,这样算不算“跨层数据交流”。
cas
- 三个ViewController这样的情况的话,应该还算不上跨层,毕竟都在View层。
- 但一定是跨了什么东西,毕竟从C到A了,就叫他跨对象好了。一般来说,要尽量避免这个情况,如果将来要迁移C的话,B和A就都要动到了,情况就比较复杂了,最好重新设计一下。
other
像这样的场景或需求,通常的做法是Notification,这是比较直接可行的,但Notification带来的是耦合度高、依赖关系强,还有的做法是单例做数据保存和引用。博主对这样的方法有何建议或看法。
cas
我觉得如果出现了这样的情况,第一个要考虑的应该是:这里是不是设计有缺陷,或者有没有办法通过设计来避免这种情况的发生,这也是我建议你去做的。
其次才是考虑处理这个场景的解决方案。如果确认设计没有缺陷,或者无法通过设计的手段来规避这种情况,这时候我们再来看解决方案。
其实剩下能用的手段就只剩下Notification和block以及你所谓的单例了。比较简单不容易出错的手段是Notification,我也建议使用这个。虽然为了处理这样的情况,这几种手段都不太好,但是Notification是缺点相对最少的一种手段了
队列请求
看了demo后发现每个继承APIBaseManager的子类发送的api请求是排队发送的,不支持并发同时请求多个api。
cas
因为2G标准下,一次只能有一个活动链接,如果有多个活动链接,多余的链接就会超时。3G下可以有两个,4G和wifi无限。因为有这样协议级的限制,所以链接必须排队出去。但是是可以根据网络类型的不同而选择是否支持并发请求。
DNS优化
由于我们的服务端拆分成一个个独立的,来服务客户端(iOS、android)。他们是通过不同的IP提供服务。拆分是为了实现3个目的:
- 拆分后服务是不同IP提供的,不会出现原来只有一个IP提供服务,一旦挂掉所有服务就全挂掉的问题(至少保证部分功能是可用的) .
- 想要实现动态更新服务端的服务(就是更新IP)
- 直接通过IP访问可以提高访问的速度,然后目标2的实现,就涉及到客户端。方案是:客户端要求本地存储所有服务器的IP地址,这里还不紧紧是IP地址,直接是全路径,ip+方法名,本地先默认配置这张我们称之为"路由表"的默认数据,然后每次根据策略从服务器同步这张"路由表"。关于提高访问速度,我大概知道CDN服务可以提高网络访问速度,以及博主文章中提到的多个ip,每次可以选择ip延时最小的方案都可以解决。而另外两个目的,我觉得是服务端的稳定性问题,而这个问题需要让客户端来承担,我觉得是不是有些不合适呢?老板给我的说法就是要去中心化,服务端那里是个中心,博主,你说"路由表"这个方案是否可行呢?是否有必要同步所有服务的IP地址呢?而且这样客户端处理起来也有很多问题。特别是调试的时候。
cas:
CDN确实可以提高访问速度,但提高的只是静态资源的访问速度,最多再加个动态资源的静态缓存。再说直白一点,CDN上一般都是图片啊,HTML、CSS、JS之类的东西,这些对于提高API的访问请求是不太能够起到作用。的CDN 提高可缓存的资源的访问速度,API调用很多是不能缓存的。
现在这种做法是有问题的,这个路由表不应该在客户端维护,应该在服务端维护。具体的做法应该是这样:
你们这里的服务端目前是多个IP,但是多个IP分管不同部分的API实现。对于客户端来说,不应该关心具体哪个IP分管哪个API实现。当客户端的API请求到达某个IP时,如果这台服务器发现这个请求自己并没有实现,就应该查表然后将请求转发到有实现的IP上去。
这么做的目的在于:
如果服务端出现IP维护,一台机器需要下线,然后一台临时机器需要上线,这种做法有可能带来IP的改变,但这种短时的改变很难让客户端同步。客户端需要的这项服务只认我这个服务器,那我就不能休息啦?
客户端对于IP的选择会更多,如果是IP+API这种,客户端对于IP的选择更少。有的时候客户端链接最快的IP,并不一定有它对应的API实现,如果用原来的方法,就不得不去链接那个慢的IP但同时有这个API实现。如果用我的方法,客户端可以不用链接那个慢的IP,服务器内部做请求转发是非常快的,做请求转发耗时肯定远远小于慢链接的耗时。所以整体上性能会有比较好的提升。离你近的人不一定满足你想要的,你只和符合你男朋友标准的人做朋友,那你选择太少了,你可以选择和基本靠谱的人交朋友,他会给你介绍符合你要求的男生的
客户端如果存储了路由表,路由表的数据同步就会成为问题,客户端不可能短期轮询更新路由表,只可能长期轮询(比如每天的第一次启动时查询一次)如果服务端在更新间隙重新作了部署,必须要等下一次轮询才能同步路由表。但如果只是IP列表,数据一致性所导致的问题就显得不那么重要了,一个IP不能连,大不了换另一个,所有IP都不能连出现的几率是很小的。所以我觉得有路由表没问题,但是不要放在客户端,应该在服务端做这个事情,如果服务端接收到了一个无法处理的请求,它再去查路由表然后转发请求。
errorMessage的讨论
baseManager是不会去设置errorMessage的,派生的子类manager可能需要给controller提供错误信息。所以为了统一外部调用的入口,设置了这个变量。派生的子类需要通过extension来在保证errorMessage在对外只读的情况下使派生的manager子类对errorMessage具有写权限。
other
父类的errorMessage是不是没有赋值的意义了,所有的errorMessage都是由子类赋值,而且父类的errorMessage变成nil。
cas
子类和父类是同一个内存,所以errorMessage也只有一个,不存在子类赋值之后,父类的errorMessage是nil的情况。所以父类只是告诉外面我们都有个errorMessage而已,具体的值是子类根据业务的需求不同,而赋不同值的。
token
当请求的manager返回token过期的错误后,是要调用loginmanager 吗?难道要在每个controller里都加上loginmanager? 如果APImanager直接操作loginmanager又感觉很乱,实在是不知道怎么组织这个刷新token的逻辑。
cas
这个可以通过interceptor拦截错误回调,然后再单独针对token失效这种情况做处理。一般的处理逻辑都是唤起登陆页面让用户登录~
文件上传
baseApiManager没有处理文件上传的功能,要每个子api自己扩展吗?
cas
当初设计APIMananger的初衷是为了调API用的,文件上传的话需要用FileUploader,这属于另一种实现了。
日志
在RTAPIBaseManager中hasCacheWithParams中,为什么在在主线程中执行日志记录和回调?
cas
其实日志记录在实际工作的时候还是异步的,回调放在主线程也是为了方便如果有UI的改变。这些操作其实都是瞬间的,所以我认为放在主线程问题不大。
您这套架构怎么和离线数据相结合呢?
cas
离线数据其实是交由持久层去做的。当有使用离线数据的需求的时候,你会有一个业务Mananger,它负责调度两个Mananger,一个就是本文提供的APIMananger,另一个就是持久层提供的LocalDataMananger。
others
那是否一个APIMananger对应一个LocalDataMananger呢?如果数据从LocalDataMananger里读取的话,reformer的作用还有那么明显吗?
cas
看需求,一般来说大部分的APP中,LocalDataMananger会是个单例,一个库对应一个Mananger。
其实数据都是从业务Mananger中取的,然后业务Mananger根据情况选择从APIMananger中取或者从LocalDataMananger中取。所以此时的reformer是针对那个业务Mananger生效的,不影响它原本的作用。
ohters
有一种情况原始数据存在APIManager中,但这个原始数据会用在很多地方,那是设计一个通用的reformer?还是设计很多reformer?如果有很多个reformer,那就意味着要解析很多遍原始数据?
cas
看情况。如果希望输出的数据完全是不同形式的,那就设计多个reformer。如果只是希望输出的数据用在不同的地方,那么其实只要输出数据的格式唯一就可以了,此时就不需要多个reformer。
others
从APIManager取出的数据肯定是网络获取的原始数据,这个在文章里已经讲到了。但从LocalDataManager取出的数据就有可能是Model了?那这个时候还有必要使用reformer吗?还是直接拿着Model直接使用?
cas
哈哈,肯定还是要经过reformer的。虽然取出的数据是model,但还是会经过reformer去取的。reformer的目的在于提供View能够直接使用的数据。只要取出来的数据不能够被View直接使用,不管是model还是JSON String还是NSDictionary,都要经过reformer才行。那意思就是本地的model其实是保存了API调用返回的所有json的数据,而不是特定业务需要的数据?
Demo中的cache都是保存在内存中的吗?图片资源也可以保存在内存中?
cas
是的。这个可以根据需求调整,对于用户平均使用时长少的APP来说,存内存就够了
看图片的作用了,临时性的就存内存,长久的就存本地
上一篇文章里提到MVC,与MVVM。我的理解是V层作为一个观察者去观察M的数据并做呈现。现在这篇文章中说到使用reformer转化API数据返回NSDictionary。这样的构架中数据层Model在哪里,还是已经弱化了这个概念
正常的数据流向应该是Controller从Mananger这边获得数据然后交给View,而不是View作为Model的Observer,View和Model之间不能有这么紧密的联系。
reformer事实上是弱化了model的概念,这么做能够使得代码维护更加灵活.数据变了怎么通知的呢?哪里的数据变了?APIManager中的,还是LocalDataManager中的
关于refomParams
关于使用reformParams方法中,在根据参数区分API返回逻辑的APIManager中使用reformParams这一条中的解决方案。解决方案是"针对不同的执行逻辑派生不同名字的APIManager,然后在reformParams里提供决定执行逻辑的参数",这里有些不是很明白,如果派生出不同的APIBaseManager,比如根据type分别有3种结果,那么分别派生3个不同的APIBaseManager,那么这样三个APIManager里的参数都是重复的,直接在paramsForApi方法里直接写全,那就用不到reformParams方法了呀(而且这里,相同的参数重复几遍)。所以这里,我理解的派生出不同名字的APIManager是根据其中一种type类型派生,比如SearchPersonHousesAPIManager,然后剩余两种type是派生自SearchPersonHousesAPIManager这个类,然后这个时候可以使用reformParams,来区分type,不知道博主,我这里的理解是否是对的?
cas
不太对。reformParams的这种解决方案,其中之一的目的,是为了解决API开发者这边为了偷懒,把同一类型不同返回的几种API通过type来区分,最终放到同一个API调用里的情况。
再说详细一点,场景就是:有一个API会返回两种数据,比如“附近的列表”、“符合某些筛选条件的列表”这两种数据通过同一个API来返回了,然后返回这两种数据的区分,是通过传入type参数的不同来区分的。
因此,在使用reformParams时,需要新建不同的APIManager,因为我们之前的约定是,一个API对应一个Mananger,现在生成不同的Mananger之后,虽然methodName之类的设置还是一样的,但最好还是不要派生自已有的Mananger,重新写一遍最好。因为在这种情况下,其实不同的type就是不同的API了。
彻底分开来的目的是,将来如果API这边重构,把这一个不合理的API拆成两个之后,你要做的事情就只是改改参数而已。如果你通过继承来做这两种API的区别,API重构之后,你就很蛋疼了
关于RTAPIManagerValidator验证输入参数的非法性
others
Demo中验证输入参数的合法性是在loadData中做的,这里一般不是在controller里,在代用loadData之前先验证的吗?
cas
传统的做法确实是把参数验证的事情放在controller里面做的。但由于现在已经有了一个大前提,一个API对应一个Mananger,所以针对特定API的参数验证,就可以放到这个特定Mananger中去做。当参数验证不通过的时候,会走到error回调,然后对应的处理就都放在error回调中处理了。这样做的好处是,一方面将来如果API迁移,或者有多个地方调用了这个API,参数验证的事情就不用写两遍了。另一方面,针对错误处理的代码就能够集中到错误回调的方法里去了,以后只要发现有错,就只要在那个方法里面下断点就可以了,特别好维护。
others
这里RTAPIManagerValidator的delegate一般设置为APIBaseManager,在APIBaseManager里写验证参数合法性的方法,一般不合法是需要提示的,可以直接在这里写提示的吗?
cas
不要在APIMannager里面做提示,真的要写提示,就把提示写在errorMessage里面,然后在回调的方法中由delegate的实现者决定是否要展示,Mananger也没有展示错误信息的权利,它只需要负责生成错误信息交付出去就好了。如果你把弹框展示错误信息的代码写在Mannager里面,当遇到同一个Mananger在有些情况需要展示,有些情况不需要展示的时候,就很蛋疼。
关于缓存,这里应该涉及本地缓存
others
Demo中缓存设置为5分钟,而且是存储在内存中。但是我们现在要去所有页面现实方式是:先显示缓存,再通过网络请求新数据,获取数据再刷新以及更新本地数据。对于这样的要求,这里是不是就没有必要使用内存的缓存了呢?
cas
这个涉及缓存策略,因为有的API的缓存策略是不允许缓存,比如查询余额这种操作。有些API的缓存策略是允许短暂缓存,比如某些列表,这个放内存就可以了。有些API的缓存策略是允许长期缓存,比如详情页面,这种就可以考虑把缓存持久化。
所以我们要根据不同API的要求去采取不同的缓存策略,安居客暂时没有可以长期缓存的需求,所以没有在demo里面给出缓存持久化的功能。
另外一点,缓存策略其实也影响请求策略。你说的先取缓存数据再发送API请求更新数据,这只是其中一种请求策略。另外一种请求策略是如果缓存存在,就直接展示缓存而不发起API请求,安居客更多的是后者,前者基本没有实际场景。
所以在这边你要改造的时候也要把这些可能都区分开来,考虑进去,不能一概而论。
问题四、关于拦截器
others
大概了解了一下AOP,知道了AOP可以做日志记录、性能统计、异常处理等等,但是这些具体怎么做还不知道,还要再学习。这里想问博主的问题是,博主PPT中提到的网络等待状态的切换,这里和文章中提到的跨城数据交流中的信号从2G变成3G变成4G变成Wi-Fi,这里切换状态有什么用?要如何用AOP来做切换?
cas
业务层需要知道网络状态的改变主要是为了采取不同的策略。比如有些视频应用,就提供用户去设置只允许在Wi-Fi环境下缓存视频,所以当网络切换的时候,就需要弹框告知用户当前可以缓存或者当前不能缓存了。
然后这里并不是通过AOP去切换的,这里扔通知出去就可以了。AOP在上篇文章中,是作为继承的替代方案提出的。
ohters
一般都有这样的需求,请求时发现没有网络时,直接提示"没有网络连接",这里要如何统一添加处理呢?可以直接在APIBaseMananger里判断提示,还是是要使用AOP来做呢?
cas
这里可以通过AOP来做,在拦截器里面拦截这种情况然后统一处理就可以了。
others
这里由于我不知道后面要怎么用AOP的方法,具体能做什么,所以我想去掉这个不部分,博主我可以先去掉这一部分吗?还是说先把AOP添加到APIBaseManager中?
cas
你就留着好了,这个功能如果你现在用不到可以不用,万一将来要是用到了那不还是得加回去。
另外,上一篇文章中的AOP是通过runtime的method swizzling实现的,AOP的实现方案也有很多种,比如这篇文章提到的拦截器方案,在这种方案下,你可以把拦截器理解成一个event delegate,只是在某段事情开始,和结束时,把这个事件切点扔出去而已,实现和不实现具体的切片内容,是无所谓的。所以你用不着的话就可以不用,但别删,万一要用再加上就很蛋疼。
关于CTService以及CTServiceProtocal
others
这里相关字段不是很理解用处?首先isOnline,这里为什么要判断是否在线呢,如果不在线,提供的offlineApiBaseUrl提供什么地址呢?网络断了,这个地址怎么能起到作用呢?
cas
这里的isOnline其实是用来区分是线上环境还是线下测试环境的,不是说有没有网络的意思。不同的环境下,调用API时使用的参数也是不同的,所以有isOnline来做区分。
others
onlineApiVersion、onlinePublicKey和onlinePrivateKey分别有什么作用?从Demo我理解的是加密的时候用到的。具体是SignatureGenerator方法用到的。(1)这里为什么要用这么多参数呢?(2)然后在生成Get请求的方法中,给参数多传个publicKey参数,这里有什么用?
cas
同上...
关于AIFAppContext和AIFCommonParamsGenerator
others
为什么这里的字段定义不写全名,很难理解?有些字段即使看源码也不是很明白,比如channelID为什么写的是A01?
cas
这些参数其实是每个公司都不同的,所以都是按照公司自己的习惯去命名的。channelID就是渠道号,A01的渠道号在安居客就是app store的渠道的意思。
others
为什么AIFAppContext提供两个初始化方法,如图。这里AIFAppContext的数据都是内部获取的吗,是否会有一些数据是需要外部传入的
cas
一个是实例化用的方法,就是sharedInstance,另外一个是设置配置的方法,这两个方法作用是不一样的。另外,原则上AppContext是自己解决这些参数的,不过有的时候也要靠外面来设置,比如定位相关的信息。
others
这些公共的参数在请求时需要,这个是你们接口要求的吧。我看了我们的接口,貌似公共的参数很少,差不多有AppId,UserId,差不多就这两个。(额,UserId有的时候也是不需要的,那就只有一个了。)我要是添加CommonParamsGenerator的话,就只有一个了。我想把博主这里的AIFAppContext中的字段添加进来,不过我有个问题,就是这些字段,服务器获取用来做什么呢?
cas
这些公共参数是用来做数据挖掘的,比如要是想知道哪里的用户比较多,那就要传递一个坐标参数,比如要是想知道某个渠道的用户数量有多少,渠道推广的效果如何,就需要带上渠道相关参数。有的时候想要知道特定用户他们的操作路径,那就需要传递用户的标示号。所以传递这些参数给服务器,是方便做数据挖掘用的。
uuid?
为什么单独写个AIFUDIDGenerator以及UIDevice (IdentifierAddition),这个看起来有些复杂,主要用来做什么的?
cas
这个就是用于区分用户的UDID,因为苹果后来不允许获取设备ID了,所以我们自己生成一个用户ID,然后存到keychain里面去,这样即便app卸载再重装,keychain里面的数据还是不会变的,除非重装系统。
关于AIFLogger模块
用户行为记录?
cas
这些就是用户操作日志,比如他点了一个按钮,就会调这个方法告诉服务器这个用户点了这个按钮,这样产品经理就能够通过用户行为分析用户使用app的思路,进而通过这些数据来改进app。
打印日志
除了打印请求、返回数据、缓存,那网络层中其他地方,打印可以用NSLog打印吗(NSLog设置宏定义只在debug下打印)?如何制定打印的统一规则呢?哪些是必须要打印的,打印的方式是什么?博主的项目是如何规定的呢?
cas
一般是我们自己定一个宏,本质上还是通过NSLog打印的,自己定义的宏能够打印更多的信息,比如代码行号,文件名啥的。
关于翻页APIManager
PPT中提到”外面第一次加载的时候调用loadData,以后每次翻页,只要调用loadNextPage就好”,这里我要如何判断是翻页的时候,来触发调用loadNextPage呢?
cas
触发翻页的事件跟加载第一页的事件肯定是分开的。比如下拉翻页或者点击下一页这种事件,就触发了loadNextPage。在viewDidAppear的时候,加载第一页用loadData就可以了
用拦截器写的参数验证方法
关于问题二2的验证参数并需要提示问题,我试了一下。具体做法是:拿登录模块为例,验证手机号不能超过11位。我在GTILoginManager中的验证方法判断并且设置errorMessage。
//验证params
- (BOOL)manager:(GTIAPIBaseManager *)manager isCorrectWithParamsData:(NSDictionary *)data {
if ([data[@"PHONE"] length] > 11) {
self.errorMessage = @"手机号不能超过11位";
return NO;
}
return YES;
}
//然后在delegate的失败代理方法中处理,代码如下:
- (void)managerCallAPIDidFailed:(GTIAPIBaseManager *)manager{
if (manager.errorType == GTIAPIManagerErrorTypeDefault) {
//返回数据失败处理
} else if (manager.errorType == GTIAPIManagerErrorTypeParamsError) {
//验证参数失败处理
NSLog(@"errorMessage:%@", manager.errorMessage);
//提示errorMessage
} else if (manager.errorType == GTIAPIManagerErrorTypeTimeout) {
//请求超时处理
} else if (manager.errorType == GTIAPIManagerErrorTypeNoContent) {
//请求成功,返回数据不正确
} else if (manager.errorType == GTIAPIManagerErrorTypeNoNetWork) {
//没有网络处理
}
}
这里失败的回调方法需要根据errorType进行类型判断的吧。是否可行呢?
cas
可行,就是这么做的。
统一处理没有网络的问题
GTILoingManager,继承beforePerformFailWithResponse,在该方法中拦截。代码如下:
- (void)beforePerformFailWithResponse:(GTIURLResponse *)response
{
[super beforePerformFailWithResponse:response];
self.errorMessage = @"您的网络不太顺畅喔~~";
//疑问这里要设置errorType为GTIAPIManagerErrorTypeNoNetWor吗?
}
后面会走到managerCallAPIDidFailed方法。
这里有个问题,就是我在
-(BOOL)manager:(GTIAPIBaseManager *)manager isCorrectWithParamsData
中设置了errorMessage.
然后在
beforePerformFailWithResponse
中设置errorMessage.
这样之前的errorMessage就会被后面的覆盖。然后上面代码那里有个疑问,就是是否要设置errorType,如果不设置,那么errorType类型就是参数不合法,而errorMessage就是"您的网络不太顺畅喔~~",这里的拦截是不是不能在这里做?还是我应该用外部拦截做这个呢?
cas
errorType可以不用设置,因为所有的错误在正常情况下,都会被正确设置的。你拦截下来的情况只是为了达到统一设置errorMessage而已,所以没必要再去改errorType了。
others
拦截代码中应该加上对errorType的判断进行设置errorMessage吧?如下:
- (void)beforePerformFailWithResponse:(GTIURLResponse *)response
{
[super beforePerformFailWithResponse:response];
if (self.errorType == RTAPIManagerErrorTypeNoNetWork) {
self.errorMessage = @"您的网络不太顺畅喔~~";
}
}
否则当出现参数验证不通过时,errorMessage会在拦截方法中被重写为@"您的网络不太顺畅喔";最终导致业务层获取到的结果就是 errorType是RTAPIManagerErrorTypeParamsError,而errorMessage是@"您的网络不太顺畅喔"
代码
- (NSArray *)CT_transformedUrlParamsArraySignature:(BOOL)isForSignature
{
NSMutableArray *result = [[NSMutableArray alloc] init];
[self enumerateKeysAndObjectsUsingBlock:^(id key, id obj, BOOL *stop) {
if (![obj isKindOfClass:[NSString class]]) {
obj = [NSString stringWithFormat:@"%@", obj];
}
if (!isForSignature) {
obj = (NSString *)CFBridgingRelease(CFURLCreateStringByAddingPercentEscapes(NULL, (CFStringRef)obj, NULL, (CFStringRef)@"!*'();:@&;=+$,/?%#[]", kCFStringEncodingUTF8));
}
if ([obj length] > 0) {
[result addObject:[NSString stringWithFormat:@"%@=%@", key, obj]];
}
}];
NSArray *sortedResult = [result sortedArrayUsingSelector:@selector(compare:)];
return sortedResult;
}
cas
这个其实就是把URL带的参数里的特殊字符转义了一下。在签名的时候用的是未转义的字符串,不签名直接作为URL使用的时候要转义一下,疑问。
HUD
每个请求对应一个RTAPIManager子类, 但这时HUD该如何添加呢? 每个RTAPIManager子类都要添加吗?
cas
可以写一个通用的拦截器,去拦截你需要挂载的HUD调用点。
一定要着陆的API调用
使用delegate的方式的话,如果有一个数据更新的业务,app发送更新的请求,还需要等待请求返回的状态去更新本地的数据,而在返回之前,由于用户退出了这个界面,RTAPIBaseManager的子类有可能已经给dealoc了,这是返回的状态就无法得到了。那应该将RTAPIBaseManager的子类放到哪里为好?
cas
那你应该选择好这个API的持有者,如果这是一个需要持续的API,那么这个API的持有者也应当具有持续性。
比如你的API是属于全局API,在当前页面发起请求之后拿到数据需要存数据库,即便当前页面退出也不影响这样的逻辑。那么这样的API就不应该由这个页面去持有,应当由你的DataCenter去持有。
ohters
也就是说,我在DataCenter(可能存在多个持续性的业务)中还必须去管理每一个API的实例,每一次的返回,根据不同的实例去调用delegate。是这样吗?
cas
不是,这种持续性的API,DataCenter就是它的Delegate了,所以叫它持有这个API。
others
那如果我有多个不同业务的持续性的API,那也就是对应的也要有几个DataCenter作为API的Delegate。可以这么去理解吗?
cas
不是,这种持续性的API,DataCenter就是它的Delegate了,所以叫它持有这个API
调用
关于feformer,我如果这样做行不行。View定义了自己的dataSource(protocol),然后reformer实现这个datasource,从而完成raw data -> reformer -> data source的adapter模式。按照你的设计,reformer在返回Dictionary的时候,使用者是通过key来取value的,这里的问题是类型弱化,在取value的时候,你必须要看文档来确定value是个什么类型,然而用dataSource则可以强化类型。
关于Block和delegate之争,我是这么看的。Block要想用好,对使用者要求比较高,然后在表达业务逻辑的时候是更直观和清晰的。异步的东西如果能用类似同步的方式来表达,应该是顺应大多数程序员的思维。
如果我想表达,在A做完之后做B,那么下面2种表达,你觉得那种更清晰呢
foo() {
A();
B();
}
foo2() {
A();
}
A() {
B();
}
cas
前者更清晰。
但对于foo函数来说,是否需要知道有B的存在,这才是需要讨论的地方。
others
如果A做完之后,会根据条件来决定是不是执行B,你觉得foo是否应该知道B的存在呢?
这个地方如果A的语义层级和B是同一层次,我觉得foo应该知道B的存在。
目前我接触的业务逻辑,一般用foo这样的方法就能表达清楚一个用例,是不是比较清晰呢?
cas
在foo需要知道B的存在的时候,你可以选择把A和B都放在foo,但是在一定程度上违背了“一个函数只做一件事情”的原则。还有就是,当foo不需要知道B的存在的时候,最好就还是后者。后者提供了更好的封装,在代码迁移的时候,动的面积最小。假设将来你需要把A拿掉换成别的,如果动到B,迁移成本就会很大。
另外,block和delegate的使用区分,并不是通过这个来区分的。区分的关键点在于:当你的回调内容做的事情在每一次都是涉及不一样的业务逻辑的时候,适合block。其他情况还是delegate。这里的区分条件请参考objc.io的《Communication Patterns》好看和好用是需要权衡的,如果将来不光有A和B,而是有了ABCDEFG,你还是会选择delegate的。
others
这里foo的职责就是调度A和B,比如门面模式,把step A, step B组织成一个大的step,然后供调用者调用。反而是体现了“一个函数只做一件事情”。如果foo里只有A的话,那它是没有存在意义的。
如果不光有A和B,有了ABCDEFG,而且随着业务的变动,ABCDEFG变成了DFEGA,用foo的方式我只需要改foo一个函数;但是用foo2的方式,要改几个函数我就不用解释了。
cas
如果你把需求细化到这里,那我也觉得可以做成block。但是在其他情况,还是要具体问题具体看待的。而且,在大多数的情况下,delegate的使用场景是比block多的,所以我建议优先选择delegate。
在本文的API回调中,由于单个API对应单个APIMananger,且APIMananger并不是作为全局实例去做的,而只是Controller持有的对象,这种场景所导致的API回调内容是固定且单一的,适用delegate。
token过期弹出登录界面
目前我们项目的网络模块采用离散型结构设计,里面有这样一个业务逻辑,帐号只能在一个设备登录,如果被踢掉token就过期了,这时要弹出提示,点击后就跳转到登录界面,这个针对的是所有接口,之前我是放在基类里处理response的地方(能很好的处理上面的逻辑),这样导致业务逻辑和网络模块混到一起了,所以我在想拆出来。不想每一个request的地方都手动加一个判断(我用的block做回调的),请问下有什么好的建议吗?
cas
我的建议是写一个通用拦截器,拦截回调方法,在拦截器里面做是否被踢出的判断,这样所有APIMananger的拦截器只要挂那个单独的拦截器对象就好了。
1、下拉刷新的时候,不管API调用成功还是失败都需要执行停止刷新的逻辑。这样就要在Success和fail都写一样的代码。类似的有一些不管调用成功和失败都需要执行的代码,要写2遍。。怎么弄好一点?
cas
对应业务API的子类中实现父类提供的拦截方法,例如afterResponse这样的,然后在这里面写停止刷新的逻辑。
others
1.已经在业务API中实现拦截方法,成功和失败都会调用。但是,停止刷新的逻辑中包含了相应的控件ScrollView,把ScrollView引到业务API中来不妥当吧。。在业务APIManager中写一个delegate,让viewController去实现?这样好像行。
cas
是的,拦截器拦截到那个点之后,把这个点delegate给业务方实现。
在单用户的时候,可以把相关的配置信息如uid,token,phone等存在NSUserDefault中,当要存多个用户的类似信息时,怎么存储好一点,存到plist文件?sqlite,建一个user表?
cas
这种东西需要存keychain的,然后以用户ID+私钥作为key,不需要你说的那么复杂。
others
为什么需要私钥?私钥是自己随便起的一个字符串吧?以userid+私钥为key,然后把@{@"name":name,@"sex":sex,@"token":token}这个dictionary为Value传进去吗?
cas
你把dictionary序列化成PropertyList或者JSON存入keychain,然后取的时候再转成dictionary
签名
有关签名,再你做完各种hash,得到最终的sig。别人还是可以抓包到你最终的sig,加到接口和参数后面,从而成功访问接口。为什么说这样就能确保是来自你自己的app呢。
cas
因为你签名的时候是加私钥的,私钥只有你和服务器知道呀。
others
服务器只会去判断根据私钥+方法名+参数生成的sig是否相同,而抓包直接就能抓到sig,传进去sig,就能访问接口了。 所以这要拿到该sig就能访问了,和私钥有什么关系
cas
是的,但这么做仅限于同一个参数同一个API,对于竞争对手来说抓取这样的sig是没有意义的。另外,一般都会带上时间,尤其是调用发送短信API时候,都会有请求时间的,请求时间超过1分钟的请求服务器是不处理的。
所以一方面请求参数有限制,另一方面请求时间有限制,这些限制对于抵御爬虫来说是足够了。
others
但是服务器和客户端的时间同步又是一个问题。判断请求时间是否超过一分钟的前提是 客户端和服务器端的时间是同步的。
cas
即便不同步,也不会差别到1分钟吧?
refomer的使用
看了下RTNetworking中的代码,其中有一个AIFCityListManager类继承自RTAPIBaseManager,把delegate、paramSource、validator都设置成AIFCityListManager自己,并且在这个类中做了一些数据处理的工作,不理解为什么这么处理。我的觉得应该是把controller作为delegate、paramSource,,然后把数据处理的工作放在reformer类中做。
cas
因为这个API涉及的整个业务都是弱业务,所以放在Mananger里面就可以了。弱业务尽量不要放到Controller里面去,Controller专注于强业务。
others
在AIFCityListManager中并没有调用loadData方法,但是最终把数据返回给了这个类,感觉怪怪的。另外,reformer这个类应该相当于viewModel吧?为什么不把cityWithLocation、cityLocationWithCityId这些事情放到reformer中做呢?
cas
reformer并不算是一个ViewModel,虽然行为上类似,但是reformer的定义并不需要那么严格,因为它只是一个数据转化逻辑,定义面狭窄就比较利于代码的复用。
这些方法其实涉及查询操作,查询操作跟数据转化操作其实是两种概念,查询操作不具备代码复用性,所以不适合放到reformer中去做。
怎样让请求超时呢?
我想测试static NSTimeInterval kAIFNetworkingTimeoutSeconds = 20.0f;这里设置的时间是否有效,不懂怎么弄呢?
cell中的同类网络请求
应该在哪里发请求? 请求完成之后如何找到对应的Cell?
请求完成后我如何找到对应的cell。比如第一个用户我没有关注。我点击关注,然后在控制器里进行请求。在请求前我是不是要保存当前cell所处的indexPath。否则请求完成后我就不知道更新哪个cell了。在tableViewCell中,点击某个按钮需要进行请求。如图。我要关注一个用户。请求调用应该写在哪。cell中,还是controller里。我现在在controller里调用1. 请求完成后我如何找到对应的cell。比如第一个用户我没有关注。我点击关注,然后在控制器里进行请求。在请求前我是不是要保存当前cell所处的indexPath。否则请求完成后我就不知道更新哪个cell了。
cas
- 在能够完成全部相关业务逻辑的主体中发送请求。就你这个需求来看,收到请求回调之后,你需要更新Cell界面,需要更新cell对应的dataSource中的相关标记(已关注or未关注),这些事情只有Controller能够完成。所以请求应当在Controller中发起。
- 你会看到NSURLRequest+AIFNetworkingMethods.h里面添加了一个requestParams,这样在得到response的时候,是能够获得request的,那么也就意味着能够获得对应的requestParams。你可以考虑在params里面带上indexPath.row,那么在response得到的时候,就也能拿到indexPath.row了。不过如果不嫌麻烦,最好还是再加一个UserInfo属性,跟这个RequestParam一样,跟着response一起带回来,你拿到response的时候就可以知道是哪个cell了。
//
// NSURLRequest+CTNetworkingMethods.h
// RTNetworking
//
// Created by casa on 14-5-26.
// Copyright (c) 2014年 casatwy. All rights reserved.
//
#import
@interface NSURLRequest (CTNetworkingMethods)
@property (nonatomic, copy) NSDictionary *requestParams;
@end
//
// NSURLRequest+CTNetworkingMethods.m
// RTNetworking
//
// Created by casa on 14-5-26.
// Copyright (c) 2014年 casatwy. All rights reserved.
//
#import "NSURLRequest+CTNetworkingMethods.h"
#import
static void *CTNetworkingRequestParams;
@implementation NSURLRequest (CTNetworkingMethods)
- (void)setRequestParams:(NSDictionary *)requestParams
{
objc_setAssociatedObject(self, &CTNetworkingRequestParams, requestParams, OBJC_ASSOCIATION_COPY);
}
- (NSDictionary *)requestParams
{
return objc_getAssociatedObject(self, &CTNetworkingRequestParams);
}
@end
先后点击关注第一个用户和第二个用户按钮,如何请求?
如果我先后点击关注第一个用户和第二个用户按钮。在Controller里我只有一个apiManager。我该怎么去同时请求关注这两个用户。
cas
请求还是正常请求的,其实关键问题还是在于区分拿到的请求是第一个用户还是第二个用户,用2的回答就能解决了。
请求参数字典添加了对请求无用的参数。会不会有些不好。
cas
所以我前面回答里说你可以用一样的方法添加一个userInfo属性。如果嫌麻烦,而且数据量小,直接写到params里面也不会影响服务端业务的正常执行。
response我看到只能在拦截器的方法里获得。这样Controller还要实现拦截器协议。而且请求成功的处理放在了两个地方。一个managerCallAPIDidSuccess:获取数据。第二个在manager: afterPerformSuccessWithResponse:方法里取到对应的indexPath.
cas
关于参数组织的问题,现在请求参数都在paramsForApi:获取。这样有些参数字段我还需要专门建一个属性来保存。像上面3点说的。在Controller里的cell的代理方法里。两个indexPath我该怎么保存以便于paramsForApi:获取。在其他请求时也会纠结于一个在某个方法里生成的参数键值需要传递给paramsForApi:获取的问题。
cas
。。。
cell中reformer反复洗了数据
最开始的时候,我在reformer里面的ATAPIManagerDataReformer协议方法中,对获取的数组进行遍历,每个元素(字典)采用固定字符串作为key,比如:
- (id)manager:(APIManager *)manager reformData:(NSDictionary *)data {
NSMutableArray *result = [NSMutableArray array];
NSMutableDictionary *singleResult = [NSMutableDictionary dictionary];
for (NSDictionary *dic in data) {
singleResult[keyAlertId] = @([dic[@"id"] integerValue]);
singleResult[keyAlertTime] = dic[@"time"];
[result addObject:singleResult];
}
return result;
}
由vc在managerCallAPIDidSuccess:方法里面调用fetchDataWithReformer:获取reformer洗过的数据并组合成dataList(分页),然后在cellForRowAtIndexPath:方法里面取dataList的第indexPath.row个数据对cell进行赋值.
这样做是可以通,但是有两个疑问:
1.1 还是使用了遍历数据的方式, 没有做到您在有一个评论里面说的”用到的时候再洗不就不用遍历了嘛”.
1.2 似乎没有达到您文章里面说的:”reformer是要提供给业务层的view直接可以使用的数据”,比如我在这里直接使用的应该是一个cell,但是我还是给了一个字典拿去给cell用.
2.接下来我看到了把reformer作为tableView的dataSource的方法,我只是单纯的把dataSource协议拿给reformer去实现,dataList改为reformer去持有(1里面是vc持有),也是可以通,但是这样就和做法1没什么区别,还是会有1.1和1.2两个问题.
3.我觉得我这个reformer应该是为cell服务的,所以它应该只是要针对数组里面的每一条数据的.所以,这次我没有对数据进行遍历洗牌,而是直接把获取的数据直接组合成dataList让reformer持有,实现效果是这样的:
在reformer中:
- (id)manager:(APIManager *)manager reformData:(NSDictionary *)data {
NSMutableDictionary *singleResult = [NSMutableDictionary dictionary];
singleResult[keyAlertId] = @([data[@"id"] integerValue]);
singleResult[keyAlertTime] = data[@"time"];
return result;
}
然后在cellForRowAtIndexPath:方法(现在也是在reformer文件下面了)里面:
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
LocationListCell *cell = [LocationListCell cellWithTableView:tableView];
NSDictionary *singleResult = [self manager:nil reformData:self.alertList[indexPath.row]];
cell.group = singleResult;
return cell;
}
但是这样的问题是:
3.1 洗过的数据还是会不停地被洗(cellForRowAtIndexPath:方法会不停被调用)
3.2 vc中没有调用fetchDataWithReformer:方法也不能直接获取洗牌后的数据(有时候vc里也会需要用,比如根据每条数据的id进入下一页时候进行请求)
3.3 还是没有达到reformer直接吐出cell的目的
- 我在3的基础上直接在manager:reformData:方法中直接吐出cell,并在cellForRowAtIndexPath:方法中使用,但是这样和3也还是没有本质区别.
综上,我觉得我始终没有觉悟出列表数据使用reformer规整的正确方法,能否请博主指点一二?
cas
用到的时候直接根据IndexPath取到对应的数据了啊,这时候直接丢到reformer里面去洗就不用遍历了你可以让你的DataSource去conform那个reformer的protocol,那不就能生产出cell来了你洗好的数据直接塞到reformer的property里面,key为IndexPath,下次直接取
取id的事情直接问reformer拿就可以了,因为reformer持有所有数据。vc要跳转的时候,把indexpath给reformer,reformer就吐出对应id了
串行请求
三个请求做成三个Command,我理解的也是三个Command就是三个APIManager,你的回答不是,这一点我有点没太懂,那这三个Command具体是什么,实现了CTAPIManagerCallBackDelegate的类?
cas
是的,不光只是是实现callback协议的三个command类,他们的基类都是同一个command,这三个command的组织结构跟单链表的node很像,都有next和payload,以及对应apimanager。command的的基类就是维护了一个单链表的结构,其他的具体要怎么做就是就是有持有的APIManager来做了
BaseCommand还要提供execute方法。
然后Command之间是不能用delegate去联系的,只允许用next去联系。
所有Command的delegate都应该是vc。
Command自己是APIManager的delegate,在接收到APIManager的成功的回调的时候,再执行[self.next execute],直到self.next 为 nil时(最后一个Command的next不用赋值,默认就是nil了)为止。
ProfileCommand *profileCommand = [[ProfileCommand alloc] init];
profileCommand.delegate = self;
profileCommand.apiManager = profileAPIManager;
LoginCommand *loginCommand = [[LoginCommand alloc] init];
loginCommand.delegate = self;
loginCommand.apiManager = loginAPIManager;
loginCommand.next = profileCommand;
[loginCommand execute];
并行请求
cas
VC使用OperationManager直接把任务参数丢过去,然后一个Operation中记录请求数(atomic),同时启动多个请求,每成功一次请求数-1,到0时通知用户提交成功。
reformParams
用于给继承的类做重载,在调用API之前额外添加一些参数,但不应该在这个函数里面修改已有的参数。子类中覆盖这个函数的时候就不需要调用[super reformParams:params]了
CTAPIBaseManager会先调用这个函数,然后才会调用到 id
如果在这个函数里面将page_size改成10,那么最终调用API的时候,page_size就变成10了。然而外面却觉察不到这一点,因此这个函数要慎用。
这个函数的适用场景:
- 当两类数据走的是同一个API时,为了避免不必要的判断,我们将这一个API当作两个API来处理。(服务端为了偷懒)
- 那么在传递参数要求不同的返回时,可以在这里给返回参数指定类型。
token
一般在调用需要登录态的API时,我们都是带token,当token失效的时候,保留当前请求,然后发起刷新token的请求,token刷新成功之后,把保留的请求再发出去。如果token刷新失败,那么就弹登录框。
在发起刷新token的请求之前,会在本地置一个“刷新中”的状态,所有后续登录态相关的请求都会去check这个状态,如果是刷新中,那就进队列。刷新成功和登录成功之后,再把这个队列的请求发出去。
others
关于 token 的问题,有个问题请教下:
如果 APIManager 请求需要使用到 token,并把 token 放在 HTTP 的 HEAD中,而有些又不需要,那么是应该修改 generateRestfulPOSTRequestWithServiceIdentifier 接口,增加判断的参数? 还是有其他更好的办法吗?
cas
你把所有不需要token的APIManager归为service1,需要token的APIManager归为service2就好了。
或者你直接给BaseManager增加一个参数,shouldUseToken,生成request的时候去check这个shouldUseToken,派生的APIManager负责设置这个shouldUseToken。
嵌套调用
博主如果嵌套调用apiManager的话。A成功后调用B B成功后调用C ,是不是只能在managerCallAPIDidSuccess 依次判断 manager == A 就调用B loadData 然后manager == B 就调用C loadData 么
cas
常规的嵌套调用的做法一般都是在AfterSuccess方法里面做的,你这么做也可以。
AfterSuccess可以走外部拦截也可以走内部拦截,具体取决于你的业务。
others
我的理解是 如果这几个api是强业务关联的话用内部拦截,弱业务的话就用外部拦截,这样以后嵌套的api变了,只要替换外部拦截对象
cas
是的
网络状态信息
有上传视频和上传音频这个业务,而且要在不同界面显示进度条,请问你有什么好的方案吗?
cas
写OperationQueue,然后progress让block返回~
others
有一个页面的控制器会被Pop掉重新创建,有一个页面不会,用block强引用就pop不掉控制器了?
cas
不会
你的OperationManager做成单例,然后block外用__weak,block里用__strong
others
博主您好,这样做还是得等到block执行完才能释放控制器是吧?
cas
你这个block里面的控制器不是那个被pop的控制器,是外面的那个
others
不是很明白,我block回调进度给控制器,block里面怎么不是被Pop掉的控制器呢?
cas
你那个控制器都被pop了,那就很明显不是它去关心progress啊
others
我再次创建被pop的控制器的时候,要重新显示进度条,不是很明白block怎么做到?我现在是用代理做的。
cas
你pop的控制器还会再次被push回来?如果是这样的话肯定不能block,你之前怎么没说?
你现在把你整个流程完整说一遍,我再给你方案
others
非常抱歉,刚刚回到家。业务逻辑是这样子的,有两个模块,一个模块是tabar的子控制(挂不掉),要显示上传进度条。另外一个模块需要显示进度条的控制器的nav的子控制器,可能随时会被pop和push的,我现在的做法是写了一个管理类,分别用代理和block回调进度。想请教博主有什么好的见解?
cas
这种的progress还是统一走notification吧,你代码里面又用代理又用block不好管理。operation统一扔progress的notification,带上标识符就好了。这样任何关心progress的对象就都能用,将来也容易拓展。
others
好的好的,非常感谢。我的安卓兄弟用的是这种策略,不过我听别人说iOS的通知比较耗性能,我很少用通知传递数据。而且我的业务逻辑是点击tableView的Cell(会有很多cells) push进去一个控制器,这个控制器显示进度条。
cas
这个性能耗不了多少的,你又不是几百个observer~
这跟你怎么push控制器没关系,控制器初始化之后就监听notification,随便你怎么push都没关系。
APIManager的管理
如果一个vc有多个请求,用delegate如果去区分返回的数据,博主说”如果你把业务模块拆出vc,那vc就不用判断了,各自业务模块直接调度就好了“,还是有点不懂,用什么去进行区分
cas
拆出之后,业务模块直接作为apimanager的delegate,而发起请求仍然不影响在controller中发起。其实这个和串行command作为APIManager的代理,vc持有command的结构有点像
others
虽然json统一由reformer转化成所需的数据,但是如果要使用的数据是数组(比如说是cell数据),内部还是使用循环遍历,这样的话感觉和model没差。(当然啦,我有仔细看去博主的去mdoel化文章)
cas
传统做法的话,无论是否去model化,遍历洗数据都是不可少的。但如果你独立出datasource之后,让这个datasource去conform reformer 的protocol,在controller中作为reformer去使用,然后hold住数据。每次取cell的时候洗数据,就不需要遍历了。之前的reformer只相当于是一个过滤器,现在的reformer是一个带有过滤器功能的,能够存储干净数据的容器了
cache
关于缓存policy NSURLRequestCachePolicy不是已经实现好了么,直接用NSURLRequestUseProtocolCachePolicy, NSURLRequestReloadIgnoringCacheData, NSURLRequestReturnCacheDataElseLoad, orNSURLRequestReturnCacheDataDontLoad这些policy就可以了吧,为什么还要自己缓存?
cas
有的时候服务端要求客户端发起请求时,要在header里带UUID以区分不同的request,这时候request就不会被系统默认缓存了。
others
关于这个问题我能再追问一下吗,我不太理解自定义缓存的需求,为什么会有需要带UUID请求的需求,为什么用UUID后就无法区分请求呢?而不是用NSURLRequestCachePolicy简单实现呢?
cas
两者的区别就在于你希望对缓存有多强的控制和多强的自定义需求。
如果只是单纯想有个缓存,缓存策略用系统提供的就已经足够,只是这种方案就比较朴素。
如果希望针对缓存和请求做更多的控制,例如缓存的持久化、每条缓存在本地生存时间的控制、不同API请求但相同数据部分的缓存和预展示等需求,那就应该自己搞一套。
long
评论中说“创建N个Manager。安居客当时正好也有这个需求:有这么一个API,它返回数据的类型是由一个Type字段来决定的,这里我就会创建N个mananger,然后每个对应的Mananger的Type写死。当成全新的APIManager去使用,哪怕它们最终实际调用的API method name是同一个,这种做法最便于维护。如果可以在paramsForApi:里面动态生成的话,那就再好不过了”
others
paramsForApi中只传递除type以外的参数,然后在reformParams中增加type字段参数对吧
cas
是的
API返回是有type的
(b)"如果可以在paramsForApi:里面动态生成的话,那就再好不过了”这里说的动态生成是指生成什么呢,又是怎么个动态生成呢,能举个例子不
cas
就是在paramsSource里面根据调用场景来决定传递什么type。这说的是reformParams不添加呢type字段了,让paramsSource判断添加的type是什么
others
(c)对于一个服务端提供的一个api接口,客户端是否需要将其拆分成多个本地的apimanager,博主的意思是依据返回的数据格式是不是一样来确定对吧,比如返回的json包含的字段是不是都是“id” “name” “price”,只要因为哪个参数会因为传的值不同导致返回的数据格式不一样(比如有一个参数值变了,导致返回的json包含的字段为“id” “name” “number”) 我们就应该拆分出新的apimanager,然后在reformParams中对这个参数进行区分设置
cas
不是,是根据API的业务场景。比如取item列表,有根据距离取,有根据价格取,有根据星星的数量取。这些可能是同一个API只是type不同,那么我倾向于写成3个APIManager
others
我有一种构思就是服务端提供一个api接口,我就只写一个apimanager,哪怕出现不同的参数导致返回数据格式不一样,我根据这个引起返回不同数据格式的参数做区分,用不同的reformer去转化,不知道博主对这种思路怎么看
cas
这里要考虑的不光是怎么解决需求,考虑更多还是类似c提到的场景:怎么更好地组织代码。我不建议这种场景只用一个APIManager,然后自己去判断type。
others
文中提到“我们在调试的时候经常会单步追踪到某一个地方之后,发现尼玛这里有个block,如果想知道这个block里面都做了些什么事情,这时候就比较蛋疼了”
实践表明单步追踪到block,在继续点击step into,就可以知道当前的block里面做了什么事,不知道是不是博主另有他意?
cas
更早版本的调试器是走不进block的。
others
文中说”如果其他业务也有相同的数据转化逻辑,其他业务直接拿这个reformer就可以用了”
但是其他业务,api返回的数据格式几乎跟现在都是不一样的,reformer在具体实现转化时怎么做到直接拿来就可以用呢。比如
if ([manager isKindOfClass:[ZuFangListAPIManager class]]) {
resultData = @{
kPropertyListDataKeyID:originData[@"id"],
kPropertyListDataKeyName:originData[@"name”],
}
}
里面的”id”,”name”字段在新的api里面完全可能没有了。难道lz上面提到的相同的数据转化逻辑也包含了该业务返回的数据格式都要一样?
cas
比如同样是列表,一个是二手房列表,一个是租房列表,一个是新房列表。最终都要展示成一个tableview。那么这个tableviewcell不需要写3个,只要写一个,让reformer根据三个APIManager的类型去转化数据。
others
ppt中提到不要把reformParams当成reformer去用:“外面传进来的参数格式千变万化,交由APIManager自己来reform是不合适的”
- (NSDictionary *)reformParams:(NSDictionary *)params;
从这个定义来看,传进来的就是一个字典,返回的也是一个字典,已经对格式进行了约束了,不大明白博主说的传进来的参数格式千变万化指的是什么
cas
这里事实上并没有任何约束,外面可以传进来各种各样的key,而API只会接受固定的一套Key。reformer是独立的对象,可以根据不同的输入使用不同的reformer转化成相同的输出,所以它能够处理这种情况,比如3里面举的例子。
而reformParams只是一个方法,所以它不应该拿来当reformer那样使用,如果要做成reformer那样,就要把reformParams的功能提取成对象。
others
博主评论中提到:“当你做迁移的时候,如果是model代码实体,你不能保证其他人在创建这个model的时候,以及后面维护这个model的时候不去包含其他的代码。一旦有这种情况,迁移起来就很蛋疼。用reformer就消除了model这一层,而reformer是属于非常独立的对象,迁移的时候非常方便”。
cas
model是跨层的,引入其他逻辑就会导致层间区分不明朗。reformer是不跨层的,所以随便引入什么逻辑都可以
others
model怎么就跨层了呢,业务层可以根据返回的jsonObject,根据需要再做model转化,这个转化成什么model可以让业务层决定,甚至框架可以提供一个接口传递Class类型的参数,框架根据业务层传递的这个Class,进行model转化,然后将转化结果通过id类型返回给业务层,整个过程框架层都不会涉及具体的model类型
cas
你不知道业务工程师傻逼的极限在哪里。
others
ppt中beforePerformSuccess只是对self.nextPageNumber进行管理,并没法对fetchedRawData进行管理啊
cas
不需要对fetchedRawData管理啊,只要看isFirstPage是no的话,controller直接append数据就可以了啊。
cancel请求
刷新界面(cancel新发起的请求)和筛选(cancel先前的请求)ApiBaseManager中requestIdList都只有一个对象,这里用数组而不是单单用一个整形变量去存存储当前正在进行的请求,是为了以后做更复杂的策略的考虑吗?
cas
是的。如果不做cancel机制,那么就会出现APIManager排队的情况,这是有可能出现的业务需求,所以就先留个口子,以后遇到了就容易做补充逻辑。
others
ppt中《AIFNetworking提供的组件》这一页里面提到 "通过独立的组件来解耦API调用逻辑
,生成带签名的Request,为未来调用touchweb而准备:AIFRequestGenerator
“,同时组建化架构图里面也有一个webview指向Request Generator,没弄明白webview和这里提到的touchweb跟AIFRequestGenerator的关联
cas
这里是13年时候,安居客准备走hybrid,那么这里的touchweb事实上就是现在H5。我们当时设计的是H5的时候也是走API的这种request去做请求,方便校验,防止搜房他们用H5的渠道爬数据。所以业务工程师需要通过requestGenerator拿到经过定制的request之后再去加载webview。
others
AIFCityListManager中并没有调用apibaseManager的loadData,自然也不会调用managerCallAPIDidSuccess,后面看到isCorrectWithParamsData中一段注释中说”我们的城市数据只有在发布前才会下载一下,然后存成Plist文件之后,再取出来放到bundle里面去”,是不是这个类一开始会调用reloaddata,只是有了数据后博主删掉了这句代码了。
cas
主要是会出现当你拿着RequestID去要求cancel的时候,这个requestID对应的request就已经调用完毕了的情况。假设你拿着requestID=1去要求cancel,但这时候1对应的请求已经跑完了,此时正在调用的很有可能是2或3。所以我没办法保证requestID的值就一定是当前正在调用的request的对应ID。
others
"如果拦截的方法是针对这个API本身,那就走内部拦截。如果拦截的方法是仅针对这个场景下的这个APIManager实例,那就走外部拦截。关于内部拦截和外部拦截的详细辨析,也在我的keynote里面讲过了"
cas
由于离散型API的调用设计,所以一个API对应一个APIManager。因此针对API本身的意思就是针对APIManager的这个特定子类。
内部拦截:在继承的子类中重载父类拦截方法进行拦截
当拦截方法内的逻辑是特定针对子类的,适用内部拦截
外部拦截:通过拦截对象在拦截时机执行拦截任务
当需要监听一类APIManager做日志的时候,把散落在每个拦截方法的逻辑提取到拦截对象中去。
important
如果一个需要信息量比较大的view(比如某个用户详情页面)可能需要多个api返回的数据才能组成一份完整的数据,这种情况怎么写reformer呢
cas
你提的这个问题有很多种解决方案。我们先假设三个API(应该足够说明问题了)分别是A、B、C,以及一个view。这个view的内容需要来自A、B、C三组API的数据。
第一种写reformer的方式是,reformer本身就是这个view的工厂,然后reformer有一个delegate指向Controller。在A、B、C三个API的回调中,都把数据交给reformer,当reformer集齐三个API的数据之后,将生产出来的view通过delegate告知Controller,然后展示。
第二种写reformer的方式是,reformer在生成的时候就生产这个view或者Controller生产好之后,让reformer去hold住这个view,在每一次A、B、C的成功回调中使用这个reformer去fetch数据,fetch到数据之后由reformer去配置这个view。
第三种方式就是reformer在第一次回调成功时就生成这个view并hold住,无论这个回调来自A、B或是C。然后在以后的每一次的回调中都使用这个reformer去fetch数据并配置view。
others
只含静态方法类,单例类,普通类这三者在类的设计时考虑的一些心法,虽然lz再回复中多次提到能不用单例就不用单例,但是感觉还是有点拿不准。(比如,AIFUDIDGenerator的所有方法都没有依赖实例变量,这个类采用单例而没有像AIFSignatureGenerator这样采用全部静态方法的考虑是什么?像AIFPListManager又只是普通的类)
cas
在RTNetworking中,对单例的实践确实是混乱的,这是历史原因,因为RTNetworking是集合了两版安居客的网络层架构的产物,在当初改造的时候考虑到工程师们的重构工作量,就没有把这些规矩做到很彻底,你提到的AIFUDIDGenerator其实不光是RTNetworking在用,在安居客App中的其它业务线也在使用,如果不做成单例,业务工程师们的重构工作就会变得很繁重,考虑到这一点权衡,我就还是老样子先放着了。
甚至RTNetworking在某些地方还是需要根据不同App的需求去改造的。在不同的App中我针对before系列的拦截方法都会做改造,例如before系列方法是否要回传bool值来决定是否进行下一步。所以RTNetwoking我并没有给出一个通用版本,而是希望大家可以根据自己的意愿基于它改造出适合自己App的网络层。
但是,单例能不用就不用确实是正确的做法。关于是否应该使用单例,判断标准就是原本这个单例要实现的任务,是否具有全局性质,以及是否需要接收并保存全局参数。例如NSNotificationCenter这种,涉及全局调用,且需要保留全局的通知注册信息,那么就应该是单例。
others
ppt中在RTAPIBaseManager存在的意义一节中说:"同一个API不用重复写同一段调用代码和回调取数据的逻辑,提高代码的组件化程度和集成度”没能很好的理解lz的意境,lz能否举个需要对同一个API重复写同一段调用代码和回调取数据的逻辑的例子
cas
早年安居客的App是直接走ASIHttpRequest的,而且也并没有做很好的封装。每一次调用API都需要做一些周边逻辑,以及API调用成功之后,也都需要做一套周边逻辑来分辨错误类型、数据转化、生命周期拦截等一系列逻辑。而这些逻辑现在都已经包含在RTBaseAPIManager中了。
others
ppt中提到关于拦截器:”可用于网络等待状态的切换”,这里lz能否说的详细点,这里说的网络等待状态的切换是指的2g切换3g切换wifi吗
cas
比如调API之前要转菊花,然后显示一些文案。API调用完之后菊花就停止,出现错误时显示另一些文案,或者成功时就直接展示View。这些类似逻辑我们都拆到拦截器中去做了,而不是跟调用的地方和接收回调的地方写在一起。
这样写就能使逻辑更加纯粹易维护。将来如果针对这些逻辑有修改,直接去拦截器方法里面改就行了,不用找API是在哪儿调用的,是在哪儿回调的。
view还是VC来提供reformer
ppt中说"reformer应该由使用者提供,因为只有使用者才知道如何转化API的数据”,这里说的使用者是指view还是controller呢
cass
View或者Controller都可以,取决于你是否要在相关逻辑中使用“范型”的概念。如果view的具体class是Controller不知道的,那么view就负责提供reformer,如果controller知道所有的view,那么controller就可以提供reformer。
view的装配
这种形式的前提应该是某个页面所有子view都必须共用一个api返回数据,但是每个view的表现形式又不一样对吧?
for (UIView *view in self.viewList) {
[view configWithData:[self.testManager
fetchDataWithReformer:view.reformer]];
}
cas
当然,你也可以让controller去提供reformer,只要Controller能够知道self.viewList里面的所有view都需要什么数据。但事实上这种场景下Controller没必要知道,所以让View去提供reformer是比较方便的做法。
拦截器刷新token
"在不同的App中我针对before系列的拦截方法都会做改造,例如before系列方法是否要回传bool值来决定是否进行下一步", 既然是拦截方法,我的理解是一般是做一些比如博主上面提到的控制转菊花之类的辅助操作,不会去做一些影响主体业务逻辑走向的事,例如:
[self beforePerformSuccessWithResponse:response];
[self.delegate managerCallAPIDidSuccess:self];
博主的意思是有时候需要根据beforePerformSuccessWithResponse的处理结果来决定是否执行managerCallAPIDidSuccess吗?如果是这样的话,博主能否给一个实际场景加以说明可能会更好把握
cas
修改拦截方法来决定是否要执行下一步的例子是在我最近做的一款App中就针对BeforePerformSuccess做了回调截断逻辑。比如在你访问一个需要用户token的API时,如果用户token超时(我们设定是1小时超时),那么就会调用refresh登录token的API,这个API在刷新成功之后它不需要立刻返回回调,而是写入新的token然后重新执行之前调用失败的API请求。这个逻辑属于“暗逻辑”,而且从我们App的业务角度上看,能够确定每一次refreshAPIManager的调用成功都需要重新调用旧的请求。业务工程师不需要自己去写这套逻辑,我也不希望业务工程师要去自己实现它。所以这种暗逻辑我会写在refreshTokenAPIManager的beforePerformSuccess里面。但refreshTokenAPIManager的delegate会被指向给旧的API对应的Manager,因此回调依旧可以由业务工程师自己决定如何处理,在业务工程师实现这个回调时,旧请求仍旧会发出去,同时业务工程师也不需要再实现重发请求的逻辑了。
4的场景并不要求拦截方法去影响APIManager是否要执行下一步。
others
这个refreshTokenAPIManager获取新token的过程会涉及到用户输入信息的交互吧(比如我们的需要重新输入帐号和密码登录),毕竟token的存在是从安全策略考虑的。
感觉不能在RTNetworking 架构中从容驾驭这个策略。比如
- refreshTokenAPIManager具体再哪调用(RTAPIBaseManager 的failedOnCallingAPI中?)
- refreshTokenAPIManager对旧api的实例的引用是通过delegate的方式还是实例变量的方式?
cas
我们有一个refreshToken和authToken。authToken是访问API时候用的,超时时间是1小时。refreshToken是用于刷authToken用的,超时时间是一周。
如果authToken过期而refreshToken不过期,那就不需要再弹登录框了。refreshToken的存在也是为了尽可能少地弹登录框。即使是refreshToken过期,弹登录框也是网络层通过CTMediator去调用弹出的,登录成功之后会继续调用之前的旧请求。
总之就是业务工程师只管调API,其它什么都不用管了。不会把这个暗逻辑更新进去。因为每家公司的token机制都不一样,更新进去没有任何意义。
是的,在RTAPIBaseManager的failedOnCallingAPI中判断是否为token失效导致的错误,如果是,就不走回调,直接调用refreshTokenAPIManager。
refreshTokenAPIManager会直接hold住旧的APIManager实例。
由于是离散型请求机制,所以请求token超时的API就一定是那个旧的API,所以BaseAPIManager直接在fail里面把自己交给refreshTokenAPImanager就可以,剩下的就看refreshTokenAPIManager怎么去调度了。刷token失败就弹登录框之类的。
由于是paramSource机制,所以调旧的API直接只要loadData就可以了,不用管参数。
tableViewCell 数据与APIManager
api请求成功通过managerCallAPIDidSuccess回调的数据,存在controller申明的数组里合适吗?
controller里有tableview,tableview的数据源是怎么获取的?是不是成功回调的数据先存储在controller申明的数组里,然后再reload表格?
cas
如果是tableView的话,把api请求的数据交给controller是合适的,因为很有可能你存在翻页需求。而APIManager只会保留每次请求的数据,上次请求的数据会被冲掉。
others
是不是也存在api请求的数据直接在对应的manager保存和处理?我有看到你也这么处理的
cas
是的,具体怎么处理还是取决于业务场景的。
看了你的ppt里如何维护nextPageNumber变量,我还有一个问题:在刷新的时候,调用loadData之前如何修改nextPageNumber变量?
cas
manager有个cleandata的方法,凡是load下一页的都是调loadNextPage,loaddata仅用于刷新页面,那么此时cleandata就会被调到
category中使用APIManager
A_ViewController , 然后有它的分类:A_ViewController+B, A_ViewController+C
然后 A_ViewController 和 它的分类中都有网络请求,如果用你说的代理方法来统一处理网络请求结果,那么我需要在 A_ViewController 和 它的分类中都实现这个代理,但是这样的情况就会出现代理被覆盖的情况,导致分类中的代理没有被调用。
对于这样的情况,不知道楼主会怎样去处理?
cas
这样的情况其实正确的做法是做成业务组件对象,然后由controller去调度,然后业务组件对象去处理这些回调。做成业务组件对象的另一个好处就在于便于复用,你做成category了就难以复用了,必须要同一类型的对象才能得到复用。
NSCache
我在你的RTNetworking 里看到这段代码 @property (nonatomic, strong) NSMutableDictionary *serviceStorage;
我看过 一篇文章说,建议用NSCache,对于这个,您有什么看法。
cas
你说的没错,在这种场景下没有使用NSCache是我写得不好。
早前生成service的时候是应用启动完就立刻生成所有service的,所以没用NSCache,当时做得也粗糙。后来我改成了使用时先检查是否有service,然后没有的话再生成service。然后我就没想起来要把service的数据改成存NSCache,这是我的疏忽大意。我只在CacheComponents里面使用了NSCache忘记这里还有一个了。。。
不用model数据怎么修改保存?
有个问题,比如说一个购物车页面,页面上的交互比较多,有勾选、数量增减、计算等等,如果用Dictionary,没有 Model ,有点难搞啊。。。
cas
如果页面每次操作你都不去修改数据,只有等用户触发“确定”或“保存”时才收集数据,你还觉得难搞吗?
关于分页
1.服务器有8条评论,id分别是1、2、3、4、5、6、7、8 。 客户端分页规则为,一次加载2条。
如果,客户端现在显示的评论为4条(8、7、6、5),这时新插入了一条评论9, 那么服务端如何来计算下一个分页呢?因为,评论的总数量变了,评论的的id的顺序就会发生变回。
2.在ppt中看到 ,nextPageNumber ,在计算分页的时候是用递减的:self.nextPageNumber--。如果计算好的分页总数为10也,但是这个时候,某些评论被删除了,实际上是只有8页,那应该还需要做根据实际返回结果来,修改nextPageNumber ?
cas
为什么评论的总数变了,评论的id的顺序就会变?
我记得nextPageNumber是递增的吧?你是不是看的方法是fail的那部分?
others
例如:当前有id分别为:1、2、3、4、5、6、7、8 ,8条评论(id值越大,插入时间越新)。
按照评论的插入时间"新-->旧"递减方式来取,当page=1的时候,取到评论8、7,当page=2的时候,取到评论6、5。 如果插入了一条心得评论9,那么当page=3的时候,就会取到评论4、5(1、2、3、4、5、6、7、8 、9)。这样,会出现评论5重复的问题抱歉,。的确是看到是fail的部分
cas
那这个只有本地做去重了,服务器不可能帮到你的。
others
1.每拉取一次数据,都要和之前的数据进行比较去重,这样会不会很影响效率!!
2.问另外一个问题:
在写分页的接口的时候,是不是需要重写loadData方法,用来重置nextPageNumber的值。例如:
- (NSInteger)loadData
{
self.nextPageNumber = 0;
NSInteger requestId = [super loadData];
return requestId;
}
cas
那你就只能让服务器换一套分页方法了。这个方法就是你给出一个基准id,然后告诉服务器基于这个id往前取多少个或者往后取多少个,这样来做分页。
是的,只有载入第一页使用loadData,载入下一页都是用loadNextPage方法。
APIManager and LocalManager
博主好,项目中BusinessManager下面管辖APIManager,LocalStoreManager。
有几个问题:
- APIManager的paramSource应该设置成ViewController,可是现在ViewController只认识BusinessManager,BusinessManager hold住APIManager,如何设置APIManager的paramSource,难道BusinessManager要暴露APIManager?
- BusinessManager被设计成单例,hold住APIManager后,APIManager的生命周期是应用的生命周期,内存上是不是不太好。
cas
BusinessManager也可以有paramSource啊,拿到的paramSource传递给APIManager就可以了。
是不好。把BusinessManager设置成单例的必要性体现在哪里?
。。。
懂了 问一下BusinessManager的粒度多大合适,现在项目中BusinessManager的粒度很大,相关的APIManager都在一个BusinessManager里,是否需要拆分成和APIManager同样的粒度?
cas
只要不是特殊情况,就尽量不要出现businessManager
BusinessManager只是针对业务负责,某个业务如果需要这些APIManager一起工作,那才需要合进去。而不是针对模块负责,不应该因为这些API的功能属于同一类模块,而都放进BusinessManager里面去。
架构师一定要记住业务第一!
others
补充一下我现在的情况,项目是即时通讯APP。有做SDK的需求,未来被接入到第三方,提供尽可能简单的接口,我觉得有必要把相关功能都放在一个BusinessManager中。之前是把BusinessManager设计成单例,原来的APIManager很简单,不需要被hold住。BusinessManager中基本也没什么成员变量。
问题来了:
1.由于前面我没说清楚要做成SDK,再次问一下博主是否有必要把BusinessManager设计成单例?我觉得会单例对第三方来说更简单。
2.很想把RTNetworking迁入到项目中,但是有上面说的hold住APIManager有内存问题,也是在BusinessManager设计单例的前提下。
希望博主能根据SDK的需求和RTNetworking的引入给个总体解决方案。感谢!!!
cas
你为什么不直接把RTNetWorkingAPIManager对应每个API的派生类给用户去用?何必多搞一个单例出来呢?
而且将来你的sdk因为API是独立的APIManager,更新和维护都非常方便。
RTApiManager就是设计来为用户服务的。
token登录
请问如何处理token过期后从任意controller跳回登陆界面的情况?我现在有几个api用到了token,那么就会存在token过期的情况,当token过期时,需要跳回登陆界面重新登陆。我在调api时服务器会在response的header里返回401,我现在是在每个需要调api的controller里都监听通知,如果api返回了401,就post通知,然后收到通知的controller就pop回root,我知道这样做不太好,所以想请教有没有更好的解决方案。
cas
。。。
调度
新手请教个问题,从Controller push 到另一个Controller 是每次都new,还是只要new一次。 还是要看具体场景。。。
others
我在我们的项目里写了一个叫NativeComponentNavigator,根据URL Schema进行页面跳转。每次都会new一个新的vc。
cas
这种做法也可以,不过我一般都是推荐在跨业务的场景下才会采用这种做法,因为有些时候还是会比较复杂的,比如要针对ViewController做delegate,例如UIImagePickerController这种。
others
确实比较复杂。在实践中,我们会提供一个context
- (UIViewController *)openURL:(NSString *)url context:(NSDictionary *)context
dependentURLs:(NSArray *)dependentURLs dependencyContexts:(NSArray *)dependencyContexts;
关于UIImagePickerController这种情况,在实践中也遇到过,我是实现了一套独立于业务的图片选择和拍照功能的模块(HGPhotoPicker),它是一个独立的只能用urlSchema创建的王国。
cas
那你就要准备两套参数系统了,一套参数系统是读URL的get参数,另一套就是从你的context里面取参数。
理论上都能完成需求,但实际操作中其实我还是不建议这么做的,代码调试的时候追踪比较麻烦。
跨业务场景是为了避免业务间耦合才采取的这种方法,如果不存在业务间耦合,我认为还是传统的调用比较合理。
others
表面上看是两套参数,但在内部会把url里的参数拆解到context里,所以对于被创建并打开的VC内部,它只需感知context里有没有自己需要的参数key。另外,这个Navigator的最初被设计出来要解决的问题可能与你提到的跨业务调用的问题还不太是一回事儿,这个Navigator的设计的主要精力是在页面路由,为了充分解决在App中页面间的切换和调用时不采用直接allloc VC的方式,同时有了这个机制后,服务器也可以在业务入口点下发不同的urlSchema来决定用户触发一个业务点后将要进入哪个业务VC页面。所以,抱歉,对于你说的跨业务调用这个情况,我还是没有理解清楚。只是可能我们的这两个问题在解决时都使用了Mediator.
cas
跨业务的情况的根源问题和你希望服务器下发URL调用页面的根源问题是一样的,都是希望能够在尽可能少的上下文情况下做到页面的调度。所以我们最终采用的方案是一样的。但是这种方案的缺点就在于实现复杂度高以及调试跟踪时麻烦。
实现复杂度高的问题在这个组件被实现了之后,其实问题也不大了,就是维护的时候烦一点。调试时跟踪麻烦这倒是一直都会存在的情况。
所以在实现跨业务场景(特点是高度耦合,需要尽可能减少上下文内容来减少耦合度)和通过服务器进行页面调度(特点是环境和上下文都不充分)这样的需求下,采用Mediator是值得的。但是在Native的环境内,上下文和环境都是充分的,只要不是跨业务调用,耦合也是可控制的,所以此时就没必要牺牲这些去完成需求了。
信息点
- 其实是每个模块就是一个pod,然后有一个主工程,主工程的podfile里面就有这些pod的描述,跟cocoapods的工作流是一模一样的。
- 不过关于 SPDY 连接复用那段, 按道理 HTTP 也一样可以实现连接复用的吧. 也就是多个请求共用同一个 socket connection. cas :理论上是你说的没错,但SPDY在实际中还是要基于HTTPS的。
- 程序执行到block的地方,会把block中的对象都retain一下。如果block中包括了self,则self也会加1;如果使用了@weakify(self),则self不会加1,直到block被调用执行到@strongify(self)的时候,才会加1,而这时候已经执行在block内部了,所以ARC会在认为self不会再被调用的时候自动释放self
- 是的,这个是不能处理图片上传的。如果是图片上传这样的请求,我们一般都是走UploadService,这里放出来的代码只是API调用的那部分。UploadService大家的设计都差不多,而且脱敏之后基本上就没啥东西了,我觉得就没有放上来的必要了。
- 然后
#
ifdef DEBUG 其实是可以通过修改scheme来配置的,苹果默认支持。不建议写pch。 - 安居客不同的API在调用的时候,使用参数的方法都不同,有的是会把参数放在请求的header里面,有的会在body里面,有的会在URL里面。但是对于业务方来说,这些是透明的。框架在组装参数的时候会根据不同的API采用不同的组装方法,如果业务方要想看一下request的参数是什么,如果没有这个category,就很蛋疼了。
- View并不是通过reformer和APIMananger藕合在一起的。reformer是Controller持有的,reformer只负责将数据展示。View是完全不需要知道自己的这个数据是来源于哪里的。
- 只有在需要进行facade的情况下,View才会持有reformer。在这篇文章我强调的是reformer交给Controller去持有。PPT里面强调了这种facade的用法,这种场景适合高度模块化的需求,每个View能够支持多种不同的数据格式且Controller无法知道哪个View需要哪种数据。
讨论
偏好用modle,NSDictionary 蛋疼
理由:
- 需要人肉递归和解析,可能出错,需要单元测试来保证。
cas:
- json->dict 只需NSJSONSerialization
- dict->view 只需要reformer处理
疑惑:
modle的原理是什么?为什么他不用递归解析。json的结构不需要预先定义吗?
多个view使用到了同一个api,数据保存在哪的?
cas:
- 原始数据保存在APIManager里面
- reformer只做数据转换的事情
controller要是dealloc 了那manager就不存在了,其他的view也要显示此manager里的数据,要再去请求吗?
cas:
- 一个API由一个APIMananger去维护
- 其他的view要使用这个manager的数据是否要再去请求那就看具体需求:
- 请求一次:一个返回List的API需要同时在两个View上展示,一个列表view,一个地图view,是不需要请求两次。
- 没说。。。
疑惑
如果这是同一个控制其中的view确实情况就是这样子的,但是如果是提问者说的这个view不是在同一个controller中呢?有人提出使用Mantle很好用。
如果涉及到修改原始数据呢,比如修改联系方式
cas:
- 这种属于持久层做的事情
- 网络层是负责调API的
- 修改这样的数据,往往也是通过调用API解决的
- API下发的数据基本上不太可能要修改之后才展示到view的,一定是先展示给用户,然后用户执行了一些操作,然后点击保存按钮的时候调API去增量更新的。
疑惑
- 不明白问题的用意,或者需要修改问题的提法。
- 为什么这是持久层要做的事?
同时好几个请求,都超时了,弹出多个alert?这个一直不知道怎么解决。目前用的串行请求。
cas:
- 不要用alertView,用一个UIView渐显的效果,这样即便啪啪啪来几个,看上去也就是一个的样子。
上一篇讲了不少 胖Model,瘦model的东西,这边又都是NSDictionary 用reformer过滤来显示,那该怎么取舍?
cas:
- 胖model的还是要被拆的
- 把他变胖的目的是因为这样才好拆。
- reformer解放的是controller的一部分工作,跟胖瘦model的概念关联不是很大。
- nsdictionary是数据载体,reformer出来的东西可以不是nsdictionary,可以直接就是个view。
疑问
- 胖瘦model的概念为什么和这个关联不大呢?
"block会延长相关对象的生命周期",可以使用weak/strong self的来解决,这个其实跟weak的delegate很像。你认为呢?
cas:
- weak/strong self解决的问题是retain cycle的问题
- 没有解决对象生命周期被延长的问题
- 这跟weak的delegate不像,是两个维度的概念
- 因为weak的delegate被回收后会变为nil,所以对应的回调方法不会跑进去。
- 现在假设你用了block,如果是weak+strong的self,那么即便外面的计数都被清除,那也至少还剩1在block里,所以你的block回调还是会被跑到,相应的self的那个对象的生命周期就被这个block给延长了。
- 如果是只weak不strong的self,外面计数被清除之后,block中的self已经是nil了,但这仍然不影响block依然会被跑到。甚至因为这个变量已经变成了nil,很有可能在执行这个block的过程中导致错误,乃至crash。
- 所以weak的delegate在这种情况是不会跑到回调的,不影响程序的流程。block即使是用了weak/strong self,也还是会被跑到的,并且会在一定程度上影响程序流程。
疑问
- 为什么没有解决生命周期被延长的问题
还有就是strong self只有在执行的时候才会延长对象的生命周期吧?没有执行retain count应该不会有变化的
cas
- 不是,只要block还在内存中,对象就不会被释放。如果外部引用都没了,就剩block在引用了,而此时block还没跑到,这还是会延长对象生命周期的。
- 引用计数是跟block有没有跑到无关的,因为block在生成的时候,就已经对那些指针的引用+1了。
man
- 首先我的理解: block只会对外部对象进行强引用,但是weakSelf是外部的,而strongSelf是内部的,所以内部的对象只有在执行的时候才会进行retain count 的变化
cas
- 不是的。在生成block对象的时候,就已经对内部对象的引用+1了。如果只是在执行的时候才+1,万一还没执行到block呢对象就被回收了怎么办?如果这种情况,真的成立,retain cycle就不会发生了。
man
- block没执行的时候,block里面对象的地址还有没有分配,怎么retain它的内部变量?
- block好像只会对外部的对象才retain,内部对象不会的,retain cycle发生是因为strong了外部对象
cas
我前面说的内部对象的意思是,block内部引用的对象,然后这个对象是外面的。
NSString *casa = @"casa";
[someBody runBlock:^(){
NSString *casa2 = [NSString stringWithFormat:"%@2", casa];
} afterSeconds:10];
>在block生成的时候,casa这个变量就已经引用+1了,但是casa2这个变量是只有在执行的时候才会被分配的。
前面我说的生命周期被延长的对象,指的是casa这个。对于casa2而言,根本不存在生命周期被延长的问题。
>
>###man
>- 但是如果是__weak NSString *casa = @"casa"; 它的retain count会增加吗?我想是不会的
>
>###case
>- 总之,block在生成的时候,就会把引用到的外面的对象抓过来引用+1。
>
>###man
>strong对象
>![测试代码](http://upload-images.jianshu.io/upload_images/686696-b6d922b40bd9937f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
>weak对象不会加1
>![weak对象](http://upload-images.jianshu.io/upload_images/686696-34098b1f6fbec5d1.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
>
>###cas
>- 没错,对于外面声明的weakSelf,是不会加的,因为已经被你__weak过了,这种情况也不会导致生命周期延长,但是如果外面的对象被回收了,你block里面也用不了了。
>- 而且你实际写的时候,是会在block里面加strongSelf的。你只写weak不写strong,确实是不会引用计数的。
>- 在block内部strong一遍的原因是为了防止block运行的时候,对象被回收。一般来说在嵌套block的情况下,是可以不用再strong的。
>
>###man
>- 但是strongSelf是内部变量,没执行,不会分配地址,所以不影响self的retain count,只有block执行到这句的时候才会影响。
>
>###cas
>strongSelf指向的地址和self指向的地址是一样的,这两个都只是指针而已。所以不存在"执行时分配地址"的说法,执行时分配地址是针对指针这个变量而言的,说的并不是指针所指向的那个变量。所以如果你strong了,block在生成的时候就会增加引用计数,跟执行不执行没关系。你可以做实验看。
>
>###man
>weak指针不会对其它引用这块地址的指针的retain count造成影响
>![](http://upload-images.jianshu.io/upload_images/686696-4285e5500bb97c14.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
>
>###sunnyxx
> 额- - 看文章时候莫名其妙被艾特了。顺便提一下,block对内部变量的retain count +1 是发生在 block_copy 时的,当dispatch_async传入一个block时,立即会调用 copy,所以不会是“跑进了block才+1”
###有3个 Service , 两个编码是UTF8, 一个是GB18030, AFNetworking 修改编码是在 AFHTTPRequestSerializer AFHTTPResponseSerializer, 你的demo里,这两个好像都是单例,只有一个。 根据 service 修改编码,好像不太容易操作。该如何是好???
>
>###cas
>改造一下requestGenerator和service,让service能够带编码信息,然后requestGenerator能够根据service的编码信息去使用不同编码的requestserializer,responseSerializer则根据response的header去决定使用哪个。
>###我的补充:
>
>
>
###在CTServiceFactory.m中有一句:
NSString * const kCTServiceGDMapV3 = @"kCTServiceGDMapV3";
这个是你定义的service的名字常量,是不是这个放在这里不合适?应该单独拉出一个文件放,而且这个文件应该能让使用者修改,因为在其他工程中,service肯定要变的,如果要是在这个里面进行修改,那不是要改这个网络组件的代码了吗?
>###cas
>是的,每一次新添加service的时候都需要修改网络库,但是添加service是一个比较低频的操作,所以我把它放进网络库了。否则要做很多额外逻辑来维护service,我觉得不划算。
>###我的理解
>- 这个是一个组件,使用时不应该改动组件代码
>- 我们要修改的地方:
>
>>- 自定义custom service的类型,有可能有多个service,公司的,第三方的。编解码方式不同的。需要用token的不需要用token的。
>>- CTServiceFactory是要修改的地方。但是作者认为添加server的操作是低频的。可以不用提供额外的方便维护的接口。
>>- 命名为customer的地方
>>![](http://upload-images.jianshu.io/upload_images/686696-2a18129fd93df60e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
>>
>>
###1.将reformer作为datasource,如果cell里面有个代理要通知vc,那是不是只能cell通知vc或者reformer多写一层代理链了吧?还有tableview的delegate适合放到reformer中吗?
>###case
>1. 把dataSource作为reformer本质上就是基于原来独立datasource,然后conform一个reformer的protocol而已。对于cell的生成和配置来说,跟之前独立datasource的做法一样。cell的delegate本来也只能交给controller去做,所以只要datasource去weak一个controller实例,然后在生产cell的时候交给cell的delegate就好了。
###如果a,b,c三个api嵌套,b写在a的外部拦截器里,c写在b的外部拦截里,c调用成功后通知vc的话,这样也只能通过发送通知吧?如果vc要获取a中的数据,b中的数据,c中的数据,这样是不是也只能一层层的往外传了? 以前用集约型的时候block直接嵌套,vc直接拿数据就好了,然后就直接刷新vc了,改成离散型之后,我上面的遇到的情况貌似就只能用通知了,除非都写在managerCallAPIDidSuccess中
###洗数据的时候反复遍历
1.洗数据要遍历:在reformer里面的APIManagerDataReformer协议方法中,对获取的数组进行遍历,每个元素(字典)采用固定字符串作为key,比如:
- (id)manager:(APIManager *)manager reformData:(NSDictionary *)data {
NSMutableArray *result = [NSMutableArray array];
NSMutableDictionary *singleResult = [NSMutableDictionary dictionary];
for (NSDictionary *dic in data) {
singleResult[keyAlertId] = @([data[@"id"] integerValue]);
singleResult[keyAlertTime] = data[@"time"];
[result addObject:singleResult];
}
return result;
}
由vc在managerCallAPIDidSuccess:方法里面调用fetchDataWithReformer:获取reformer洗过的数据并组合成dataList,然后在cellForRowAtIndexPath:方法里面取dataList的第indexPath.row个数据对cell进行赋值.
这样做是可以通,但是有两个疑问:
1.1 还是使用了遍历数据的方式, 没有做到您在有一个评论里面说的”用到的时候再洗不就不用遍历了嘛”.
1.2 似乎没有达到您文章里面说的:”reformer是要提供给业务层的view直接可以使用的数据”,比如我在这里直接使用的应该是一个cell,但是我还是给了一个字典拿去给cell用.
2.接下来我看到了把reformer作为tableView的dataSource的方法,我只是单纯的把dataSource协议拿给reformer去实现,dataList改为reformer去持有(1里面是vc持有),也是可以通,但是这样就和做法1没什么区别,还是会有1.1和1.2两个问题.
3.我觉得我这个reformer应该是为cell服务的,所以它应该只是要针对数组里面的每一条数据的.所以,这次我没有对数据进行遍历洗牌,而是直接把获取的数据直接组合成dataList让reformer持有,实现效果是这样的:
###很大一部分页面需要在服务器数据库改变了以后,马上通知到 APP 的 View。现在做的是轮询策略,觉得不是科学,想征求下博主实现策略。感激不尽,这段时间一直在学习博主的文章,受益匪浅!
> - 推送
>
> - 启用[HTTP Chunked](https://zh.wikipedia.org/wiki/%E5%88%86%E5%9D%97%E4%BC%A0%E8%BE%93%E7%BC%96%E7%A0%81),通过长链接来告诉App有数据更新。安居客应用的聊天功能在早期就是这么做的。
或者直接开启socket,[这个库](https://github.com/robbiehanson/CocoaAsyncSocket)还是不错的。
###博主可否点评或者说说使用 ReactiveCocoa 对 网络层的设计? 我个人很喜欢 [OctoKit](https://github.com/octokit/octokit.objc)当中用AFNetworking, Mantle, 和 ReactiveCocoa 结合的处理方法
>###others
>我只看过[resetkit](https://github.com/RestKit/RestKit)
>###cas
>....
>###pl
>[resetkit](https://github.com/RestKit/RestKit)RestKit is a framework for consuming and modeling RESTful web resources on iOS and OS X
>
###block weak strong 再议
>###others
>看评论发现争论最激烈的那个关于block的问题,我也发表一下我的看法。我与吕鹏伟 同学的结论基本是一致的。使用block,对与viewController的self如果不做weak/strong操作就会导致引用循环,也算是延长了生命周期,但是使用了weak/strong操作之后,block capture的就不是self而是指针地址,这样并不会导致self的生命周期被延长,这时如果请求还没有落地而viewController被释放了,那么block中的weakSelf就是nil了,viewController还是正常的生命周期,这跟使用delegate要先判断(delegate && [delegate responseSelector:@"XXX"])再回调是一样的吧。 特殊的情况就是请求落地了而viewController还没有被释放,在block中weakSelf转为了strongSelf,在这种情况下确实是对viewController的引用计数+1,也可以理解为暂时的延长了生命周期,不过这也没什么影响,因为这时viewController本来也没有被释放。
所以我觉得就算有延长也仅仅是在执行到block并且此时weakSelf还存在的情况下。而不是block一被创建就意味着生命周期被延长了。
>###cas
>关键问题其实在于,block会capture的不止是self。
而且一开始我们其实讨论的是delegate和block的取舍问题,weak/strong只是其中一部分,是其中被扯得比较远的部分。
如果分清楚capture之后的内存管理方式,和block针对自己内部变量的内存管理方式,就不会出现这样的问题了,也不会讨论这么久。
weak/strong正是把前者转变为后者的手段。这么做可以避免个别对象生命周期的延长,但不建议针对所有capture变量都这么做,也不建议用block代替delegate去做网络层数据的回调。
>###others
>对于block的weak, self. 其实应该就应该外面weak, 里面strong. 如果strong失败, 就直接return掉. 这才是正确的处理方式. 如果strong成功了, 就苟延残喘一阵子, 直到block结束. 对于block里面, 多变量引用, 可以都weak化和strong化, 这样就没问题了. 用@weakify(self, xxxViewController) 就可以实现多变量申明. 方便. 另外 delegate也不能解决引用失效的问题. 你想想, self.delegate是弱引用, 但是传的参数都是强引用吧. 所以还是会延续block那种错误
>###cas
>ARC给传的参数加强引用的时机是要执行那个方法了才加的。
因为delegate被回收了,所以也进不去那个方法了,所以强引用是不会被ARC给加上去的~
>###others
>但是delegate进去了, 然后传递给delegate方法的参数和self, self.delegate无关呢, 就有可能是一个野指针引用. 所有最好也weak strong一下. 不过这样考虑代码太累, 我的办法是先上线再说, 遇到崩溃下一版单独处理改掉
###block很好用呀
>###others
>这个我感觉还好,毕竟在网络请求这里,主要就是就是把response塞给viewController,所以capture的只有self,至少在我们的项目中是这样的。
>###cas
>如果你的回调代码里面除了self,其他任何外部变量都没有涉及,那就不存在生命周期被延长的问题了。但这种情况其实不多的。
>###ohters
>其实也有涉及到其他的变量,但基本都是self的实例变量,通过weakSelf.都可以取到了。
>###cas
>那这个就需要你跟业务方之间要做好约定了。但这并不影响“不要使用block去代替delegate做回调”的观点。
>###other
>至于用什么做回调,我其实还是支持使用block的,不单纯是说网络这里。因为block简洁,所需的代码量极少,读代码时也很方便,能直接看到这个回调做了什么事情,而delegate的话,需要看protocol的声明,再找到对应的方法再看回调内容,无形中就多了那么几次代码片段的跳转。还有一个特性就是block可以capture住局部变量,这样可以在block中直接使用,而使用delegate的话则需要另外的方式将这个变量进行存储。
>###cas
>在我这边有一个我很在乎的点是这样的:调用API的地方和API回调的地方不应该在一起,一个方法就只要做一件事情。另外,block在单步追踪的时候,挺难追踪具体做的事情的,比如文中举的那个例子就很蛋疼。
###为什么不用reformer吐出model对象呢,我感觉处理Dictionary太麻烦了,特别是在别的页面需要修改数据的时候。
>###cas
>首先我想强调的是reformer的设计初衷是吐出能够直接被view使用的对象,甚至可以直接就是view。
然后我想问的是,你觉得麻烦的点在哪里?另外还有一个小问题,修改数据这个操作背后的需求是什么?我觉得这里应该是有设计问题了。
>###others
>我想了半天还是不太理解你的意思,reformer是不是只是用来吐出view直接需要的数据,但是数据持久化或者跨界面访问修改的时候还是需要使用model的?我感觉麻烦是觉得处理dictionary没有处理model来的方便,应该是我没有完全理解你的意思
>###cas
>1. 嗯,我们先不称呼它Model,我们称呼他为Manager。
>
>>>a.当既存在API数据的需求,又存在持久化的需求时,你会有一个APIManager,然后有一个LocalStorageManager,然后有一个业务DataMananger,这个DataManager下面管辖APIMananger和LocalStorageManager。以上我们可以理解为LocalStorageMananger的数据是APIMananger数据的本地存储。
>>>
>>>b.当需要数据的时候,controller拿着reformer去问DataMananger要数据,DataMananger决定是要拿APIMananger的数据还是LocalStorageMananger的数据去喂reformer,这样Controller就拿到了直接可以给View使用的数据了,这个数据的载体可以是NSDictionary,也可以直接就是个View,甚至cell。
>>
>2. dictionary只会比model更加方便,你不用做json转化,迁移的时候也不用迁移具体代码,把key丢过去就可以了,不会出现拔出萝卜带出泥的情况。另外,一种写法是data[kPropertyListItemKeyName],另一种是data.PropertyListItemName,两者在写法上是没有什么特别大的差别的。这么做省了很多事情,具体的我在文章中都已经描述过了。
>
>###others
>博主,这里的LocalStorageMananger可以做成单例,统一存储数据吗?(这个算是集约型存储吗),还是哪里需要就通过init初始化,然后再做存储呢?
>###cas
>这个看需求吧,如果你的LocalStorageMananger操作的是sqlite的话,单例也可以。如果只是操作文件啥的,就没必要单例了。全局单例是要尽量避免滥用的,所以如果不用单例也能解决问题的话,就尽量不要用单例。
>###others
>我的LocalStorageMananger操作是sqlite,使用sqlite,如果不用单例,多个地方同时存储是否会有问题呢?我们被统一要求使用数据库。关于数据存储,我想请教一下,现在我们使用sqlite,但是有个数据库升级的问题不知道有什么好的方案,我们在数据库表中添加版本号,来判断是否要升级数据库,而升级数据库的表时还要保留之前的旧数据。在加上用sqlite要写sql语句,所以有android和.net的同事推荐我们使用ORM技术,我了解了一下,CoreDataj就是ORM框架,但是有人又说coredatab不好用(coredata没有使用过),目前也不知道是否该替换存储方案。
>###cas
>...
>
- 通过const的名字来表示是否optional : 参数可选
- reformParams 可以用来添加翻页参数:页码和页大小,而这个是业务方不关心也不提供的数据。
- view的装配问题:
> [self.XXXView configWithData:reformedXXXData]
>1. view直接提供方法configWithData
>2. view去conform一个protocol,然后facade的方式提供方法configWithData