美文网首页iOS开发
iOS 上传大文件(切片上传)

iOS 上传大文件(切片上传)

作者: 小和大大 | 来源:发表于2021-04-09 14:50 被阅读0次

    <article class="_2rhmJa">

    在开发项目中,我们有可能会遇到上传大文件,比如几百兆,甚至于一个G,因此我们不能直接拿到文件的Path直接转化成NSData文件上传,这样的话我们会在项目中引用这个大的Data对象,可能直接会导致项目的运行内存暴涨,程序被强退.既然我们不能使用以前的方法,那么我们可以使用拿到文件所在的路径,然后将文件划分成数个小文件上传,这个其实也就是我们平时所说的分片上传,在这里我使用的是简单的上传方法,也就是一般项目中所用的,退出app不会保存上传记录的,废话不多说,直接上代码吧.
    FileStreamOperation.h文件

    #import <Foundation/Foundation.h>
    
    #define FileFragmentMaxSize         1024 *1024 // 1MB
    
    @class FileFragment;
    
    /**
     * 文件流操作类
     */
    @interface FileStreamOperation : NSObject<NSCoding>
    @property (nonatomic, readonly, copy) NSString *fileName;//包括文件后缀名的文件名
    @property (nonatomic, readonly, assign) NSUInteger fileSize;//文件大小
    @property (nonatomic, readonly, copy) NSString *filePath;//文件所在的文件目录
    @property (nonatomic, readonly, strong) NSArray<FileFragment*> *fileFragments;//文件分片数组
    
    + (instancetype)sharedOperation;
    //若为读取文件数据,打开一个已存在的文件。
    //若为写入文件数据,如果文件不存在,会创建的新的空文件。(创建FileStreamer对象就可以直接使用fragments(分片数组)属性)
    - (instancetype)initFileOperationAtPath:(NSString*)path forReadOperation:(BOOL)isReadOperation ;
    
    //获取当前偏移量
    - (NSUInteger)offsetInFile;
    
    //设置偏移量, 仅对读取设置
    - (void)seekToFileOffset:(NSUInteger)offset;
    
    //将偏移量定位到文件的末尾
    - (NSUInteger)seekToEndOfFile;
    
    //关闭文件
    - (void)closeFile;
    
    #pragma mark - 读操作
    //通过分片信息读取对应的片数据
    - (NSData*)readDateOfFragment:(FileFragment*)fragment;
    
    //从当前文件偏移量开始
    - (NSData*)readDataOfLength:(NSUInteger)bytes;
    
    //从当前文件偏移量开始
    - (NSData*)readDataToEndOfFile;
    
    #pragma mark - 写操作
    //写入文件数据
    - (void)writeData:(NSData *)data;
    
    @end
    
    typedef NS_ENUM(NSInteger, FileUpState)
    {
        FileUpStateWaiting = 0,//加入到数组
        FileUpStateLoading = 1,//正在上传
        FileUpStateSuccess = 2//上传成功
    };
    
    //上传文件片
    @interface FileFragment : NSObject<NSCoding>
    @property (nonatomic,copy)NSString          *fragmentId;    //片的唯一标识
    @property (nonatomic,assign)NSUInteger      fragmentSize;   //片的大小
    @property (nonatomic,assign)NSUInteger      fragementOffset;//片的偏移量
    @property (nonatomic,assign)FileUpState            fragmentStatus; //上传状态 
    @end
    
    

    FileStreamOperation.m

    #import "FileStreamOperation.h"
    #import <CommonCrypto/CommonDigest.h>
    
    //// 把FileStreamOpenration类保存到UserDefault中
    //static NSString *const UserDefaultFileInfo = @"UserDefaultFileInfo";
    
    #pragma mark - FileStreamOperation
    
    @interface FileStreamOperation ()
    @property (nonatomic, copy) NSString                          *fileName;
    @property (nonatomic, assign) NSUInteger                      fileSize;
    @property (nonatomic, copy) NSString                          *filePath;
    @property (nonatomic, strong) NSArray<FileFragment*>          *fileFragments;
    @property (nonatomic, strong) NSFileHandle                    *readFileHandle;
    @property (nonatomic, strong) NSFileHandle                    *writeFileHandle;
    @property (nonatomic, assign) BOOL                            isReadOperation;
    @end
    
    @implementation FileStreamOperation
    
    + (instancetype)sharedOperation
    {
        static id instance = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            instance = [[[self class] alloc] init];
        });
        return instance;
    }
    
    + (NSString *)fileKey {
    
        CFUUIDRef uuid = CFUUIDCreate(kCFAllocatorDefault);
        CFStringRef cfstring = CFUUIDCreateString(kCFAllocatorDefault, uuid);
        const char *cStr = CFStringGetCStringPtr(cfstring,CFStringGetFastestEncoding(cfstring));
        unsigned char result[16];
        CC_MD5( cStr, (unsigned int)strlen(cStr), result );
        CFRelease(uuid);
    
        return [NSString stringWithFormat:
                @"%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%08lx",
                result[0], result[1], result[2], result[3],
                result[4], result[5], result[6], result[7],
                result[8], result[9], result[10], result[11],
                result[12], result[13], result[14], result[15],
                (unsigned long)(arc4random() % NSUIntegerMax)];
    }
    
    - (void)encodeWithCoder:(NSCoder *)aCoder {
    
        [aCoder encodeObject:[self fileName] forKey:@"fileName"];
        [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:[self fileSize]] forKey:@"fileSize"];
        [aCoder encodeObject:[self filePath] forKey:@"filePath"];
        [aCoder encodeObject:[self fileFragments] forKey:@"fileFragments"];
    }
    
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
        self = [super init];
        if (self != nil) {
            [self setFileName:[aDecoder decodeObjectForKey:@"fileName"]];
            [self setFileSize:[[aDecoder decodeObjectForKey:@"fileSize"] unsignedIntegerValue]];
            [self setFilePath:[aDecoder decodeObjectForKey:@"filePath"]];
            [self setFileFragments:[aDecoder decodeObjectForKey:@"fileFragments"]];
        }
    
        return self;
    }
    
    - (BOOL)getFileInfoAtPath:(NSString*)path {
    
        NSFileManager *fileMgr = [NSFileManager defaultManager];
        if (![fileMgr fileExistsAtPath:path]) {
            NSLog(@"文件不存在:%@",path);
            return NO;
        }
    
        self.filePath = path;
    
        NSDictionary *attr =[fileMgr attributesOfItemAtPath:path error:nil];
        self.fileSize = attr.fileSize;
    
        NSString *fileName = [path lastPathComponent];
        self.fileName = fileName;
    
        return YES;
    }
    
    // 若为读取文件数据,打开一个已存在的文件。
    // 若为写入文件数据,如果文件不存在,会创建的新的空文件。
    - (instancetype)initFileOperationAtPath:(NSString*)path forReadOperation:(BOOL)isReadOperation {
    
        if (self = [super init]) {
            self.isReadOperation = isReadOperation;
            if (_isReadOperation) {
                if (![self getFileInfoAtPath:path]) {
                    return nil;
                }
                self.readFileHandle = [NSFileHandle fileHandleForReadingAtPath:path];
                [self cutFileForFragments];
            } else {
                NSFileManager *fileMgr = [NSFileManager defaultManager];
                if (![fileMgr fileExistsAtPath:path]) {
                    [fileMgr createFileAtPath:path contents:nil attributes:nil];
                }
    
                if (![self getFileInfoAtPath:path]) {
                    return nil;
                }
    
                self.writeFileHandle = [NSFileHandle fileHandleForWritingAtPath:path];
            }
        }
    
        return self;
    }
    
    #pragma mark - 读操作
    // 切分文件片段
    - (void)cutFileForFragments {
    
        NSUInteger offset = FileFragmentMaxSize;
        // 块数
        NSUInteger chunks = (_fileSize%offset==0)?(_fileSize/offset):(_fileSize/(offset) + 1);
    
        NSMutableArray<FileFragment *> *fragments = [[NSMutableArray alloc] initWithCapacity:0];
        for (NSUInteger i = 0; i < chunks; i ++) {
    
            FileFragment *fFragment = [[FileFragment alloc] init];
            fFragment.fragmentStatus = FileUpStateWaiting;
            fFragment.fragmentId = [[self class] fileKey];
            fFragment.fragementOffset = i * offset;
    
            if (i != chunks - 1) {
                fFragment.fragmentSize = offset;
            } else {
                fFragment.fragmentSize = _fileSize - fFragment.fragementOffset;
            }
    
            [fragments addObject:fFragment];
        }
    
        self.fileFragments = fragments;
    }
    
    // 通过分片信息读取对应的片数据
    - (NSData*)readDateOfFragment:(FileFragment*)fragment {
    
        if (fragment) {
            [self seekToFileOffset:fragment.fragementOffset];
            return [_readFileHandle readDataOfLength:fragment.fragmentSize];
        }
    
        return nil;
    }
    
    - (NSData*)readDataOfLength:(NSUInteger)bytes {
        return [_readFileHandle readDataOfLength:bytes];
    }
    
    - (NSData*)readDataToEndOfFile {
        return [_readFileHandle readDataToEndOfFile];
    }
    
    #pragma mark - 写操作
    
    // 写入文件数据
    - (void)writeData:(NSData *)data {
        [_writeFileHandle writeData:data];
    }
    
    #pragma mark - common
    // 获取当前偏移量
    - (NSUInteger)offsetInFile{
        if (_isReadOperation) {
            return [_readFileHandle offsetInFile];
        }
    
        return [_writeFileHandle offsetInFile];
    }
    
    // 设置偏移量, 仅对读取设置
    - (void)seekToFileOffset:(NSUInteger)offset {
        [_readFileHandle seekToFileOffset:offset];
    }
    
    // 将偏移量定位到文件的末尾
    - (NSUInteger)seekToEndOfFile{
        if (_isReadOperation) {
            return [_readFileHandle seekToEndOfFile];
        }
    
        return [_writeFileHandle seekToEndOfFile];
    }
    
    // 关闭文件
    - (void)closeFile {
        if (_isReadOperation) {
            [_readFileHandle closeFile];
        } else {
            [_writeFileHandle closeFile];
        }
    }
    
    @end
    
    #pragma mark - FileFragment
    
    @implementation FileFragment
    
    - (void)encodeWithCoder:(NSCoder *)aCoder {
    
        [aCoder encodeObject:[self fragmentId] forKey:@"fragmentId"];
        [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:[self fragmentSize]] forKey:@"fragmentSize"];
        [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:[self fragementOffset]] forKey:@"fragementOffset"];
        [aCoder encodeObject:[NSNumber numberWithUnsignedInteger:[self fragmentStatus]] forKey:@"fragmentStatus"];
    }
    
    - (nullable instancetype)initWithCoder:(NSCoder *)aDecoder {
        self = [super init];
        if (self != nil) {
            [self setFragmentId:[aDecoder decodeObjectForKey:@"fragmentId"]];
            [self setFragmentSize:[[aDecoder decodeObjectForKey:@"fragmentSize"] unsignedIntegerValue]];
            [self setFragementOffset:[[aDecoder decodeObjectForKey:@"fragementOffset"] unsignedIntegerValue]];
            [self setFragmentStatus:[[aDecoder decodeObjectForKey:@"fragmentStatus"] integerValue]];
        }
    
        return self;
    }
    
    @end
    
    

    下面用到的才是最直接方便的,是我们所用到的上传工具类.
    JYUpdataTool.h文件

    #import <Foundation/Foundation.h>
    
    @interface JYUpdataTool : NSObject
    
    /**
     根据路径上传本地文件
    
     @param path 文件所在的本地路径
     */
    -(void)upDataWithPath:(NSString *)path;
    
    @end
    
    

    JYUpdataTool.m文件

    #import "JYUpdataTool.h"
    #import "FileStreamOperation.h"
    
    @interface JYUpdataTool()
    
    @property(strong,nonatomic) FileStreamOperation *fileStreamer;
    @property(assign,nonatomic) NSInteger currentIndex;
    @property(nonatomic,strong)NSThread *thread1;
    @property(nonatomic,strong)NSThread *thread2;
    @property(nonatomic,strong)NSThread *thread3;
    @property(strong,nonatomic) NSDate *date1;
    @end
    
    @implementation JYUpdataTool
    
    -(void)upDataWithPath:(NSString *)path{
    
        FileStreamOperation *fileStreamer = [[FileStreamOperation alloc] initFileOperationAtPath:path forReadOperation:YES];
        self.fileStreamer = fileStreamer;
        [self toUpData];
    }
    
    #pragma mark  懒加载
    -(NSThread *)thread1{
        if (!_thread1) {
            _thread1=[[NSThread alloc]initWithTarget:self selector:@selector(upOne) object:nil];
        }
        return _thread1;
    }
    -(NSThread *)thread2{
        if (!_thread2) {
            _thread2=[[NSThread alloc]initWithTarget:self selector:@selector(upOne) object:nil];
        }
        return _thread2;
    }
    -(NSThread *)thread3{
        if (!_thread3) {
            _thread3=[[NSThread alloc]initWithTarget:self selector:@selector(upOne) object:nil];
        }
        return _thread3;
    }
    
    #pragma mark  方法
    
    -(void)toUpData{
        self.date1 = [NSDate date];
        [self.thread1 start];
        [self.thread2 start];
        [self.thread3 start];
    
    }
    
    -(void)upOne{
        while (1) {
            //        线程安全,防止多次上传同一块区间
    //        @synchronized (self) {
                @autoreleasepool {
                    if (self.currentIndex < self.fileStreamer.fileFragments.count) {
                        if (self.fileStreamer.fileFragments[self.currentIndex].fragmentStatus == FileUpStateWaiting) {
                            self.fileStreamer.fileFragments[self.currentIndex].fragmentStatus = FileUpStateLoading;
                            NSData *data = [self.fileStreamer readDateOfFragment:self.fileStreamer.fileFragments[self.currentIndex]];
                            //                在这里执行上传的操作
                            [NSThread sleepForTimeInterval:0.2];
                            NSLog(@"这是第%zd个上传----%@",self.currentIndex,[NSThread currentThread]);
                            self.currentIndex++;
                        }
    
                    } else {
                        NSLog(@"时间间隔是%zd",(int)[[NSDate date] timeIntervalSinceDate:self.date1]);
                        [NSThread exit];
    
                    }
                }
    //        }
        }
    
    }
    
    @end
    
    

    就这样一个简单的大文件上传就搞定了,但是需要注意的是我们还要和后端那边协商下,因为我们穿的data是一个分段的,也就是切片的,所以需要后端那边进行合并下,因此我们是要在上传的时候在哪里设置标识让后端进行区分,也是可以和后台那边进行协商的.就是这么简单任性...
    demo地址:https://github.com/LUJYM/OC-Demo.git

    </article>

    作者链接:https://www.jianshu.com/p/567968830431

    相关文章

      网友评论

        本文标题:iOS 上传大文件(切片上传)

        本文链接:https://www.haomeiwen.com/subject/vziukltx.html