下面我将设计一个支持断点续传、多线程上传的大文件上传类,采用Objective-C实现,考虑线程安全、数据库持久化和高效上传。
#import <Foundation/Foundation.h>
typedef NS_ENUM(NSInteger, UploadStatus) {
UploadStatusPending,
UploadStatusUploading,
UploadStatusPaused,
UploadStatusCompleted,
UploadStatusFailed
};
@interface UploadTask : NSObject
@property (nonatomic, copy) NSString *taskID;
@property (nonatomic, copy) NSString *filePath;
@property (nonatomic, copy) NSString *serverURL;
@property (nonatomic, copy) NSString *fileHash;
@property (nonatomic, assign) long long fileSize;
@property (nonatomic, assign) UploadStatus status;
@property (nonatomic, strong) NSDate *createdAt;
@property (nonatomic, strong) NSDate *updatedAt;
@end
@interface UploadChunk : NSObject
@property (nonatomic, copy) NSString *chunkID;
@property (nonatomic, copy) NSString *taskID;
@property (nonatomic, assign) NSInteger chunkIndex;
@property (nonatomic, assign) long long chunkOffset;
@property (nonatomic, assign) long long chunkSize;
@property (nonatomic, assign) long long uploadedSize;
@end
// FileChunker.h
#import <Foundation/Foundation.h>
#import "UploadModels.h"
@interface FileChunker : NSObject
+ (NSArray<UploadChunk *> *)splitFile:(NSString *)filePath
chunkSize:(NSUInteger)chunkSize
taskID:(NSString *)taskID;
@end
// FileChunker.m
#import "FileChunker.h"
@implementation FileChunker
+ (NSArray<UploadChunk *> *)splitFile:(NSString *)filePath
chunkSize:(NSUInteger)chunkSize
taskID:(NSString *)taskID {
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary *attrs = [fileManager attributesOfItemAtPath:filePath error:nil];
long long fileSize = [attrs fileSize];
NSMutableArray<UploadChunk *> *chunks = [NSMutableArray array];
long long offset = 0;
NSInteger index = 0;
while (offset < fileSize) {
UploadChunk *chunk = [[UploadChunk alloc] init];
chunk.chunkID = [NSUUID UUID].UUIDString;
chunk.taskID = taskID;
chunk.chunkIndex = index++;
chunk.chunkOffset = offset;
chunk.chunkSize = MIN(chunkSize, fileSize - offset);
chunk.uploadedSize = 0;
[chunks addObject:chunk];
offset += chunkSize;
}
return chunks;
}
@end
// UploadDatabaseManager.h
#import <Foundation/Foundation.h>
#import "UploadModels.h"
@interface UploadDatabaseManager : NSObject
+ (instancetype)sharedManager;
- (void)saveTask:(UploadTask *)task;
- (void)saveChunk:(UploadChunk *)chunk;
- (UploadTask *)taskForFilePath:(NSString *)filePath;
- (NSArray<UploadChunk *> *)chunksForTaskID:(NSString *)taskID;
- (void)updateChunk:(UploadChunk *)chunk;
- (void)completeTask:(NSString *)taskID;
- (void)saveErrorForTask:(NSString *)taskID error:(NSError *)error;
@end
// UploadDatabaseManager.m
#import "UploadDatabaseManager.h"
#import <sqlite3.h>
@interface UploadDatabaseManager ()
@property (nonatomic, assign) sqlite3 *database;
@property (nonatomic, strong) dispatch_queue_t databaseQueue;
@end
@implementation UploadDatabaseManager
+ (instancetype)sharedManager {
static UploadDatabaseManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[UploadDatabaseManager alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_databaseQueue = dispatch_queue_create("com.upload.database", DISPATCH_QUEUE_SERIAL);
[self initializeDatabase];
}
return self;
}
- (void)initializeDatabase {
dispatch_sync(_databaseQueue, ^{
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths firstObject];
NSString *dbPath = [documentsDirectory stringByAppendingPathComponent:@"uploads.db"];
if (sqlite3_open([dbPath UTF8String], &_database) != SQLITE_OK) {
NSLog(@"Failed to open database");
return;
}
char *errMsg;
const char *sql = "CREATE TABLE IF NOT EXISTS UploadTasks ("
"taskID TEXT PRIMARY KEY, "
"filePath TEXT NOT NULL, "
"serverURL TEXT NOT NULL, "
"fileHash TEXT NOT NULL, "
"fileSize INTEGER, "
"status INTEGER, "
"createdAt REAL, "
"updatedAt REAL);"
"CREATE TABLE IF NOT EXISTS UploadChunks ("
"chunkID TEXT PRIMARY KEY, "
"taskID TEXT NOT NULL, "
"chunkIndex INTEGER, "
"chunkOffset INTEGER, "
"chunkSize INTEGER, "
"uploadedSize INTEGER);"
"CREATE TABLE IF NOT EXISTS UploadErrors ("
"errorID INTEGER PRIMARY KEY AUTOINCREMENT, "
"taskID TEXT, "
"errorMessage TEXT, "
"createdAt REAL);";
if (sqlite3_exec(_database, sql, NULL, NULL, &errMsg) != SQLITE_OK) {
NSLog(@"Failed to create tables: %s", errMsg);
sqlite3_free(errMsg);
}
});
}
- (void)saveTask:(UploadTask *)task {
dispatch_sync(_databaseQueue, ^{
const char *sql = "INSERT OR REPLACE INTO UploadTasks "
"(taskID, filePath, serverURL, fileHash, fileSize, status, createdAt, updatedAt) "
"VALUES (?, ?, ?, ?, ?, ?, ?, ?);";
sqlite3_stmt *stmt;
if (sqlite3_prepare_v2(_database, sql, -1, &stmt, NULL) == SQLITE_OK) {
sqlite3_bind_text(stmt, 1, [task.taskID UTF8String], -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 2, [task.filePath UTF8String], -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 3, [task.serverURL UTF8String], -1, SQLITE_TRANSIENT);
sqlite3_bind_text(stmt, 4, [task.fileHash UTF8String], -1, SQLITE_TRANSIENT);
sqlite3_bind_int64(stmt, 5, task.fileSize);
sqlite3_bind_int(stmt, 6, (int)task.status);
sqlite3_bind_double(stmt, 7, [task.createdAt timeIntervalSince1970]);
sqlite3_bind_double(stmt, 8, [task.updatedAt timeIntervalSince1970]);
if (sqlite3_step(stmt) != SQLITE_DONE) {
NSLog(@"Error saving task: %s", sqlite3_errmsg(_database));
}
sqlite3_finalize(stmt);
}
});
}
// 其他数据库方法实现类似...
@end
// ChunkUploadOperation.h
#import <Foundation/Foundation.h>
#import "UploadModels.h"
@interface ChunkUploadOperation : NSOperation
@property (nonatomic, strong) UploadChunk *chunk;
@property (nonatomic, copy) NSString *filePath;
@property (nonatomic, strong) NSURL *serverURL;
@property (nonatomic, assign) long long uploadedBytes;
@property (nonatomic, assign) BOOL isPaused;
@property (nonatomic, strong) NSLock *lock;
- (instancetype)initWithChunk:(UploadChunk *)chunk
filePath:(NSString *)filePath
serverURL:(NSURL *)serverURL;
- (void)pauseUpload;
- (void)resumeUpload;
@end
// ChunkUploadOperation.m
#import "ChunkUploadOperation.h"
#import "UploadDatabaseManager.h"
@interface ChunkUploadOperation () <NSURLSessionDataDelegate>
@property (nonatomic, strong) NSURLSessionUploadTask *uploadTask;
@property (nonatomic, strong) NSFileHandle *fileHandle;
@end
@implementation ChunkUploadOperation
- (instancetype)initWithChunk:(UploadChunk *)chunk
filePath:(NSString *)filePath
serverURL:(NSURL *)serverURL {
self = [super init];
if (self) {
_chunk = chunk;
_filePath = filePath;
_serverURL = serverURL;
_uploadedBytes = chunk.uploadedSize;
_lock = [[NSLock alloc] init];
}
return self;
}
- (void)main {
@autoreleasepool {
if (self.isCancelled) return;
// 打开文件句柄
self.fileHandle = [NSFileHandle fileHandleForReadingAtPath:self.filePath];
if (!self.fileHandle) {
NSLog(@"Failed to open file: %@", self.filePath);
return;
}
// 定位到分块起始位置
[self.fileHandle seekToFileOffset:self.chunk.chunkOffset + self.uploadedBytes];
// 计算剩余需要上传的大小
long long remainingSize = self.chunk.chunkSize - self.uploadedBytes;
// 创建请求
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:self.serverURL];
request.HTTPMethod = @"POST";
// 设置Content-Range头
NSString *contentRange = [NSString stringWithFormat:@"bytes %lld-%lld/%lld",
self.chunk.chunkOffset + self.uploadedBytes,
self.chunk.chunkOffset + self.chunk.chunkSize - 1,
self.chunk.chunkSize];
[request setValue:contentRange forHTTPHeaderField:@"Content-Range"];
// 创建上传任务
__weak typeof(self) weakSelf = self;
self.uploadTask = [[NSURLSession sharedSession] uploadTaskWithRequest:request
fromFile:[NSURL fileURLWithPath:self.filePath]
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
__strong typeof(weakSelf) strongSelf = weakSelf;
[strongSelf.lock lock];
if (error) {
if (error.code != NSURLErrorCancelled) {
NSLog(@"Chunk %ld upload failed: %@", (long)strongSelf.chunk.chunkIndex, error);
[[UploadDatabaseManager sharedManager] saveErrorForTask:strongSelf.chunk.taskID error:error];
}
} else {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if (httpResponse.statusCode >= 200 && httpResponse.statusCode < 300) {
// 上传成功
strongSelf.chunk.uploadedSize = strongSelf.chunk.chunkSize;
[[UploadDatabaseManager sharedManager] updateChunk:strongSelf.chunk];
} else {
NSLog(@"Server responded with status: %ld", (long)httpResponse.statusCode);
}
}
[strongSelf.fileHandle closeFile];
[strongSelf.lock unlock];
}];
[self.uploadTask resume];
// 等待操作完成或暂停
while (!self.isFinished && !self.isCancelled) {
if (self.isPaused) {
[self.uploadTask suspend];
while (self.isPaused && !self.isCancelled) {
[NSThread sleepForTimeInterval:0.1];
}
if (!self.isCancelled) {
[self.uploadTask resume];
}
}
[NSThread sleepForTimeInterval:0.1];
}
// 取消任务
if (self.isCancelled) {
[self.uploadTask cancel];
}
}
}
- (void)pauseUpload {
[self.lock lock];
self.isPaused = YES;
[self.lock unlock];
}
- (void)resumeUpload {
[self.lock lock];
self.isPaused = NO;
[self.lock unlock];
}
- (void)cancel {
[super cancel];
[self.uploadTask cancel];
}
@end
// FileUploadTask.h
#import <Foundation/Foundation.h>
#import "UploadModels.h"
@class UploadDatabaseManager;
@interface FileUploadTask : NSObject
@property (nonatomic, strong) UploadTask *task;
@property (nonatomic, strong) NSArray<UploadChunk *> *chunks;
@property (nonatomic, strong) NSMutableArray<ChunkUploadOperation *> *operations;
@property (nonatomic, strong) NSOperationQueue *uploadQueue;
@property (nonatomic, assign) BOOL isPaused;
@property (nonatomic, strong) NSLock *lock;
- (instancetype)initWithFilePath:(NSString *)filePath
serverURL:(NSURL *)serverURL
chunkSize:(NSUInteger)chunkSize;
- (void)startUpload;
- (void)pauseUpload;
- (void)resumeUpload;
- (void)cancelUpload;
@end
// FileUploadTask.m
#import "FileUploadTask.h"
#import "FileChunker.h"
#import "UploadDatabaseManager.h"
#import "ChunkUploadOperation.h"
@implementation FileUploadTask
- (instancetype)initWithFilePath:(NSString *)filePath
serverURL:(NSURL *)serverURL
chunkSize:(NSUInteger)chunkSize {
self = [super init];
if (self) {
_lock = [[NSLock alloc] init];
_uploadQueue = [[NSOperationQueue alloc] init];
_uploadQueue.maxConcurrentOperationCount = 4; // 同时上传的分块数
// 创建任务
_task = [[UploadTask alloc] init];
_task.taskID = [NSUUID UUID].UUIDString;
_task.filePath = filePath;
_task.serverURL = [serverURL absoluteString];
_task.status = UploadStatusPending;
_task.createdAt = [NSDate date];
// 获取文件信息
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDictionary *attrs = [fileManager attributesOfItemAtPath:filePath error:nil];
_task.fileSize = [attrs fileSize];
// 这里需要实现文件哈希计算(如MD5)
_task.fileHash = @"file_hash_placeholder";
// 检查数据库是否有已存在的任务
UploadTask *existingTask = [[UploadDatabaseManager sharedManager] taskForFilePath:filePath];
if (existingTask && [existingTask.fileHash isEqualToString:_task.fileHash]) {
_task = existingTask;
_chunks = [[UploadDatabaseManager sharedManager] chunksForTaskID:_task.taskID];
} else {
// 创建新的分块
_chunks = [FileChunker splitFile:filePath chunkSize:chunkSize taskID:_task.taskID];
[[UploadDatabaseManager sharedManager] saveTask:_task];
for (UploadChunk *chunk in _chunks) {
[[UploadDatabaseManager sharedManager] saveChunk:chunk];
}
}
// 创建上传操作
_operations = [NSMutableArray array];
for (UploadChunk *chunk in _chunks) {
if (chunk.uploadedSize < chunk.chunkSize) {
ChunkUploadOperation *operation = [[ChunkUploadOperation alloc]
initWithChunk:chunk
filePath:filePath
serverURL:serverURL];
[_operations addObject:operation];
}
}
}
return self;
}
- (void)startUpload {
[self.lock lock];
if (self.task.status == UploadStatusUploading) {
[self.lock unlock];
return;
}
self.task.status = UploadStatusUploading;
self.task.updatedAt = [NSDate date];
[[UploadDatabaseManager sharedManager] saveTask:self.task];
// 添加完成块操作
NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
[self checkCompletion];
}];
// 添加操作到队列
for (ChunkUploadOperation *operation in self.operations) {
if (!operation.isFinished && !operation.isExecuting) {
[completionOperation addDependency:operation];
[self.uploadQueue addOperation:operation];
}
}
[self.uploadQueue addOperation:completionOperation];
[self.lock unlock];
}
- (void)pauseUpload {
[self.lock lock];
self.isPaused = YES;
self.task.status = UploadStatusPaused;
self.task.updatedAt = [NSDate date];
[[UploadDatabaseManager sharedManager] saveTask:self.task];
for (ChunkUploadOperation *operation in self.operations) {
[operation pauseUpload];
}
[self.lock unlock];
}
- (void)resumeUpload {
[self.lock lock];
self.isPaused = NO;
self.task.status = UploadStatusUploading;
self.task.updatedAt = [NSDate date];
[[UploadDatabaseManager sharedManager] saveTask:self.task];
for (ChunkUploadOperation *operation in self.operations) {
[operation resumeUpload];
}
[self startUpload]; // 重启上传
[self.lock unlock];
}
- (void)cancelUpload {
[self.lock lock];
[self.uploadQueue cancelAllOperations];
for (ChunkUploadOperation *operation in self.operations) {
[operation cancel];
}
self.task.status = UploadStatusFailed;
self.task.updatedAt = [NSDate date];
[[UploadDatabaseManager sharedManager] saveTask:self.task];
[self.lock unlock];
}
- (void)checkCompletion {
[self.lock lock];
BOOL allCompleted = YES;
for (UploadChunk *chunk in self.chunks) {
if (chunk.uploadedSize < chunk.chunkSize) {
allCompleted = NO;
break;
}
}
if (allCompleted) {
self.task.status = UploadStatusCompleted;
self.task.updatedAt = [NSDate date];
[[UploadDatabaseManager sharedManager] completeTask:self.task.taskID];
// 通知服务器合并文件
[self mergeChunksOnServer];
}
[self.lock unlock];
}
- (void)mergeChunksOnServer {
// 实现调用服务器合并接口
NSURL *mergeURL = [NSURL URLWithString:[self.task.serverURL stringByAppendingString:@"/merge"]];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:mergeURL];
request.HTTPMethod = @"POST";
NSDictionary *body = @{@"filePath": self.task.filePath,
@"fileHash": self.task.fileHash,
@"chunkCount": @(self.chunks.count)};
request.HTTPBody = [NSJSONSerialization dataWithJSONObject:body options:0 error:nil];
[[[NSURLSession sharedSession] dataTaskWithRequest:request
completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (error) {
NSLog(@"Merge failed: %@", error);
} else {
NSLog(@"File merged successfully");
}
}] resume];
}
@end
// FileUploadManager.h
#import <Foundation/Foundation.h>
@interface FileUploadManager : NSObject
+ (instancetype)sharedManager;
- (void)startUploadForFile:(NSString *)filePath
toServer:(NSURL *)serverURL
chunkSize:(NSUInteger)chunkSize;
- (void)pauseUploadForFile:(NSString *)filePath;
- (void)resumeUploadForFile:(NSString *)filePath;
- (void)cancelUploadForFile:(NSString *)filePath;
@end
// FileUploadManager.m
#import "FileUploadManager.h"
#import "FileUploadTask.h"
@interface FileUploadManager ()
@property (nonatomic, strong) NSMutableDictionary<NSString *, FileUploadTask *> *activeTasks;
@property (nonatomic, strong) NSLock *lock;
@end
@implementation FileUploadManager
+ (instancetype)sharedManager {
static FileUploadManager *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[FileUploadManager alloc] init];
});
return instance;
}
- (instancetype)init {
self = [super init];
if (self) {
_activeTasks = [NSMutableDictionary dictionary];
_lock = [[NSLock alloc] init];
}
return self;
}
- (void)startUploadForFile:(NSString *)filePath
toServer:(NSURL *)serverURL
chunkSize:(NSUInteger)chunkSize {
[self.lock lock];
FileUploadTask *task = self.activeTasks[filePath];
if (!task) {
task = [[FileUploadTask alloc] initWithFilePath:filePath
serverURL:serverURL
chunkSize:chunkSize];
self.activeTasks[filePath] = task;
}
[task startUpload];
[self.lock unlock];
}
- (void)pauseUploadForFile:(NSString *)filePath {
[self.lock lock];
FileUploadTask *task = self.activeTasks[filePath];
[task pauseUpload];
[self.lock unlock];
}
- (void)resumeUploadForFile:(NSString *)filePath {
[self.lock lock];
FileUploadTask *task = self.activeTasks[filePath];
[task resumeUpload];
[self.lock unlock];
}
- (void)cancelUploadForFile:(NSString *)filePath {
[self.lock lock];
FileUploadTask *task = self.activeTasks[filePath];
[task cancelUpload];
[self.activeTasks removeObjectForKey:filePath];
[self.lock unlock];
}
@end
**NSLock**
保护关键数据结构和状态**NSOperationQueue**
管理并发上传任务// 开始上传
FileUploadManager *manager = [FileUploadManager sharedManager];
[manager startUploadForFile:@"/path/to/largefile.zip"
toServer:[NSURL URLWithString:@"https://api.example.com/upload"]
chunkSize:5 * 1024 * 1024]; // 5MB分块
// 暂停上传
[manager pauseUploadForFile:@"/path/to/largefile.zip"];
// 恢复上传
[manager resumeUploadForFile:@"/path/to/largefile.zip"];
// 取消上传
[manager cancelUploadForFile:@"/path/to/largefile.zip"];
这个设计提供了可靠的大文件上传解决方案,支持断点续传、多线程上传和错误恢复,适用于iOS/macOS应用中的大文件上传场景。