大文件上传
前言
简单的demo记录
本文思路
本文实现的思路:
- 大文件切片.
- 设置上传的最大并发.
- 根据
YYcache
缓存的数据进行断点续传. - 设置一个属性, 处理全部上传完成的通知操作.
实现
代码如下:
- 导入了
pod 'AFNetworking', '~> 4.0'
,YYModel
//
// ViewController.m
// 大文件上传
//
// Created by 吉冠坤 on 7/9/2021.
//
#import "ViewController.h"
#import <AFNetworking/AFNetworking.h>
#import "YYCache.h"
@interface ViewController ()
@property (nonatomic, assign) NSInteger pageCount; //切片数量
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
[self upload];
}
- (void)upload {
AFHTTPSessionManager *manager = [AFHTTPSessionManager manager];
//序列化
manager.requestSerializer = [AFJSONRequestSerializer serializer];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
//请求头设置
[manager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
NSString *url = @"url";
NSDictionary *parameter = @{};
NSString *filePath = @"文件路径";
unsigned long long pageSize = 1 << 20;//1兆, 分片大小
unsigned long long size = fileSizeAtPath(filePath); //文件大小
int count = ceil(size/pageSize * 1.0); //向上取整
//在缓存中那数据
NSMutableDictionary *uploadDic = @{}.mutableCopy;
YYCache *cache = [[YYCache alloc] initWithName:@"netUpload"];
if ([cache objectForKey:@"netData"]) {
//充缓存中拿数据
uploadDic = (NSMutableDictionary *)[cache objectForKey:@"netData"];
//设置切片数量
self.pageCount = uploadDic.allKeys.count;
} else {
@autoreleasepool {
for (int i = 0; i < count; i++) {
NSString *key = [NSString stringWithFormat:@"%d", i];
[uploadDic setObject:key forKey:key];
}
}
//设置切片数量
self.pageCount = count;
[cache setObject:uploadDic forKey:@"netData"];
}
dispatch_queue_t queue = dispatch_queue_create(0, DISPATCH_QUEUE_CONCURRENT);
dispatch_semaphore_t semp = dispatch_semaphore_create(5);
dispatch_async(queue, ^{
NSMutableArray *indexArr = uploadDic.allKeys.mutableCopy;
for (int i = 0; i < indexArr.count; i++) {
dispatch_semaphore_wait(semp, DISPATCH_TIME_FOREVER);
[manager POST:url parameters:parameter headers:nil constructingBodyWithBlock:^(id<AFMultipartFormData> _Nonnull formData) {
//获取文件偏移
int index = [indexArr[i] intValue];
//文件分片
NSFileHandle *handle = [NSFileHandle fileHandleForReadingAtPath:filePath];
//根据偏移地址取数据
//1. 设置偏移地址
[handle seekToFileOffset:(pageSize * index)];
//2. 设置读取的大小, 拿到切片数据
NSData *blockData = [handle readDataOfLength:pageSize];
//3.上传分片数据
[formData appendPartWithFileData:blockData name:@"block" fileName:filePath.lastPathComponent mimeType:@"video/mp4"];
} progress:^(NSProgress * _Nonnull uploadProgress) {
} success:^(NSURLSessionDataTask * _Nonnull task, id _Nullable responseObject) {
//上传成功
// 1. 处理数据
[uploadDic removeObjectForKey:indexArr[i]];
@synchronized (self) {
self.pageCount -= 1;
}
dispatch_async(dispatch_get_main_queue(), ^{
// 2. 主线程更新UI进度
});
dispatch_semaphore_signal(semp);
} failure:^(NSURLSessionDataTask * _Nullable task, NSError * _Nonnull error) {
//上传失败
// 设置自己的失败机制, 处理失败消息, 看是重新上传还是做其他用户提示操作.
// 根据缓存 netData 对应的数据, 可以判断传输问题.
dispatch_semaphore_signal(semp);
}];
}
});
}
- (void)setPageCount:(NSInteger)pageCount {
_pageCount = pageCount;
if (pageCount == 0) {
//处理全部加载完成的 操作
}
}
long long fileSizeAtPath(NSString* filePath){
NSFileManager* manager = [NSFileManager defaultManager];
if ([manager fileExistsAtPath:filePath]){
return [[manager attributesOfItemAtPath:filePath error:nil] fileSize];
}
return 0;
}
@end
总结
如果是小文件还可以转成NSData
在内存中操作, 如果是几个G的大文件, 那么只能通过偏移地址利用NSFileHandle
一点一点的进行文件切片处理.
其他思路
还有一种思路:
-
需要上传的数据进行在
Document
新建一个以文件哈希值为名字的文件夹. -
然后利用
NSFileHandle
对文件进行切片, 在文件夹内对每一个切片新建一个文件, 然后数据存入文件, 命名可以以哈希值命名. 但是在上传的时候需要告诉服务端, 对应的拼接顺序. -
在上传的时候, 只需要读文件夹, 拿到数组.
- 上传文件, 成功之后删除
-
最后根据文件夹是否为空, 就可以判断是否传输完毕.
这种方式相对来说, 程序中断或者其他的下次启动直接找对应的文件夹, 不需要做其他的处理. 但是解析文件成对应的文件夹然后分片也很耗费I/O. 不太建议, 只是一种思路探讨.
总结
需要注意的问题:
- 大文件不可以直接加载进内存, 会内存溢出.
- 通过
NSFileHandle
类处理
- 通过
- 异步上传的时候要告诉服务端两件事:
- 文件片的拼接方式
- 文件片传输完成之后的通知.
- 子线程上传, 主线程刷新UI.
- 控制并发量.
转载自:https://juejin.cn/post/7005027760104013855