首页
/ 攻克Flutter大文件上传难题:基于dio的分片上传与断点续传实战

攻克Flutter大文件上传难题:基于dio的分片上传与断点续传实战

2026-03-08 03:50:10作者:郁楠烈Hubert

在移动应用开发中,大文件上传一直是技术难点,尤其是在弱网络环境下经常出现上传中断、进度丢失、内存溢出等问题。本文将基于dio网络库,通过分片上传、断点续传和进度追踪三大核心技术,构建一个健壮的大文件上传系统。我们将从原理分析到代码实现,全面解决大文件上传的痛点问题,让你掌握企业级文件上传方案的设计与实现。

一、问题导入:大文件上传的技术挑战

1.1 常见问题剖析

大文件上传面临着诸多技术挑战,主要包括以下几个方面:

  • 内存溢出:一次性读取大文件到内存会导致OOM(Out Of Memory)错误
  • 网络不稳定:网络波动或中断会导致上传失败,需要重新开始
  • 上传进度不直观:用户无法了解当前上传状态,体验差
  • 服务器压力:单个大文件请求会占用大量服务器资源

这些问题在实际开发中非常常见,特别是在需要上传视频、高清图片等场景下更为突出。

1.2 解决方案概览

针对上述问题,我们将采用以下技术方案:

  • 分片上传:将大文件分割成小片段分别上传
  • 断点续传:记录已上传片段,支持从中断处继续上传
  • 进度追踪:实时监控上传进度,提供用户反馈
  • 并发控制:控制同时上传的分片数量,避免网络拥塞

二、核心原理:分片上传与断点续传机制

2.1 分片上传原理

分片上传(Chunked Upload)是将大文件分割成固定大小的小块(Chunk),然后逐个或并行上传这些小块,最后在服务器端合并所有小块形成完整文件的过程。

sequenceDiagram
    participant 客户端
    participant 服务器
    
    客户端->>客户端: 1. 文件分片处理
    客户端->>服务器: 2. 请求上传凭证
    服务器->>客户端: 3. 返回分片上传参数
    loop 上传所有分片
        客户端->>服务器: 4. 上传单个分片
        服务器->>客户端: 5. 返回分片上传结果
    end
    客户端->>服务器: 6. 请求合并分片
    服务器->>客户端: 7. 返回合并结果

2.2 断点续传实现

断点续传依赖于两个关键机制:

  1. 分片唯一标识:为每个分片生成唯一标识,通常基于文件内容的哈希值
  2. 上传状态记录:客户端或服务器记录已成功上传的分片信息

当上传中断后,客户端可以查询已上传的分片,仅上传未完成的部分,从而实现断点续传。

三、分步实现:基于dio的大文件上传系统

3.1 项目依赖配置

首先,在pubspec.yaml中添加必要依赖:

dependencies:
  dio: ^5.4.0
  dio_cookie_manager: ^2.1.0
  crypto: ^3.0.3  # 用于生成文件哈希
  path_provider: ^2.1.2  # 获取本地文件路径

执行以下命令安装依赖:

flutter pub get

3.2 文件分片处理

创建文件分片工具类,负责将大文件分割成小片段:

import 'dart:io';
import 'dart:convert';
import 'package:crypto/crypto.dart';

class FileChunkUtil {
  // 分片大小,通常为2MB-10MB,根据网络状况调整
  static const int chunkSize = 4 * 1024 * 1024; // 4MB
  
  /// 将文件分割成多个分片
  static Future<List<File>> splitFile(File file) async {
    List<File> chunks = [];
    int fileSize = await file.length();
    int chunkCount = (fileSize / chunkSize).ceil();
    
    for (int i = 0; i < chunkCount; i++) {
      int start = i * chunkSize;
      int end = (i + 1) * chunkSize;
      if (end > fileSize) end = fileSize;
      
      // 创建临时文件存储分片
      Directory tempDir = await getTemporaryDirectory();
      File chunkFile = File('${tempDir.path}/chunk_${i}_${file.path.split('/').last}');
      
      // 读取文件片段并写入临时文件
      RandomAccessFile raf = await file.open(mode: FileMode.read);
      await raf.setPosition(start);
      List<int> bytes = await raf.read(end - start);
      await chunkFile.writeAsBytes(bytes);
      await raf.close();
      
      chunks.add(chunkFile);
    }
    
    return chunks;
  }
  
