1. 程式人生 > >關於iOS NSOperation 自定義的那些事兒

關於iOS NSOperation 自定義的那些事兒

在常規的開發中很少使用到場景比較複雜的多執行緒技術,一般用於網路下載或者一些邏輯的運算。

在日程開發的過程中,前端僅僅只是一個數據的展示,很多邏輯的問題都是交給後臺伺服器去處理,在去年連續遇到了兩個比較特殊的專案,這兩個專案要求支援離線使用了和考慮大使用者群體的問題,將邏輯運算放置在了前端,後臺僅僅是一個數據儲存的作用,不會參雜邏輯的運算去處理。

整個邏輯層的結構比較簡單,首先,UI層,邏輯層和網路層,沒有直接關係,相互獨立,通過介面呼叫實現互動。可以簡單的理解,UI和網路層之間是用一個數據庫關聯的,兩者都是操作資料庫,從資料庫裡面去讀取,修改資料,這裡就涉及到了一個執行緒安全和資料安全的問題,後臺回來沒有資料的處理能力,比對顧慮,修改新增都放置在了前端,很多時候,很容易出現執行緒的混亂,導致資料的錯亂。

很多人都認為使用FMDB就可以避免這樣的問題。雖然FMDB是執行緒安全,但是這裡也僅是在簡單的操作場景中安全,FMDB本身沒有資料識別能力。

在前端使用執行緒的解決方案常用的有兩種,第一種是apple首推的GCD, 另外一個是稍微底層一點NSOperationQueue和NSOperation。

這裡先為大家提供一種NSOperation的解決方案。

首先,需要寫一個用於管理執行緒的管理類,在邏輯上,我們可以為我們的管理類加多條執行緒(建議3條以下,能儘量少用就少用,開啟執行緒是佔用CPU和記憶體的),舉例子為兩條:

 .h檔案

//
//  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
.m檔案

//
//  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


在上面的.m檔案上寫了一個QYJSyncOperation ,下面是相關程式碼

//
//  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的重新整理必須在某一個數據庫操作節點之後,或者是全部完成之後才可重新整理。