最近项目中需要做一个后台多线程上传图片的功能,你可能觉得这个有什么难,iOS自带的框架中使用NSURKSessionUploadTask和多线程的相关类就能实现,拜托,要是这么简单我就不会单独写个博客。
由于在开发中需要使用第三方图片存储服务器(upyun)的API,其实他们也是用的AFN,只是我不想去改它的接口,以免对项目的其他模块造成影响,所以就只能自己来实现上传的相关操作,与此同时还要兼顾本地化的相关内容。
下面是需求:
1、当网络条件不好时用户可以保存当前的上传任务,以便在其他时间上传;
2、上传任务还要保存在本地,以便在程序退出后再进入时也能进行未完成的任务(不是断点续传哈)
3、对于正在上传的任务或者本地存储的任务,用户可以选择删除
需求分析:
1、本地化采用sqlite(FMDB)同时要考虑到线程同步
2、上传操作要使用多线程,由于用户可能会删除任务,上传操作就可以停止和恢复,所以选择NSOpreationQueue和NSOperation的相关类进行封装
3、对数据库和操作数组(把所有的NSOperation记录在一个数组中)要注意线程同步,由于数据库使用了FMDB来操作,它本身就是线程安全的,所以就只考虑管理操作数组的线程安全,考虑使用@synchronized()或者GCD的SERIAL类型的queue,等一下我再说选哪个
4、上传操作肯定要使用一个类来管理上传任务,另一个类来做具体的上传动作。管理类需要使用单例,因为只需要一个管理者就行了,调用方法时也只导入这个类。所以就需要这两个类
UploadTask.h
UploadTask.m
UploadTaskManager.h
UploadTaskManager.m
下面来看看每个类应该实现的功能
#import
@class BackgroundTaskModel;
typedef void(^UploadTaskProgress)(CGFloat progress);
typedef void(^UploadTaskComplete)(BOOL result, id taskModel);
@interface UploadTask : NSObject
@property (nonatomic, strong) BackgroundTaskModel *model;
@property (nonatomic, copy) UploadTaskProgress progress;
@property (nonatomic, copy) UploadTaskComplete complete;
@property (nonatomic, strong) NSNumber *progressValue;
+ (instancetype)taskWithModel:(BackgroundTaskModel *)model progress:(UploadTaskProgress)progress complete:(UploadTaskComplete)complete;
- (NSOperation *)getTaskOperation;//方便取得任务对应的Opreation对象,就能进行暂停
- (void)stopTask;//实际的停止动作,为什么和上面那个分开,上传图片时实际上是一张一张的传,operation中block里面的block没法停止
@end
说明一下,BackgroundTaskModel是一个模型类,里面只有这些
@property (nonatomic, assign) NSInteger taskID;
@property (nonatomic, strong) NSString *taskURL;
@property (nonatomic, strong) NSMutableDictionary *para;
@property (nonatomic, strong) NSString *picKey;
@property (nonatomic, strong) NSString *saveKey;
@property (nonatomic, strong) NSMutableArray *photos;
@property (nonatomic, assign) long updateTime;
@property (nonatomic, assign) NSInteger taskType;
@property (nonatomic, strong) NSString *title;
/**
任务状态 0准备 1上传中
*/
@property (nonatomic, assign) NSInteger taskStatus;
typedef void(^BackgroudTaskUploadBlock)(NSMutableArray *photoArrayUrls);
@interface UploadTask ()
@property (nonatomic, strong) NSBlockOperation *operation;
@property (nonatomic, assign) BOOL isStop;
@end
@implementation UploadTask
+ (instancetype)taskWithModel:(BackgroundTaskModel *)model progress:(UploadTaskProgress)progress complete:(UploadTaskComplete)complete {
UploadTask *manager = [[UploadTask alloc] init];
manager.model = model;
manager.progress = progress;
manager.complete = complete;
[manager startOperation];
return manager;
}
- (NSOperation *)getTaskOperation {
return self.operation;
}
- (void)startOperation {
_isStop = NO;
__weak typeof(self) weakSelf = self;
self.operation = [NSBlockOperation blockOperationWithBlock:^{
//对弱引用对象进行强引用,避免self对象销毁后,调用此方法时会引起崩溃
__strong typeof(weakSelf) strongWSelf = weakSelf;
[strongWSelf uploadFile:_model.photos saveKey:_model.saveKey complete:^(NSMutableArray *photoArrayUrls) {
NSMutableString *photourls=[[NSMutableString alloc]init];
for (NSInteger i=0; i"%@,",[photoArrayUrls objectAtIndex:i]];
}
if (photourls.length!=0)
{
[photourls setString:[photourls substringToIndex:photourls.length-1]];
}
[_model.para setObject:photourls forKey:_model.picKey];
//这是在上传完成后,把图片的名称和其他的参数传给自己的服务器
NSDictionary *result=[RequestHelper GetResponseDictionary:_model.taskURL postParam:_model.para];
[weakSelf finishUpoadWithResult:result];
} progress:^(CGFloat progress) {
if (progress < weakSelf.progressValue.floatValue) {
progress = weakSelf.progressValue.floatValue + 0.05;
}
if (weakSelf.progress) {
dispatch_async(dispatch_get_main_queue(), ^{
weakSelf.progressValue = @(progress);
weakSelf.progress(progress);
});
}
NSLog(@"%f", progress);
}];
}];
}
//
- (void)finishUpoadWithResult:(NSDictionary *)result {
dispatch_async(dispatch_get_main_queue(), ^{
if ([BaseBll codeSuccess:result]) {
if (self.progress) {
self.progressValue = @(1);
self.progress(1.0);
}
if (self.complete) {
self.complete([BaseBll codeSuccess:result], _model);
}
} else {
if (self.complete) {
self.complete(NO, result[@"msg"]);
}
}
});
}
- (void)uploadFile:(NSMutableArray*)photoArray saveKey:(NSString *)saveKey complete:(BackgroudTaskUploadBlock)complete progress:(UploadTaskProgress)progress {
//如果没有图片就直接返回
if(photoArray.count==0 || (photoArray.count == 1 && [photoArray.firstObject isEqualToString:@""])){
if (complete) {
complete(photoArray);
}
}
NSMutableArray *upfileArray=[[NSMutableArray alloc]init];
NSString *upfileSaveKey=[NSString stringWithFormat:@"/%@/%@",[[[LoginBll alloc]init] getCompanyCode],saveKey];
UpYun *uy = [[UpYun alloc] init];
__block int upfileCount=0;
//这里使用信号量来控制上传,为什么,因为UpYun的操作不支持队列,导致这里计算进度时不准确,所以要等第一张图片传完了再传第二张
dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);
for (NSInteger i=0; i//如果已经停止就不要再上传了,免得占用带宽
if (_isStop) {
break;
}
NSString *newFileName=[photoArray objectAtIndex:i];
uy.successBlocker = ^(id data)
{
upfileCount=upfileCount+1;
if(upfileCount==photoArray.count){
for (NSInteger i=0; i"/%@/%@",saveKey,[photoArray objectAtIndex:i]];
[upfileArray addObject:upfileUrl];
}
//全部上传完成
if(complete)
complete(upfileArray);
}
dispatch_semaphore_signal(semaphore);
};
uy.failBlocker = ^(NSError * error)
{
NSString *message = [error.userInfo objectForKey:@"message"];
if (!message) {
message = [NSString stringWithFormat:@"%@", [error localizedDescription]];
}
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"error" message:message preferredStyle:UIAlertControllerStyleAlert];
UIAlertAction *cancel = [UIAlertAction actionWithTitle:@"取 消" style:UIAlertActionStyleCancel handler:nil];
[alert addAction:cancel];
//这是一个获取顶层ViewController的方法
UIViewController *vc = [ToolsCenter getTopControllerFromViewController:[UIApplication sharedApplication].keyWindow.rootViewController];
[vc presentViewController:alert animated:YES completion:nil];
dispatch_semaphore_signal(semaphore);
// NSLog(@"%@",error);
};
uy.progressBlocker = ^(CGFloat percent, long long requestDidSendBytes)
{
if (progress) {
if (upfileCount != photoArray.count) {
progress(0.9 / (photoArray.count) * (upfileCount + percent));
}
}
};
//带路径的文件名
NSString *fullFilePathName= [PhotoHelper dataPath:newFileName];
NSData *imageData=[NSData dataWithContentsOfFile: fullFilePathName];
//上传的URL,这里的upfileSaveKey是UpYun上传时的参数,用来标记服务端的文件存储路径
NSString *uploadUrl=[NSString stringWithFormat:@"%@/%@",upfileSaveKey,newFileName];
[uy uploadImageData:imageData savekey:uploadUrl];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);
}
}
- (void)stopTask {
_isStop = YES;
}
@end
#import
@class BackgroundTaskModel;
@class BackgroundTaskBll;
typedef void(^BackgoundTaskCompleteBlock)(BOOL result, id model);
typedef void(^BackgroudTaskProgress)(CGFloat progress);
@interface UploadTaskManager : NSObject
@property (nonatomic, strong, readonly) NSOperationQueue *queue;
+ (instancetype)shareManger;
- (void)addBackgroundTask:(BackgroundTaskModel *)model complete:(BackgoundTaskCompleteBlock)complete; //添加任务,马上进行
- (void)addTask:(BackgroundTaskModel *)model; //添加任务,但是不进行上传
- (void)cancelTask:(BackgroundTaskModel *)model;
- (void)startTask:(BackgroundTaskModel *)model progress:(BackgroudTaskProgress)progress complete:(BackgoundTaskCompleteBlock)complete; //开始上传,cell中使用的,任何时候都可以取得任务的进度
@end
@interface UploadTaskManager () {
NSMutableArray *_tasks;
}
@property (nonatomic, strong) dispatch_queue_t queue_t;
@end
static UploadTaskManager *_manager = nil;
@implementation UploadTaskManager
+ (instancetype)shareManger {
if (_manager) {
return _manager;
}
_manager = [[self alloc] init];
return _manager;
}
- (instancetype)init {
if (_manager) {
return _manager;
}
if (self = [super init]) {
_queue = [[NSOperationQueue alloc] init];
_tasks = [NSMutableArray array];
//这里为什么要用DISPATCH_QUEUE_SERIAL队列,因为我发现@synchronized()的效率没有这个好,还有队列中的任务一定会进行,但是@synchronized()中的就不一定了
_queue_t = dispatch_queue_create("data_queue", DISPATCH_QUEUE_SERIAL);
}
return self;
}
#pragma mark - Public Method
- (void)addBackgroundTask:(BackgroundTaskModel *)model complete:(BackgoundTaskCompleteBlock)complete {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self addTask:model];
[self startTask:model progress:nil complete:complete];
});
}
- (void)addTask:(BackgroundTaskModel *)model {
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:[DBHelper dbGetPath]];
// 如果要支持事务
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
NSMutableString *photoString = [NSMutableString string];
for (NSString *photoName in model.photos) {
[photoString appendFormat:@"%@,", photoName];
}
if (photoString.length > 0) {
photoString = [[photoString substringToIndex:photoString.length - 1] mutableCopy];
}
NSInteger time = [NSDate date].timeIntervalSince1970;
NSInteger taskID = arc4random() % 1000 + 1;
model.taskID = taskID;
NSInteger userid = [LoginBll getUserRemoteIDToLong];
BOOL executeResult = NO;
NSString *sql = @"insert into TAB_BACKGROUND_TASK ";
sql = [sql stringByAppendingString:@"(COL_ID, COL_URL, COL_PHTOTS, COL_SAVE_KEY, COL_PARA, COL_PIC_KEY, COL_UPDATE_TIME, COL_TITLE, COL_TASK_TYPE, COL_USER_ID) "];
sql = [sql stringByAppendingString:@"values (?,?,?,?,?,?,?,?,?,?)"];
executeResult = [db executeUpdate:sql, @(taskID), model.taskURL, photoString, model.saveKey, [self stringFromDic:model.para], model.picKey, @(time), model.title, @(model.taskType), @(userid)];
if (!executeResult) {
*rollback = YES;
}
}];
}
//取消任务,因为不能打断UI,所以就要在新的线程中进行,但是对_tasks的操作需要线程保护,不然就容易乱
- (void)cancelTask:(BackgroundTaskModel *)model {
dispatch_async(_queue_t, ^{
[self changeTask:model Status:0];
NSString *name = [NSString stringWithFormat:@"%ld", (long)model.taskID];
UploadTask *findTask = nil;
for (UploadTask *task in _tasks) {
NSOperation *operation = [task getTaskOperation];
if ([operation.name isEqualToString:name]) {
[operation cancel];
findTask = task;
}
}
if (findTask) {
[_tasks removeObject:findTask];
findTask.progress = nil;
findTask.complete = nil;
[findTask stopTask];
}
});
}
//开始任务后,首先要检测当前有没有同一个的任务进行,如果有就返回该任务的进程,如果没有就开始任务。同样的要进行线程同步
- (void)startTask:(BackgroundTaskModel *)model progress:(BackgroudTaskProgress)progress complete:(BackgoundTaskCompleteBlock)complete {
dispatch_async(_queue_t, ^{
[self changeTask:model Status:1];
NSString *name = [NSString stringWithFormat:@"%ld", (long)model.taskID];
BOOL isFound = NO;
for (UploadTask *task in _tasks) {
NSOperation *operation = [task getTaskOperation];
if ([operation.name isEqualToString:name]) {
dispatch_async(dispatch_get_main_queue(), ^{
if (progress) {
task.progress = progress;
progress(task.progressValue.floatValue);
}
if (complete) {
task.complete = complete;
}
});
isFound = YES;
break;
}
}
if (!isFound) {
UploadTask *task = [UploadTask taskWithModel:model progress:progress complete:^(BOOL result, id taskModel) {
dispatch_async(dispatch_get_main_queue(), ^{
if (complete) {
complete(result, taskModel);
}
});
if (result) {
[[[BackgroundTaskBll alloc] init] deleteTask:taskModel];
}
}];
NSOperation *operation = [task getTaskOperation];
operation.name = [NSString stringWithFormat:@"%ld", (long)model.taskID];
[_queue addOperation:operation];
[_tasks addObject:task];
}
});
}
#pragma mark - Private Method
- (void)changeTask:(BackgroundTaskModel *)mode Status:(NSInteger)status {
FMDatabaseQueue *queue = [FMDatabaseQueue databaseQueueWithPath:[DBHelper dbGetPath]];
// 如果要支持事务
[queue inTransaction:^(FMDatabase *db, BOOL *rollback) {
BOOL executeResult = NO;
NSString *sql = @" update TAB_BACKGROUND_TASK set COL_TASK_STATUS = ? where COL_ID = ?";
executeResult = [db executeUpdate:sql, @(status), @(mode.taskID)];
if (!executeResult) {
*rollback = YES;
}
}];
}
- (NSString *)stringFromDic:(NSDictionary *)dic {
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
[dic enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) {
NSString *keyString = nil;
NSString *valueString = nil;
if ([key isKindOfClass:[NSString class]]) {
keyString = key;
}else{
keyString = [NSString stringWithFormat:@"%@",key];
}
if ([obj isKindOfClass:[NSString class]]) {
valueString = obj;
}else{
valueString = [NSString stringWithFormat:@"%@",obj];
}
[dict setObject:valueString forKey:keyString];
}];
NSData *data = [NSJSONSerialization dataWithJSONObject:dict options:NSJSONWritingPrettyPrinted error:nil];
if (data) {
return [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
} else {
return @"{}";
}
}
@end
写在最后,在构建这个现在这个方案时,我还想过就在一个类中实现任务的添加和管理,但是发现不行,因为这个操作是带有进度回调block和完成回调block的,我试过用runtime给operation对象添加属性,但是在这一步
- (void)startOperation {
}
如果换成NSBlockOperation来直接创建队列,那么这两个回调只能在operation对象创建后赋值,但是在以上的代码中可以发现,在创建operation对象的block中就需要使用回调,所以必须单独创建一个类来绑定回调block和operation对象。
其实本来就应该这样设计的,不同的功能就应该分成不同的类去实现