在常规的开发中很少使用到场景比较复杂的多线程技术,一般用于网络下载或者一些逻辑的运算。
在日程开发的过程中,前端仅仅只是一个数据的展示,很多逻辑的问题都是交给后台服务器去处理,在去年连续遇到了两个比较特殊的项目,这两个项目要求支持离线使用了和考虑大用户群体的问题,将逻辑运算放置在了前端,后台仅仅是一个数据保存的作用,不会参杂逻辑的运算去处理。
整个逻辑层的结构比较简单,首先,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
//
// 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的刷新必须在某一个数据库操作节点之后,或者是全部完成之后才可刷新。