在iOS开发中,网络是必不可少的一部分,没有人不知道大名鼎鼎的AFNetwork框架的,因为它提供了非常丰富实用,方便的网络调用。使得很多需求都能够调用已有的方法完成。但是面对业务需求,如何合理的将AFNetwork近一步封装能够更加方便的完成业务需求却是需要好好考虑的。以下根据自己的工作经历中对AFNetwork的封装。
一、首次接触
自己在A公司的时候刚刚接触iOS不久,看到的工程中已经存在的封装是这样子的
+ (void)postJSONWithUrl:(NSString *)urlStr parameters:(NSMutableDictionary *)dic success:(void (^)(id responseObject))success fail:(void (^)())fail; + (void)JSONDataWithUrl:(NSString *)url andDic:(NSMutableDictionary *)dic success:(void (^)(id json))success fail:(void (^)(id error))fail;
这两个函数很简单,就是发起一个网络请求,使用的时候urlStr基本上就是固定的用宏来定义的。dic就是传递的字典.success 和fail 分别表示请求成功和失败的回调。但是他们缺点重重,最明显的
post和get应该可以用一个方法来实现的。
urlStr每次都写都要写太麻烦。
二、接触正规需求
后来就是现在的公司了,因为业务需求比较正规了,iOS客户端又是刚刚开始研发,所以跟我提了一系列需求,让我封装一套通用网络方法,相信这也是大部分互联网公司的需求吧,只是我以前见识太少。
网络请求的数据要能够缓存,但是也可以不进行缓存,缓存的数据还要还要可以设置缓存时间。
对于每次网络请求要优先从缓存中读取内容,如果缓存读取成功,就用回调成功block,然后检查缓存是否过期,过期就从网络请求数据,然后把新的数据存入到缓存中。
网络请求的结果:
成功获得数据
请求数据失败、返回特殊状态码。(用户没有登陆,已经是最新数据等等)
缓存的应用场景主要是tableview加载数据,其他的场景也适合,以下以tableview为例
首次进入一个界面tableview的数据应该先读取缓存,主要防止没有网络或者网络不好的情况下用户等待数据时间过长或者没有数据用户体验不好,主要用于某些tableview请求的数据量很大,或者数据短时间内变化不是很快的地方。
用户下拉刷新表示重新请求数据,应该强制更新数据,不论本地是否有缓存,但是这部分数据应该存入数据库,保证退出后下次进入界面的时候能够读取最后更新的缓存。
上拉加载更多的时候不读缓存,因为如果客户端自己在处理这部分逻辑比较复杂,不是说实现起来复杂,因为对于本地存储来说调用的是统一的一个方法,主要是因为如果用户下拉刷新需要强制更新数据,不论本地有无缓存,都要从服务器请求最新数据。那么问题来了,如果我把用户上拉加载更多时候的数据也存入本地,假设用户上拉加载了5页数据,然后又强制下拉刷新了一下,这个时候tableview显示的是最新的第一页数据,如果接着上拉加载更多我应该是读取缓存还是直接发起网络请求呢。毫无疑问应该直接发起请求,因为下拉强制刷新已经导致了第一页为最新数据,如果第2-5页数据缓存没有过期并且服务器数据确实变化了的话,客户端将得不到新的数据,所以干脆仅仅第一次进入界面tableview请求第一页数据的时候要读缓存。
根本不用缓存的地方主要是某些数据变化非常快,或者发起的网络请求是一次性的操作,比如收藏某个题目等等。
根据上面的需求,我首先想到用sqlite作为缓存的数据库,面对一个完整的url和url请求得到的json字符串提供增删改查,用了一天时间我封装了属于工程的数据库。提供了如下接口
/** * 插入一个json缓存数据 * @param key 键 * @param value 数据 * @param expire_time 过期时间 */ + (void)insertJonsCacheStringWithKey:(NSString *)key andWithValue:(NSString*) value andWithExpireTime:(NSInteger )expire_time; /** * 根据JSON缓存获取一个包含对JSON数据操作的对象 * @param key 键 * @return --- JsonCacheData类型的可以对提取的json处理 */ +(JsonCacheData *)queryJsonCacheTableWithKey:(NSString *)key; /** * 插入的值是字典类型 * @param key 键 * @param value 值 * @param expire_time 过期时间 */ + (void)insertJsonCacheDictWithKey:(NSString *)key andWithValue:(NSDictionary*) value andWithExpireTime:(NSInteger )expire_time; /** * 删除数据缓存 * @param key */ + (BOOL)deleteJsonCacheDictWithKey:(NSString *)key;
并且创建了一个JsonCacheData对象存储数据库的数据,因为有的时候存入数据库的不一定是json,很可能是个字符串什么,这样可以按照不同的需求调用类里面的方法得到自己想要的数据类型.
@interface JsonCacheData : NSObject //数据存入数据库的时间戳 @property (nonatomic,assign) NSInteger saveTime; //缓存有效期长度 @property (nonatomic,assign) NSInteger expireTime; //从数据库返回的字符串信息 @property (nonatomic,strong) NSString * jsonCache; -(BOOL)isExpire; /** * 转换成JSON类型 * @return JSON数据的字典 */ -(NSDictionary *)JsonData; @end
AFNet封装完成后函数如下
/** * 网络请求数据的方法,可以进行缓存 * @param param 上传请求的参数 * @param success 获取的json数据,可能是网络请求的,也可能是本地缓存的 * @param codeError 网络返回数据的错误码 * @param fail 网络连接失败 * @param minutes 缓存的有效时长 */ +(void)loadDataWithCache:(NSDictionary *) param NetBlock:(void (^)(NSDictionary *json))success ErrorCode :(void(^)(int errorCode))codeError Fail:(void(^)())fail cacheTime:(int) minutes { NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithDictionary:param]; NSString * sqliteKey = [HSNetUrlProcess createSqliteKeyWithParm:dict]; JsonCacheData * data = [SQLiteJsonCache queryJsonCacheTableWithKey:sqliteKey]; //缓存存在并且,缓存时间大于0表示要求读取缓存的 if (data.jsonCache.length>0&&minutes>0) { if (data.jsonCache.length>0) { //如果缓存存在, 有缓存就显示,并且缓存时间要大于0否则就没有意义 if (success!=nil) { success(data.JsonData); } if (data.isExpire) { if ([data.JsonData[@"version"]length]>0) { dict[@"version"]=data.JsonData[@"version"]; } //这里调用网络方法请求数据 [AFNetworkTool JSONDataWithDic:dict success:^(id json) { if (json!=nil) { //加密 [SQLiteJsonCache insertJsonCacheDictWithKey:sqliteKey andWithValue:json andWithExpireTime:60*minutes]; success(json); } } codeError:^(int errorCode) { if (codeError!=nil) { if (errorCode==NetCode_RESULT_NEWEST) { //更新缓存的时间,已经是最新数据 [SQLiteJsonCache insertJsonCacheDictWithKey:sqliteKey andWithValue:data.JsonData andWithExpireTime:minutes*60]; } codeError(errorCode); } } fail:^{ if (fail!=nil) { fail(); } }]; } } } else //这里是不读取缓存 { [AFNetworkTool JSONDataWithDic:dict success:^(id json) { if (minutes!=CACHE_NO_SAVE) { // CACHE_NO_SAVE = -1 表示不需要存入数据库。 [SQLiteJsonCache insertJsonCacheDictWithKey:sqliteKey andWithValue:json andWithExpireTime:minutes*60]; } success(json); } codeError:^(int errorCode) { if (codeError!=nil) { codeError(errorCode); } } fail:^{ if (fail!=nil) { fail(); } }]; } }
传递的cachetime有以下类型
上面的封装也不太好,但是时间太紧迫,赶鸭子上架,而且都等着用我写的网络方法呢,所有就先这样了,在使用的时候暴露了很多问题:
网络请求有的时候需要控制SVProgressHud的显示,但是很多场合要么多了,要么少了,每次调用需要自己加麻烦。
真正使用起来鸡肋,为了完成上述tableview请求数据的需求。我们只能这样做
self.cacheTime = CACHE_NO_NORMAL; [AFNetworkTool loadDataWithCache:dict NetBlock:^(NSDictionary *json) { self.cacheTime = CACHE_NO_READ; //process data } ErrorCode:^(int errorCode) { } Fail:^{ } cacheTime:self.cacheTime];
用一个全局变量保存缓存策略然后请求数据后在改,一个界面如果存在两个同的网络请求都用到缓存,就要定义两个cachetime.太麻烦了,不能忍。
解决方案
我想我可以通过给网络调用添加参数的形式方式扩展功能,但是放弃了,因为如果给原来的方法添加一个参数,工程所有的调用的地方都要改,实在麻烦,如果原来的方法保留,复制一下加个参数工程中会多出很多冗余代码,而且参数过多调用起来也不方便。
自定义一个网络配置类 NetSetting,包含发起一次网络请求所有的控制策略,将类的对象作为网络调用的参数,网络如何发起完全根据传递的NetSetting对象的设置
所以最终经过我的不断修改一个能够适用于所有需求的网络请求成功了。
NetSetting类包含了所有的设置,包括请求方式,缓存策略,是否加密,是否自己单独控制SVProgressHud的显示。
typedef NS_ENUM(int, HSCacheTime) { HSCacheNoRead = 0, HSCacheNoSave = -1, HSCacheNormal = 5, HSCacheOneMinute = 1, HSCacheOneDay = 24*60, }; typedef enum { NetMethodPOST = 0, NetMethodGET = 1, }NetMethod; @interface HSNetSetting : NSObject //加载动画控制方式,yes表示由调用的控制器控制,NO表示有AFNetWork类控制 @property (nonatomic,assign) BOOL isCtrlHub; //缓存的策略 @property (nonatomic,assign) HSCacheTime cachePolicy; //是否加密 @property (nonatomic,assign) BOOL isEncrypt; //获取数据的方式,get请求或者post请求 @property (nonatomic,assign) NetMethod askMethod; //只读缓存的内容 @property (nonatomic,assign) BOOL justReadCache;//启动页广告会用到 //默认的设置 +(instancetype)noSaveCacheSet;//不写缓存 +(instancetype)noReadCacheSet;//不读缓存 +(instancetype)readCacheSet;//读缓存 +(instancetype)noEncryptPost;
此时AFNetwork封装的方法包含了两个部分
对外开放的部分,所有的网络请求只能调用这个方法发起。改方法会根据netSetting的配置去合理的处理网络请求的细节。
//这是对外提供的方法 +(void)HVDataCache:(NSDictionary *) param NetBlock:(void (^)(NSDictionary *json))success ErrorCode :(void(^)(int errorCode))codeError Fail:(void(^)())fail Setting:(HSNetSetting*)netSettting { HSNetSetting * set = netSettting ; if (set == nil ) { set = [HSNetSetting noSaveCacheSet];//默认 set.isEncrypt = YES; } NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithDictionary:param]; NSString * sqliteKey = [HSNetUrlProcess createSqliteKeyWithParm:dict]; JsonCacheData * data = [SQLiteJsonCache queryJsonCacheTableWithKey:sqliteKey]; //读缓存,后面的null是服务器最近返回的错误数据, if (data.jsonCache.length>0&&(set.cachePolicy>HSCacheNoRead||set.justReadCache==YES)) { if (data.jsonCache.length>0) { //如果缓存存在, 有缓存就显示,并且缓存时间要大于0否则就没有意义 if (success!=nil) { if (data.isExpire==NO) { set.cachePolicy = HSCacheNoRead; } success(data.JsonData); } if (data.isExpire) { //要把version带上,服务器用于md5计算,是否返回数据 if ([data.JsonData[@"version"]length]>0) { dict[@"version"]=data.JsonData[@"version"]; } int cache_span = [data.JsonData[@"cache_span"]intValue]; cache_span = cache_span>24*60?60:cache_span; set.cachePolicy = cache_span; [AFNetworkTool HVJSONDataWithDic:dict success:^(id json) { //加密 [SQLiteJsonCache insertJsonCacheDictWithKey:sqliteKey andWithValue:json andWithExpireTime:set.cachePolicy*60]; set.cachePolicy = HSCacheNoRead; if (set.justReadCache==NO) { success(json); } } codeError:^(int errorCode) { if (codeError!=nil) { if (errorCode==NetCode_RESULT_NEWEST) { //更新缓存的时间 [SQLiteJsonCache insertJsonCacheDictWithKey:sqliteKey andWithValue:data.JsonData andWithExpireTime:cache_span *60]; } codeError(errorCode); } } fail:^{ if (fail!=nil) { fail(); } } Setting:set]; } } } else { if (!set.isCtrlHub) { [SVProgressHUD show]; } [AFNetworkTool HVJSONDataWithDic:dict success:^(id json) { if (!set.isCtrlHub) { [SVProgressHUD dismiss]; } set.cachePolicy = [json[@"cache_span"]intValue]; if (set.cachePolicy!=HSCacheNoSave) {//判断是不是 不存缓存 set.cachePolicy = HSCacheNoRead; [SQLiteJsonCache insertJsonCacheDictWithKey:sqliteKey andWithValue:json andWithExpireTime:set.cachePolicy*60]; } if(set.justReadCache==NO) { success(json); } else { codeError(NetCode_NO_OLDDATA);// } } codeError:^(int errorCode) { if (codeError!=nil) { codeError(errorCode); } if (!set.isCtrlHub) { [SVProgressHUD dismiss]; } } fail:^{ [SVProgressHUD dismiss]; if (fail!=nil) { fail(); } } Setting:set]; } }
非对外开放的部分,该方法用于对外开放的方法的使用。
+ (void)HVJSONDataWithDic:(NSDictionary *)paramDict success:(void (^)(id json))success codeError:(void(^)(int errorCode))codeError fail:(void (^)())fail Setting:(HSNetSetting*)netSettting { AFHTTPSessionManager *manager = [AFHTTPSessionManager manager]; manager.requestSerializer.timeoutInterval = 10.f;//设置请求超时的时间 manager.responseSerializer.acceptableContentTypes = [NSSet setWithObjects:@"text/html",@"image/png",@"image/jpg", nil]; //[manager.requestSerializer setValue:@"headers" forHTTPHeaderField:@"Referer: http://www.vyanke.com/n"]; manager.responseSerializer = [AFHTTPResponseSerializer serializer]; NSMutableDictionary * dict = [NSMutableDictionary dictionaryWithDictionary:paramDict]; NSString * url; NSDictionary * param; //拼接将要访问的URL地址 NSDictionary * dict2 = [HSNetUrlProcess createCompDictWithParm:dict]; NSString * value = [dict2 mj_JSONString]; url = [HSNetUrlProcess createHeadURL:dict]; NSLog(@"URL是%@",url); if (netSettting.isEncrypt==YES) { param = @{@"param":[EncryProcess textEncrypt:value]}; NSLog(@"加密后完整参数%@",[EncryProcess textEncrypt:value]); } else { param = dict2; } if (netSettting.askMethod == NetMethodPOST) { [manager POST:url parameters:param progress:nil success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) { //解密处理 NSString * str = [[NSString alloc]initWithData:responseObject encoding:NSUTF8StringEncoding]; if (netSettting.isEncrypt) { str = [EncryProcess textDecrypt:str]; NSLog(@"解密后的参数=%@",str); } NSData *jsonData = [str dataUsingEncoding:NSUTF8StringEncoding]; NSError *err; NSDictionary *JSON = [NSJSONSerialization JSONObjectWithData:jsonData options:NSJSONReadingMutableContainers error:&err]; int code = [JSON[@"state"]intValue]; if (success&&code==NetCode_NETWORK_SUCCESS) { success(JSON); return; } switch (code) { case NetCode_TOKEN_EXPIRE_ERROR://{ { [FLLoginDataModel currentUser].isLogin = NO; [HSCoverView showMessage:JSON[@"msg"] finishBlock:nil]; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ [SVProgressHUD dismiss];//消失 HSLoginController * log = [[HSLoginController alloc]init]; log.hidesBottomBarWhenPushed =YES; log.finishLoginBlock = ^{ [AFNetworkTool HVJSONDataWithDic:paramDict success:success codeError:codeError fail:fail Setting:netSettting]; }; if (![[HSTool getCurrentVC] isKindOfClass:[HSLoginController class]]) { [[HSTool getCurrentVC].navigationController pushViewController:log animated:YES]; } }); } default: [HSCoverView showMessage:JSON[@"msg"] finishBlock:nil]; } if (code!= NetCode_NETWORK_SUCCESS&&codeError!=nil) { dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ codeError(code);//这里必须写,否则tableView在使用本类的时候无法停止刷新。一直处于刷新状态.需要在调用者里面继续调用。 }); } } failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) { NSString * tips = [NSString stringWithFormat:@"%ld %@",(long)error.code,error.userInfo[@"NSLocalizedDescription"]]; NSData * data = error.userInfo[@"com.alamofire.serialization.response.error.data"]; NSString * message = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding]; NSLog(@"服务器的错误原因:%@",message); [HSCoverView showMessage:tips finishBlock:nil]; if (fail) { fail(error); } }]; } else if(netSettting.askMethod ==NetMethodGET){ //调用manage的 GET方法同上 } }
公开方法在需要向网络获取新数据的时候会把自己的所有参数传递给私有方法,私有方法在完成网络请求的时候会把结果回调给公开方法,然后公开方法最终把数据返回给调用者。随着业务的需求逻辑会愈加复杂,变动更加多样,需要自己编写代码的执行逻辑。但是这样的方法使得在很多情况下仅仅更改某几行代码就实现了功能。
应用场景
为了方便调用,所有的控制继承根控制器,根控制器设置了一个HSNetSetting 属性。并且懒加载的形式重写了get方法
-(HSNetSetting*)defaultSet { if (_defaultSet == nil) { _defaultSet =[ [HSNetSetting alloc]init]; //设置默认的配置 } return _defaultSet; }
这样所有的子类,在有tableview的时候发起网络请求的时候直接传递self.defaultSet 就可以了,而且在 公开方法内部在完成后数据请求后,把传递的netSetting改了,如果传递的缓存策略是读缓存,请求完成后直接改成不读缓存,不用自己手动更改。
前段时间启动页加广告,返回的是json,包含了广告的停留时间和图片地址,显示与否信息。但是启动页时间太短,本次广告应该读取上一次的,直接在HSNetSetting中加入了
@property (nonatomic,assign) BOOL justReadCache;//启动页广告会用到
然后自己调整缓存方法的逻辑,用同一个方法实现需求。
app 不是要求用户登录的,但是进行某些功能要登录。比如:在A界面点击按钮Push到B界面,但是B界面需要用户登录后才能获得正常信息。否则服务器返回消息提示用户登录,所有会在B界面push到登录页,登录完成后pop到B界面,然而刚才B页面没有数据,如果不是tableview或者没有下拉加载功能,只能返回A,然后再push到B界面。用户体验很不好,在网络封装方法中提示用户登录时加入下面代码:
HSLoginController * log = [[HSLoginController alloc]init]; log.finishLoginBlock = ^{ [AFNetworkTool HVJSONDataWithDic:paramDict success:success codeError:codeError fail:fail Setting:netSettting]; }; if (![[HSTool getCurrentVC] isKindOfClass:[HSLoginController class]]) { [[HSTool getCurrentVC].navigationController pushViewController:log animated:YES]; }
给登录控制器加一个block,登录完成后重新调用一次自己刚刚的操作就能解决这问题。
SVProgressHud 网络请求的时候需要自己总是写麻烦,可以根据netSetting 配置让网络方法内部处理。但是某些场合不需要出现SVProgressHud,即使读取的数据是从网络获取的也不出现,只需要关闭netSetting对SVProgressHud的控制,但是自己也不添加SVProgressHud的控制就时间了。
缓存时间不由客户端控制,需要交给服务器控制,无所谓,稍微调整网络方法逻辑就能完成(就是现在这样的)。
有些界面数据更新并不一定是非常频繁的,但是需要更新的时候就必须更新,比如一个界面包含了用户获得得积分,在用户消费了积分后,再次进入这个界面必须刷新数据。把这种类型的界面设置不缓存,并且在viewwillappear
中发送请求然后刷新UI无疑能解决问题。但是并不好,一个是因为请求太频繁了,浪费流量,如果数据比较多,网络比较慢,用户体验很不好。但是通过上面介绍的缓存策略,将缓存时间改成一个极小的值,比如1秒,就能有效解决问题。因为如果数据没有发生变化虽然发起请求,服务器只返回简单提示:客户端的数据是最新的,请求的数据很快。但是还是不好。因为数据虽然在本地,但是UI还是会刷新一下,就是会闪一下。解决这样的问题依然不难,可以在网络配置类加一种类型:只要最新的数据,在本地有缓存的情况下,仅仅在缓存过期并且数据确实发生变化的时候才把最新的数据用success块回调给控制器,当然和前面一样这里要更新缓存数据和日期,其他情况不回调给控制器就解决了。
到此为止一个相对完整的网络请求的框架完成,随着需求的一步步明确,自己分析的透彻,才有了现在形势。其实可以更进一步优化,可以在数据库和数据之间在加一层缓存,也许会更好。自己后来看了一本叫做《图解http协议》的书,发现里面好多缓存策略和上面介绍的很符合,所以以后可以参考下http协议里面的缓存策略的来扩展网络类。