IOS的MKNetworkKit简介与使用
MKNetworkKit 资源地址:https://github.com/MugunthKumar/MKNetworkKit
一、MKNetworkKit的介绍
MKNetworkKit是一个 O-C 编写的网络框架,支持块,ARC且用法简单。MKNetworkKit集 ASIHTTPRequest 和 AFNetworking 两个框架于一体。ASIHTTPRequest 框架是一个用O-C编写,对 CFNetwork API 进行了封装,并且使用简便的一套API,可以用于各种从简单到复杂的HTTP请求,或者可用于处理Amazon S3、Rackspace等REST服务的强大框架,可以说是网络框架的终结者,但是Ben在2011年9月21日就已经声明停止开发和支持该框架。而 AFNetworking相对于只有两个类的MKNetworkKit框架,便显得有些繁琐了,所以这次决定初步研究一下MKNetworkKit的入门。
二、MKNetworkKit 新特性的了解与初步解析
首先要导入一些依赖框架,CFNetwork.Framework、SystemCofiguration.framework、Security.framework。
相对于ASIHTTPRequest 和 AFWorking ,MKNetworkKit 增添了一些新的功能,现在我们看看这些新特性和相关的demo。①超轻量级框架。
MKNetworkKit 整个框架只有两个类(MKNetworkEngine与MKNetworkOperation)和一些类别方法(Categories和Reachability)。
MKNetworkEngine是整个框架的核心,MKNetworkOperation是对网络的一些具体操作。如果想找相关方法的实现,找的也很方便,但是作者有些放封装的很好,还需要慢慢解析。
②完全支持Arc
MKNetworkKit完全支持Arc,Arc通常比非Arc代码更快,而且目前Arc的使用量越来越广,逐渐成为主流,而且的确十分方便,所以在新的项目中使用新的网络框架时就不用再放弃之前已有的框架了。
③在整个程序中只有一个全局队列
关于拥有网络请求的的APP而言,控制网络线程并发数是重中之重,控制不好极其容易出现各种错误。所以MKNetworkKit 使用单例生成一个全局共享单例来保证这个问题。demo如下:
//实例化一个共享队列 static NSOperationQueue *_sharedNetworkQueue; |
④显示网络状态指示
MKNetworkKit使用了单例的共享队列,在共享队列中有一个线程通过KVO方式随时观察operationCount属性,通过观察者方法随时改变网络状态。demo如下:
KVO观察:
//单例、KVO观察_sharedNetworkQueue中的operationCount,并将队列的并发连接最大数设置为6 +(void) initialize { //单例 if(!_sharedNetworkQueue) { //线程保护 static dispatch_once_t oncePredicate; dispatch_once(&oncePredicate, ^{ //初始化_sharedNetworkQueue _sharedNetworkQueue = [[NSOperationQueuealloc]init]; /** *使用KVO观察 *当线程并发数operationCount发生变化时,调用观察者方法 *观察者:[self self] *观察对象:_sharedNetworkQueue *观察的属性:operationCount */ [_sharedNetworkQueueaddObserver:[selfself] forKeyPath:@"operationCount" options:0 context:NULL]; //将队列的并发连接数设置为6 [_sharedNetworkQueuesetMaxConcurrentOperationCount:6]; }); } }
KVO观察者方法: /** *观察者方法 *声明为类方法调用更方便,不用实例化调用者,高效但是耗费内存 *当观察者观察到全局共享队列中的并发连接数发生变化时,就会注册一个通知,而通知会将全局共享队列的对列数传送给kMKNetworkEngineOperationCountChanged(MKNetworkKit中的宏定义),然后改变网络状态 *keyPath 监听属性名 *object 被观察对象 *change 属性值 *context 上下设备文,(环境) */ + (void) observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { //如果被观察对象是全局共享队列_sharedNetworkQueue且观察的属性值是线程并发数 operationCount if (object == _sharedNetworkQueue && [keyPath isEqualToString:@"operationCount"]) { /** *注册通知 *postNotificationName 同步阻塞操作(NSNotificationCenter在post消息后,会一直调用函数中会一直等待被调用函数执行完全,然后返回控制权到主函数中,再接着执行后面的功能) *object 参数 _sharedNetworkQueue全局共享队列的并发数 *这是将全局共享队列_sharedNetworkQueue的链接并发数传递给kMKNetworkEngineOperationCountChanged(改变网络状态,在MKNetworkKit中有宏定义) */ [[NSNotificationCenterdefaultCenter]postNotificationName:kMKNetworkEngineOperationCountChanged object:[NSNumber numberWithInteger:(NSInteger)[_sharedNetworkQueueoperationCount]]]; #if TARGET_OS_IPHONE //networkActivityIndicatorVisible网络活动可视指示(请求网络时状态栏上会有小图标转动,这是一个BOOL型,如果全局共享队列_sharedNetworkQueue的连接并发数大于0则为YES) [UIApplication sharedApplication].networkActivityIndicatorVisible = ([_sharedNetworkQueue.operationscount] >0); #endif } //如果观察对象不是我们想要的 else { [super observeValueForKeyPath:keyPath ofObject:object change:changecontext:context]; } } |
⑤自动改变队列大小
绝大部分的移动网络不允许两个以上的并发连接,因此一般队列在3G网络下应该设置为2。而MKNetworkKit会为自动处理队列大小问题,当网络处于 3G情况时,并发数为2,但是当网络处于WiFi环境下时,则自动调整到6。实现demo在MKNetworkEngine.m类中,首先是在初始化方法中注册通知,用来监听网络类型,当网络类型发生变化时,随即队列大小也一同改变。具体实现demo如下:
- (id) initWithHostName:(NSString*) hostName portNumber:(int)portNumber apiPath:(NSString*) apiPath customHeaderFields:(NSDictionary*) headers; |
注册通知:
if(hostName) { /** *注册通知 * *reachabilityChanged 网络类型改变时调用 *观察对象为 name *即:kReachabilityChangedNotification(网络类型) */ [[NSNotificationCenterdefaultCenter]addObserver:self selector:@selector(reachabilityChanged:) name:kReachabilityChangedNotification object:nil]; //域名 self.hostName = hostName; /** *调用了Reachability.m中的 * +(Reachability*)reachabilityWithHostname:(NSString*)hostname { //用来保存创建测试连接返回的引用 //第一个参数可以为NULL或kCFAllocatorDefault,第二个参数为需要测试连接的IP地址 SCNetworkReachabilityRef ref = SCNetworkReachabilityCreateWithName(NULL, [hostname UTF8String]); //如果拥有 if (ref) { //初始化赋值 id reachability = [[self alloc] initWithReachabilityRef:ref];
#if __has_feature(objc_arc) //ARC情况下 return reachability; #else //mARC情况下 return [reachability autorelease]; #endif
}
return nil; }
*/ self.reachability = [ReachabilityreachabilityWithHostname:self.hostName]; //开启一个异步线程 dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT,0), ^{ //启动通知 [self.reachabilitystartNotifier]; }); } |
通知触发的方法:
//通知触发的事件,转变网络状态 -(void) reachabilityChanged:(NSNotification*) notification { //如果当前网络类型为WiFi,并将队列并发连接最大数设置为6 if([self.reachabilitycurrentReachabilityStatus] ==ReachableViaWiFi) { DLog(@"Server [%@] is reachable via Wifi",self.hostName); //将队列并发连接最大数设置为6 [_sharedNetworkQueuesetMaxConcurrentOperationCount:6]; //检查和恢复冷冻操作 [self checkAndRestoreFrozenOperations]; } //如果当前网络类型为WWAN,将队列并发连接最大数设置为0 else if([self.reachabilitycurrentReachabilityStatus] ==ReachableViaWWAN) { if(self.wifiOnlyMode) {
DLog(@" Disabling engine as server [%@] is reachable only via cellular data.",self.hostName); [_sharedNetworkQueuesetMaxConcurrentOperationCount:0]; } //如果为蜂窝数据类型,将队列并发连接最大数设置为2 else { DLog(@"Server [%@] is reachable only via cellular data",self.hostName); [_sharedNetworkQueuesetMaxConcurrentOperationCount:2]; //检查和恢复冷冻操作 [self checkAndRestoreFrozenOperations]; } } //未知网络 else if([self.reachabilitycurrentReachabilityStatus] ==NotReachable) { DLog(@"Server [%@] is not reachable",self.hostName); //调用冷冻操作 [self freezeOperations]; } if(self.reachabilityChangedHandler) { self.reachabilityChangedHandler([self.reachability currentReachabilityStatus]); } }
|
⑥自动缓存
MKNetworkKit会自动缓存所有的GET请求,当再次发送相同的请求时,MKNetworkKit随即就能调用response缓存(可用条件下)传递给handler进行处理。而且也会同时向服务器发出请求,一旦获取服务器数据,handler会被再次要求处理新的数据。也就是我们只需要使用:
[[MKNetworkEngineshareEngine]useCache];
也可以子类化覆写这个方法,自定义缓存路径和缓存占用的内存开销。
⑦冻结网络操作
MKNetworkKit能够“冻结”网络操作。在一个网络被“冻结”情况下,一旦网络断开,它们将自动序列化并在设备再次连接网络时在被提交一次。
/** *冷冻操作,如果网络被标记为可冻结,一旦网络连接失败,它们将自动序列化并在设备再次连线时自动被提交一次 *首先判断是不是启动缓存,符合启动缓存的条件才能继续执行,然后需要判断是否被标记为可冷冻,并且需要拥有域名,而且域名中要存在操作的url字符串。符合这些条件后,需要创建路径,然后将此操作持久化归档在这个路径中,然后就可以取消此次操作了 */ -(void) freezeOperations { //如果不是isCacheEnabled启动缓存,直接返回结束 if(![selfisCacheEnabled])return; //遍历全局共享线程中的操作,并由MKNetworkOperation实例化接收 for(MKNetworkOperation *operationin_sharedNetworkQueue.operations) { //如果不是可冻结操作,跳过此次循环 // freeze only freeable operations.只能冻结操作 if(![operation freezable]) continue; //如果没有域名,直接结束遍历 if(!self.hostName)return; // freeze only operations that belong to this server冻结操作只属于此服务器 /** *如果在域名中没有url的绝对字符串,则跳过此次循环 *rangeOfString 前面的参数是要求被搜索的字符串,后面的是要搜索的字符串 *NSNotFound 表示请求操作的某个内容或者item没有发现,或者不存在 */ if([[operation url] rangeOfString:self.hostName].location ==NSNotFound)continue; /** *档案路径 *cacheDirectoryName 缓存目录名称 *stringByAppendingPathComponent连接路径 *NSKeyedArchiver 数据持久化归档,用来数据存储 *uniqueIdentifier 唯一标示符 *stringByAppendingPathExtension添加扩展路径 *archiveRootObject 档案根对象 */ NSString *archivePath = [[[selfcacheDirectoryName]stringByAppendingPathComponent:[operationuniqueIdentifier]] stringByAppendingPathExtension:kFreezableOperationExtension]; [NSKeyedArchiver archiveRootObject:operation toFile:archivePath]; //操作取消 [operation cancel]; } } //检查和恢复冷冻操作,用来再次执行那些网络连接失败的操作 -(void) checkAndRestoreFrozenOperations { //如果没有启动缓存,直接返回 if(![selfisCacheEnabled])return; //构建错误内容 NSError *error = nil; /** *NSFileManager 能够执行许多通用文件系统操作并隔离应用程序与底层文件系统 *defaultManager 默认 *contentsOfDirectoryAtPath:[self cacheDirectoryName]获取缓存路径中的内容 */ NSArray *files = [[NSFileManagerdefaultManager]contentsOfDirectoryAtPath:[selfcacheDirectoryName]error:&error]; //如果出错 if(error) DLog(@"%@", error); /** *filteredArrayUsingPredicate用谓语筛选数组,返回一个新数组。 *evaluatedObject 求值对象 *bindings 绑定 *pendingOperations 待定操作 */ NSArray *pendingOperations = [filesfilteredArrayUsingPredicate:[NSPredicatepredicateWithBlock:^BOOL(id evaluatedObject,NSDictionary *bindings) { //将求值对象转换为字符串 NSString *thisFile = (NSString*) evaluatedObject; //rangeOfString:获取指定短字符串在长字符串中的开始,结尾索引值 //判断thisFile里面是否是否拥有kFreezableOperationExtension,有的话返回 return ([thisFile rangeOfString:kFreezableOperationExtension].location !=NSNotFound); }]]; //遍历待定操作的数组 for(NSString *pendingOperationFilein pendingOperations) { //获取这些待定操作的路径 NSString *archivePath = [[selfcacheDirectoryName]stringByAppendingPathComponent:pendingOperationFile]; //将这些路径字符串赋值为MKNetworkOperation对象 MKNetworkOperation *pendingOperation = [NSKeyedUnarchiverunarchiveObjectWithFile:archivePath]; //入队操作 [self enqueueOperation:pendingOperation]; NSError *error2 = nil; //入队操作后移除archivePath [[NSFileManager defaultManager] removeItemAtPath:archivePatherror:&error2]; if(error2) DLog(@"%@", error2); } }
|
⑧类似的请求只执行一个操作
当我们加载缩略图(针对 推ter stream)时,最终要为每个实际的图片创建一个新的请求。而事实上我们请求的都是同一个URL。MKNetworkKit对于队列中的每个GET请求都只会执行一次。在MKNetworkEngine.m中存在这样的方法:
//取消包含相同URL字符串的操作 +(void) cancelOperationsContainingURLString:(NSString*) string { //取消相同操作 [self cancelOperationsMatchingBlock:^BOOL (MKNetworkOperation* op) { /** *看传进来的字符串string中是否有request的请求url,只要能发现请求操作的内容,就返回 *absoluteString 绝对字符串(NSURL基本方法 absoluteString,就是完整的URL字符串) *rangeOfString 前面的参数是要求被搜索的字符串,后面的是要搜索的字符串 *NSNotFound 表示请求操作的某个内容或者item没有发现,或者不存在 */ return [[op.readonlyRequest.URLabsoluteString]rangeOfString:string].location !=NSNotFound; }]; } //取消相同的操作 +(void) cancelOperationsMatchingBlock:(BOOL (^)(MKNetworkOperation* op))block { //获取全局共享队列_sharedNetworkQueue的正在进行的所有操作 NSArray *runningOperations =_sharedNetworkQueue.operations; //enumerateObjectsUsingBlock枚举对象使用块 [runningOperations enumerateObjectsUsingBlock:^(id obj,NSUInteger idx,BOOL *stop) {
MKNetworkOperation *thisOperation = obj; //如果这个操作存在就取消这个操作 if (block(thisOperation)) [thisOperation cancel]; }]; } //取消所有操作 -(void) cancelAllOperations { //是否存在存在域名 if(self.hostName) { //取消所有包含当前域名字符串的操作 [MKNetworkEngine cancelOperationsContainingURLString:self.hostName]; } else { //没有构建域名,不能取消操作 DLog(@"Host name is not set. Cannot cancel operations."); } } |
⑨图片缓存
MKNetworkKit内置了缩略图缓存。只需要子类化覆盖几个方法便可以自定义设置内存中最大能缓存的图片的数量,以及缓存目录的路径。
如下方法,直接将内存缓存存入硬盘,随即销毁内存缓存。
//保存缓存,将内存缓存转移到硬盘中 -(void) saveCache { //遍历内存缓存的所有key for(NSString *cacheKey in [self.memoryCache allKeys]) { //拼接路径 NSString *filePath = [[self cacheDirectoryName] stringByAppendingPathComponent:cacheKey]; //如果所在路径存在文件 if([[NSFileManager defaultManager] fileExistsAtPath:filePath]) {
NSError *error = nil; //移除项目路径 [[NSFileManager defaultManager] removeItemAtPath:filePath error:&error]; ELog(error); }
[(self.memoryCache)[cacheKey] writeToFile:filePath atomically:YES]; } //移除所有对象 [self.memoryCache removeAllObjects]; [self.memoryCacheKeys removeAllObjects]; //拼接字符串 NSString *cacheInvalidationPlistFilePath = [[self cacheDirectoryName] stringByAppendingPathExtension:@"plist"]; //将可变字典cacheInvalidationParams写入到cacheInvalidationPlistFilePath中 [self.cacheInvalidationParams writeToFile:cacheInvalidationPlistFilePath atomically:YES]; } |
⑩性能
MKNetworkKit缓存是内置的,就如NSCache,当发现内存警告时,缓存到内存的数据将被写入缓存目录。实现方法是注册通知,监听内存警告,当发生内存警告时,便调用saveCache方法,将内存缓存写入缓存目录,并移除内存缓存。
//注册通知,收到内存警告时调用saveCache方法 [[NSNotificationCenterdefaultCenter]addObserver:selfselector:@selector(saveCache) name:UIApplicationDidReceiveMemoryWarningNotification object:nil];
|
三、MKNetworkKit的使用
MKNetworkKit 的用法也很简单,这里就仅仅实现一个get方法,加载一个网络图片。在此方法中,发送request请求的操作加入请求队列时就执行了,很明显的说明了MKNetworkKit框架很好的封装性。
首先,我们新建一个工程,然后将MKNetworkKit文件夹添加进来,记得copy。
因为MKNetworkKit完全支持Arc机制,所以此时我们只用导入 SystemConfiguration.framework,CFNetwork.framework,Security.framework和 ImageIO.framework。
然后,在工程中Supporting Files 文件夹中的以 .pch 的文件中加入“MKNetworkKit.h”类,这时,你就可以随时随地调用MKNetworkKit中的方法了。
#ifdef __OBJC__ #import <UIKit/UIKit.h> #import <Foundation/Foundation.h> #import "MKNetworkKit.h" |
或者,你只需要在你需要使用MKNetworkKit的类中导入”MKNetworkKit.h”就行了。
加载网络图片demo示例:
//加载图片的URLhttp://pic1.win4000.com/pic/4/3f/4124407336.jpg
/**
*MKNetworkEngine 可以理解为MKNetworkKit的核心,是MKNetworkKit的网络引擎
*HostName 域名
*apiPath 域名后面跟着的路径(path在Engine与operation中都一样)
*customHeaderFields 自定义头域
*/
MKNetworkEngine *engine = [[MKNetworkEnginealloc]initWithHostName:@"pic1.win4000.com"apiPath:@"pic/4/3f/4124407336.jpg"customHeaderFields:Nil];
/**
*MKNetworkOperation 网络操作(默认的是get方法)
*operationWithPath 操作路径(域名后面跟着的路径)
*params 参数
*httpMethod http方法
*/
MKNetworkOperation *operation = [engineoperationWithPath:Nilparams:NilhttpMethod:@"GET"];
//添加完成处理程序
[operation addCompletionHandler:^(MKNetworkOperation *completedOperation) {
//请求成功,为_imgView添加图片
_imgView.image = [UIImage imageWithData:[completedOperation responseData]];
} errorHandler:^(MKNetworkOperation *completedOperation, NSError *error) {
//请求出错
NSLog(@"%@",completedOperation.error);
}];
//发起网络请求
[engine enqueueOperation:operation];