關於iOS NSOperation 自定義的那些事兒
阿新 • • 發佈:2019-01-03
在常規的開發中很少使用到場景比較複雜的多執行緒技術,一般用於網路下載或者一些邏輯的運算。
在日程開發的過程中,前端僅僅只是一個數據的展示,很多邏輯的問題都是交給後臺伺服器去處理,在去年連續遇到了兩個比較特殊的專案,這兩個專案要求支援離線使用了和考慮大使用者群體的問題,將邏輯運算放置在了前端,後臺僅僅是一個數據儲存的作用,不會參雜邏輯的運算去處理。
整個邏輯層的結構比較簡單,首先,UI層,邏輯層和網路層,沒有直接關係,相互獨立,通過介面呼叫實現互動。可以簡單的理解,UI和網路層之間是用一個數據庫關聯的,兩者都是操作資料庫,從資料庫裡面去讀取,修改資料,這裡就涉及到了一個執行緒安全和資料安全的問題,後臺回來沒有資料的處理能力,比對顧慮,修改新增都放置在了前端,很多時候,很容易出現執行緒的混亂,導致資料的錯亂。
很多人都認為使用FMDB就可以避免這樣的問題。雖然FMDB是執行緒安全,但是這裡也僅是在簡單的操作場景中安全,FMDB本身沒有資料識別能力。
在前端使用執行緒的解決方案常用的有兩種,第一種是apple首推的GCD, 另外一個是稍微底層一點NSOperationQueue和NSOperation。
這裡先為大家提供一種NSOperation的解決方案。
首先,需要寫一個用於管理執行緒的管理類,在邏輯上,我們可以為我們的管理類加多條執行緒(建議3條以下,能儘量少用就少用,開啟執行緒是佔用CPU和記憶體的),舉例子為兩條:
.h檔案
.m檔案// // QYJOperationQueueManager.h // CustomOperation // // Created by qyji on 17/2/3. // Copyright © 2017年 qyji. All rights reserved. // #import <Foundation/Foundation.h> #import "QYJCustomerOperationHeader.h" @class QYJNetRequestModel; /** * 用於回撥重新整理介面或者進行其它操作 */ @protocol QYJOperationQueueManagerDelegate <NSObject> - (void)refreshUI; @end @interface QYJOperationQueueManager : NSObject //用於介面互動的,本人是比較喜歡使用block的,後期維護起來感覺程式碼塊太過於大,巢狀得比較深,不易閱讀,故改用delegate和protocol @property (weak, nonatomic) id<QYJOperationQueueManagerDelegate>delegate; /* @method shareOperationManager @abstrac 獲取一個QYJOperationQueueManager的物件,用於管理執行緒 @discussion 獲取一個QYJOperationQueueManager的物件,用於管理執行緒 @param No param @result return QYJOperationQueueManager's object */ + (QYJOperationQueueManager *)shareOperationManager; /* @method addSyncOperation @abstrac 發一個請求任務 @discussion 發一個一個下載請求任務 @param No param @result No return result */ - (void)addSyncOperation; /* @method addUpdataHandleDataBase @abstrac 上傳資料 @discussion 上傳資料 @param No param @result No param */ - (void)addUpdataHandleDataBase; /* @method stopSyncOperation @abstrac 停止當前下載任務 @discussion 停止當前下載任務 @param No param @result No return result */ - (void)stopSyncOperation; @end
// // QYJOperationQueueManager.m // CustomOperation // // Created by qyji on 17/2/3. // Copyright © 2017年 qyji. All rights reserved. // #import "QYJOperationQueueManager.h" static QYJOperationQueueManager *single = nil; @interface QYJOperationQueueManager () /** * 用於資料庫互動的執行緒 */ @property (nonatomic, strong) NSOperationQueue *handleDataBaseQueue; /** * 用於處理網路請求的執行緒 */ @property (nonatomic, strong) NSOperationQueue *syncQueue; /** * 用於發起上傳請求的執行緒 */ @property (nonatomic, strong) NSOperationQueue *updateQueue; @end @implementation QYJOperationQueueManager #pragma mark - life Method + (QYJOperationQueueManager *)shareOperationManager { static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ single = [[QYJOperationQueueManager alloc] init]; }); return single; } + (instancetype)allocWithZone:(struct _NSZone *)zone { single = [super allocWithZone:zone]; return single; } - (instancetype)init { if (self = [super init]) { _handleDataBaseQueue = [[NSOperationQueue alloc] init]; _handleDataBaseQueue.maxConcurrentOperationCount = 1; _handleDataBaseQueue.name = @"com.qyj.netService"; //其餘的queue同樣的建立方式 } return self; } #pragma mark - public Method - (void)addSyncOperation { //確保只有一個任務在執行 [self.syncQueue cancelAllOperations]; QYJSyncOperation *operation = [[QYJSyncOperation alloc] initWithRowsPerRequest:10 opType:ModelOpTypeIsRefresh]; [self.syncQueue addOperation:operation]; } - (void)addUpdataHandleDataBase { //類似於上面的操作, 寫一個上傳操作的Operation } - (void)stopSyncOperation { [[NSNotificationCenter defaultCenter] postNotificationName:@"operationStop" object:nil]; [self.syncQueue cancelAllOperations]; } #pragma mark - private Method #pragma mark - delegate Method @end
//
// QYJSyncOperation.h
// CustomOperation
//
// Created by qyji on 17/2/3.
// Copyright © 2017年 qyji. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "QYJCustomerOperationHeader.h"
@interface QYJSyncOperation : NSOperation
/**
* Intialization
*
* @param pages 每次請求下次的頁數,預設為10
*
* @return Operation
*/
- (instancetype)initWithRowsPerRequest:(NSInteger)pages opType:(NSUInteger)opType;
@end
.m檔案
//
// QYJSyncOperation.m
// CustomOperation
//
// Created by qyji on 17/2/3.
// Copyright © 2017年 qyji. All rights reserved.
//
#import "QYJSyncOperation.h"
@interface QYJSyncOperation ()
@property (assign, nonatomic) NSInteger rows;
@property (assign, nonatomic) NSInteger pageNumber;// 請求下載的頁index
//為了方便展示故作NSDictionary, 在實際開發中可作為一個實體model
@property (strong, nonatomic) NSDictionary *model;
//用於臨時存放資料的
@property (strong, nonatomic) NSMutableArray *datas;
//操作型別,控制是否重新整理頁面或者其它操作
@property (assign, nonatomic) ModelOpType opType;
@property (strong, nonatomic) NSURLSessionTask *sessionTask;
@end
@implementation QYJSyncOperation
- (instancetype)initWithRowsPerRequest:(NSInteger)pages opType:(NSUInteger)opType {
self = [super init];
if (self) {
_rows = pages <= 0 ? 20 : pages;
_pageNumber = 1;
_datas = @[].mutableCopy;
_opType = opType;
}
return self;
}
- (void)main {
@autoreleasepool {
if ([self isCancelled]) return;
//確保當前只有一個正在進行的任務註冊了通知
[self registerCannelNotification];
[self sync];
}
}
- (void)registerCannelNotification {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(cannelCurrentOperation) name:@"operationStop" object:nil];
}
- (void)deallocCannelNotificaion {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)sync {
self.sessionTask = [QYJPostNetWork postPath:@" " parameters:[self downloadParameters] compress:YES success:^(NSData *data) {
self.pageNumber++;
[self handleData:data];
} failure:^(NSError *error) {
NSLog(@"download error:%@", error.localizedDescription);
}];
}
#pragma mark - Helper
// 請求下載引數
- (NSDictionary *)downloadParameters {
return @{
@"這是引數":@"假裝是一個json格式的引數",
@"row":@(_rows),
@"page":@(_pageNumber)
};
}
/**
* 處理後臺返回的資料,更新本地資料
*
* @param model 返回資料封裝model
*/
- (void)handleData:(id)model {
if ([model isKindOfClass:[NSDictionary class]]) return;
if ([model[@"success"] boolValue]) {
NSLog(@"%s:download fail!!!", __func__);
return;
}
self.model = model;
[self handleNetworkData];
}
- (void)syncCompletion {
//登出通知
[self deallocCannelNotificaion];
//重新整理UI和其它操作
id<QYJOperationQueueManagerDelegate> delegate = [QYJOperationQueueManager shareOperationManager].delegate;
//這裡可以增加一個引數,進行控制是否重新整理或者其它操作
if ([delegate respondsToSelector:@selector(refreshUI)]) {
[delegate refreshUI];
}
}
- (void)handleNetworkData {
@synchronized (self.model) {
//這裡需要加上同步鎖,避免出現多處地方操作
//對資料進行增刪查改(此內容不屬於本文章的重點,不再贅述)
//如果存在有下一頁繼續下載
if ([self.model[@"hasNext"] boolValue]) [self sync];
else [self syncCompletion];
}
}
- (void)cannelCurrentOperation {
if (self.sessionTask.state == NSURLSessionTaskStateRunning) {
//停止當前的下載任務
[self.sessionTask suspend];
}
}
@end
網路請求類
//
// QYJPostNetWork.h
// CustomOperation
//
// Created by qyji on 17/2/3.
// Copyright © 2017年 qyji. All rights reserved.
//
#import <Foundation/Foundation.h>
typedef void(^Success)(id object);
typedef void(^Failure)(NSError *error);
@interface QYJPostNetWork : NSObject
/*
@method postPath:parameters:compress:success:failure:
@abstrac post請求發起
@discussion post請求發起
@param path 網路地址, param 請求引數, compress 是否需要壓縮引數, success 請求成功的回撥, failure 請求失敗的回撥
@result reture NSURLSessionTask的物件
*/
+ (NSURLSessionTask *)postPath:(NSString *)path
parameters:(NSDictionary *)param
compress:(BOOL)compress
success:(Success)success
failure:(Failure)failure;
/*
@method getPath:parameters:success:failure:
@abstrac get請求發起
@discussion get請求發起
@param path 網路地址, param 請求引數, success 請求成功的回撥, failure 請求失敗的回撥
@result reture NSURLSessionTask的物件
*/
+ (NSURLSessionTask *)getPath:(NSString *)path
parameters:(NSDictionary *)param
success:(Success)success
failure:(Failure)failure;
@end
//
// QYJPostNetWork.m
// CustomOperation
//
// Created by qyji on 17/2/3.
// Copyright © 2017年 qyji. All rights reserved.
//
#import "QYJPostNetWork.h"
@implementation QYJPostNetWork
+ (NSURLSessionTask *)postPath:(NSString *)path
parameters:(NSDictionary *)param
compress:(BOOL)compress
success:(Success)success
failure:(Failure)failure
{
NSParameterAssert(path);
path = [path stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSURL *url = [NSURL URLWithString:path];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
[request setHTTPMethod:@"POST"];
[request setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
[request setValue:@"application/json" forHTTPHeaderField:@"Accept"];
[request setTimeoutInterval:30];
if (param) {
BOOL validJSON = [NSJSONSerialization isValidJSONObject:param];
if (!validJSON) NSLog(@"*****Unvalid JSON:%@", param);
NSError *error = nil;
NSData *body = [NSJSONSerialization dataWithJSONObject:param
options:NSJSONWritingPrettyPrinted
error:&error];
if (error) {
NSLog(@"tranform parameter to JSON fail");
if (failure) failure(error);
return nil;
}
// if (compress) body = [GzipUtil compressData:body];
[request setHTTPBody:body];
}
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSURLSessionDataTask *task = [session dataTaskWithRequest:[request copy] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"sessionTask fail:%@", error.localizedDescription);
if (failure) failure(error);
return;
}
NSData *result = data;
// if (compress) result = [GzipUtil decompressData:data];
if (success) success(result);
}];
[task resume];
return task;
}
+ (NSURLSessionTask *)getPath:(NSString *)path
parameters:(NSDictionary *)param
success:(Success)success
failure:(Failure)failure
{
NSParameterAssert(path);
NSMutableString *pathString = [NSMutableString stringWithString:path];
[pathString appendString:@"?"];
[param enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
[pathString appendString:[NSString stringWithFormat:@"%@=%@&", key, obj]];
}];
[pathString deleteCharactersInRange:NSMakeRange(pathString.length-1, 1)];
NSString *urlString = [pathString stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request setHTTPMethod:@"GET"];
[request setTimeoutInterval:30];;
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSURLSessionDataTask *task = [session dataTaskWithRequest:[request copy] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
if (error) {
NSLog(@"get path fail:%@", error.localizedDescription);
if (failure) failure(error);
return;
}
if (success) success(data);
}];
[task resume];
return task;
}
@end
上述就是程式碼,這裡就是一個簡單的一個Operation的使用,這裡有一點值得注意的是,網路請求回來也是一個非同步的執行緒,這個時候就要非常注意了,在回撥的執行緒裡面做一個數據庫的操作,同時去操作資料庫,很可能會衣服資料錯亂,最常見的就是本地的資料是刪除的狀態,還沒有來得及過濾對比,就被上傳到了伺服器,造成一些垃圾資料,或者是無法刪除的問題。UI的重新整理必須在某一個數據庫操作節點之後,或者是全部完成之後才可重新整理。