大文件上传类设计(OC实现)

下面我将设计一个支持断点续传、多线程上传的大文件上传类,采用Objective-C实现,考虑线程安全、数据库持久化和高效上传。

设计概览

类文件划分

  1. FileUploadManager.h/m - 上传任务管理中心
  2. FileUploadTask.h/m - 单个上传任务控制
  3. ChunkUploadOperation.h/m - 分块上传操作
  4. UploadDatabaseManager.h/m - 数据库操作
  5. FileChunker.h/m - 文件分块工具
  6. UploadModels.h - 数据模型

核心类实现

1. 数据模型 (UploadModels.h)
#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
2. 文件分块工具 (FileChunker.h/m)
// 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
3. 数据库管理 (UploadDatabaseManager.h/m)
// 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
4. 分块上传操作 (ChunkUploadOperation.h/m)
// 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
5. 上传任务控制 (FileUploadTask.h/m)
// 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
6. 上传任务管理中心 (FileUploadManager.h/m)
// 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

关键设计要点

1. 线程安全设计

  • 使用 **NSLock** 保护关键数据结构和状态
  • 每个上传操作有自己的锁,用于保护上传状态
  • 数据库操作使用串行队列保证线程安全
  • 使用 **NSOperationQueue** 管理并发上传任务

2. 断点续传实现

  • 文件分块存储(默认5MB/块)
  • 每个分块独立上传,记录上传进度
  • 使用SQLite数据库持久化上传状态
  • 文件哈希校验确保文件未修改

3. 分块上传流程

  1. 文件分块计算
  2. 设置Content-Range头
  3. 使用NSURLSessionUploadTask上传
  4. 实时更新分块上传进度
  5. 所有分块完成后通知服务器合并

4. 错误处理

  • 数据库记录上传错误
  • 网络错误自动重试(系统级)
  • 分块上传失败可独立重试
  • 文件访问错误处理

5. 内存优化

  • 使用FileHandle而不是加载整个文件到内存
  • 合理的分块大小(5MB)
  • 使用流式上传减少内存占用

6. 性能优化

  • 多分块并行上传(默认4线程)
  • 后台线程处理数据库操作
  • 批量更新数据库减少IO
  • 使用高效的文件访问方式

使用示例

// 开始上传
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"];

扩展建议

  1. 进度回调:添加进度回调接口
  2. 速度限制:实现上传速度限制
  3. 后台上传:支持后台任务继续上传
  4. 断网重试:实现网络恢复自动重试
  5. 加密上传:支持文件加密上传
  6. 分块验证:实现分块上传后校验

这个设计提供了可靠的大文件上传解决方案,支持断点续传、多线程上传和错误恢复,适用于iOS/macOS应用中的大文件上传场景。

你可能感兴趣的:(iOS开发,ios,oracle,objective-c)