  /// 生成文件唯一标识(基于文件内容的MD5哈希)
  static Future<String> generateFileId(File file) async {
    List<int> fileBytes = await file.readAsBytes();
    String md5Hash = md5.convert(fileBytes).toString();
    return md5Hash;
  }
}

为什么这么做

  • 固定大小的分片便于服务器处理和断点续传
  • 使用MD5哈希作为文件唯一标识,可以避免重复上传相同文件
  • 临时文件存储分片可以避免占用过多内存

3.3 上传管理器实现

创建上传管理器,协调分片上传、断点续传和进度追踪:

import 'dart:io';
import 'package:dio/dio.dart';
import 'package:path_provider/path_provider.dart';

class ChunkedUploader {
  final Dio _dio;
  final String _uploadUrl;
  final Function(double progress) _onProgress;
  
  ChunkedUploader(this._dio, this._uploadUrl, this._onProgress);
  
  /// 上传文件
  Future<String> uploadFile(File file) async {
    try {
      // 1. 生成文件唯一标识
      String fileId = await FileChunkUtil.generateFileId(file);
      
      // 2. 分割文件为分片
      List<File> chunks = await FileChunkUtil.splitFile(file);
      int totalChunks = chunks.length;
      
      // 3. 检查已上传分片
      List<int> uploadedChunks = await _checkUploadedChunks(fileId, totalChunks);
      
      // 4. 上传未完成的分片
      int uploadedCount = uploadedChunks.length;
      
      for (int i = 0; i < totalChunks; i++) {
        if (uploadedChunks.contains(i)) continue;
        
        await _uploadChunk(
          fileId: fileId,
          chunkFile: chunks[i],
          chunkIndex: i,
          totalChunks: totalChunks,
          fileName: file.path.split('/').last,
        );
        
        uploadedCount++;
        double progress = uploadedCount / totalChunks;
        _onProgress(progress);
      }
      
      // 5. 通知服务器合并分片
      return await _mergeChunks(fileId, file.path.split('/').last);
      
    } catch (e) {
      print('上传失败: $e');
      rethrow;
    }
  }
  
  /// 检查已上传的分片
  Future<List<int>> _checkUploadedChunks(String fileId, int totalChunks) async {
    try {
      Response response = await _dio.get(
        '$_uploadUrl/check',
        queryParameters: {'fileId': fileId, 'totalChunks': totalChunks},
      );
      
      if (response.statusCode == 200) {
        return List<int>.from(response.data['uploadedChunks'] ?? []);
      }
    } catch (e) {
      print('检查已上传分片失败: $e');
    }
    return []; // 发生错误时,默认重新上传所有分片
  }
  
  /// 上传单个分片
  Future<void> _uploadChunk({
    required String fileId,
    required File chunkFile,
    required int chunkIndex,
    required int totalChunks,
    required String fileName,
  }) async {
    FormData formData = FormData.fromMap({
      'fileId': fileId,
      'chunkIndex': chunkIndex,
      'totalChunks': totalChunks,
      'fileName': fileName,
      'chunk': await MultipartFile.fromFile(
        chunkFile.path,
        filename: 'chunk_$chunkIndex',
      ),
    });
    
    await _dio.post(
      '$_uploadUrl/chunk',
      data: formData,
      options: Options(
        contentType: 'multipart/form-data',
      ),
    );
  }
  
  /// 通知服务器合并分片
  Future<String> _mergeChunks(String fileId, String fileName) async {
    Response response = await _dio.post(
      '$_uploadUrl/merge',
      data: {
        'fileId': fileId,
        'fileName': fileName,
      },
    );
    
    if (response.statusCode == 200) {
      return response.data['fileUrl'];
    } else {
      throw Exception('合并分片失败: ${response.statusCode}');
    }
  }
}

为什么这么做

  • 模块化设计使代码更易维护和扩展
  • 上传前检查已上传分片,实现断点续传功能
  • 实时计算并回调上传进度,提升用户体验
  • 分离的分片上传和合并步骤,符合RESTful API设计原则

3.4 初始化与使用

在应用中初始化dio和上传管理器:

