上传任务的分享

上传任务的多线程实现与分析

最近项目中需要做一个后台多线程上传图片的功能,你可能觉得这个有什么难,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

下面来看看每个类应该实现的功能

UploadTask.h

#import <Foundation/Foundation.h>

@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;

UploadTask.m

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.count; i++) {
                [photourls appendFormat:@"%@,",[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<photoArray.count; 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<photoArray.count; i++) {
                    NSString *upfileUrl=[NSString stringWithFormat:@"/%@/%@",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

UploadTaskManager.h

#import <Foundation/Foundation.h>

@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

UploadTaskManager.m

@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对象。

其实本来就应该这样设计的,不同的功能就应该分成不同的类去实现

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值