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];