关于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的刷新必须在某一个数据库操作节点之后,或者是全部完成之后才可刷新。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值