import 'package:dio/dio.dart';
import 'package:dio_cookie_manager/cookie_manager.dart';
import 'package:cookie_jar/cookie_jar.dart';

// 初始化dio
final dio = Dio()
  ..interceptors.add(CookieManager(CookieJar()))
  ..interceptors.add(LogInterceptor(responseBody: true));

// 创建上传管理器
final uploader = ChunkedUploader(
  dio,
  'https://api.example.com/upload',
  (progress) {
    print('上传进度: ${(progress * 100).toStringAsFixed(1)}%');
    // 更新UI显示进度
  },
);

// 使用上传管理器
void uploadLargeFile(File file) async {
  try {
    String fileUrl = await uploader.uploadFile(file);
    print('文件上传成功: $fileUrl');
  } catch (e) {
    print('文件上传失败: $e');
  }
}

四、场景扩展:高级功能实现

4.1 并发上传控制

为提高上传效率,可以实现分片并发上传,但需要控制并发数量避免网络拥塞:

// 修改上传管理器,添加并发控制
Future<void> uploadChunksConcurrently(
  List<File> chunks, 
  String fileId, 
  String fileName,
  int totalChunks,
  List<int> uploadedChunks,
) async {
  const int maxConcurrent = 3; // 最大并发数
  int uploadedCount = uploadedChunks.length;
  List<int> toUpload = List.generate(totalChunks, (i) => i)
    .where((i) => !uploadedChunks.contains(i))
    .toList();
  
  // 创建信号量控制并发
  final semaphore = Semaphore(maxConcurrent);
  
  await Future.wait(toUpload.map((i) async {
    await semaphore.acquire();
    try {
      await _uploadChunk(
        fileId: fileId,
        chunkFile: chunks[i],
        chunkIndex: i,
        totalChunks: totalChunks,
        fileName: fileName,
      );
      uploadedCount++;
      double progress = uploadedCount / totalChunks;
      _onProgress(progress);
    } finally {
      semaphore.release();
    }
  }));
}

4.2 上传暂停与恢复

实现上传任务的暂停与恢复功能:

class UploadTask {
  CancelToken _cancelToken = CancelToken();
  bool _isPaused = false;
  
  Future<String> startUpload(File file, ChunkedUploader uploader) async {
    try {
      return await uploader.uploadFile(
        file,
        cancelToken: _cancelToken,
      );
    } on DioException catch (e) {
      if (e.type == DioExceptionType.cancel) {
        if (_isPaused) {
          throw UploadPausedException();
        }
      }
      rethrow;
    }
  }
  
  void pause() {
    _isPaused = true;
    _cancelToken.cancel();
    _cancelToken = CancelToken(); // 为恢复上传准备新的cancelToken
  }
  
  // 恢复上传的实现...
}

4.3 常见问题解决方案

错误类型 可能原因 解决方案
分片合并失败 部分分片上传失败或损坏 验证每个分片的MD5哈希,重新上传损坏的分片
上传速度慢 并发数设置过高或分片过大 调整并发数和分片大小,通常4-8MB分片大小较为合适
内存占用高 同时加载多个分片到内存 实现分片文件的流式读取,避免一次性加载整个分片
上传中断后无法续传 未正确保存上传状态 使用本地数据库记录上传进度,如Hive或SQLite

五、总结与扩展

5.1 核心知识点总结

  1. 分片上传机制:将大文件分割成固定大小的分片,逐个上传后合并,有效解决大文件上传的内存和网络问题
  2. 断点续传实现:通过文件唯一标识和已上传分片记录,实现从中断处继续上传,大幅提升用户体验
  3. 进度追踪与反馈:实时计算上传进度并提供用户反馈,是提升大文件上传体验的关键

5.2 进阶学习方向

  1. 智能分片策略:根据文件类型、网络状况动态调整分片大小和并发数,优化上传效率
  2. 断点续传优化:实现基于服务器的断点续传,支持多设备间的上传状态同步

5.3 互动引导

你在实际项目中遇到过哪些大文件上传的挑战?有什么优化方案或创新思路?欢迎在评论区分享你的经验和想法!如果需要本文完整代码示例,可以访问项目中的示例代码目录获取详细实现。

大文件上传流程示意图

登录后查看全文
热门项目推荐
相关项目推荐