flutter dio分片上传

3,557 阅读3分钟

当要上传的文件过大,又需要支持断点续传的话,则需要将一个大文件分成一片一片的,分多个请求依次或者同时上传。 为了简单方便,本文采用依次上传。

文件分片

首要需要定义好分片的大小,然后依次从文件中取出来。

File file = File('file path')
var sFile = await file.open();
try {
  int fileLength = sFile.lengthSync(); // 获取文件长度
  int chunkSize = 1024*1024; // 分片大小 这里设置 1M大小
  int x = 0; // 已经上传的长度
  int chunkIndex = 0; // 传到第几片了
  
  while (x < fileLength) {
    // 是否是最后一片了
    bool isLast = fileLength - x >= chunkSize ? false : true;
    // 获取当前这一片的长度,最后一片可能没有设定的分片大小那么长,
    // 想象一根 56cm 长的绳子,一次只取 10cm
    // 最后一次则只能取 56 - 50 = 6cm
    int _len = isLast ? fileLength - x : chunkSize;
    // 获取一片
    List<int> postData = sFile.readSync(_len).toList();
    
    // TODO 将分出来的 bytes 上传
    
    // 这里假设已经上传成功
    chunkIndex++;
    x += _len; // 记录已经取出来的长度
  }
} finally {
  sFile.close(); // 最后一定要关闭文件
}

将分出来的 bytes 上传

这里用 Dio 上传

以流的形式提交二进制数据:

// 二进制数据
List<int> postData = <int>[...];
await dio.post(
  url,
  data: Stream.fromIterable(postData.map((e) => [e])), //创建一个Stream<List<int>>
  options: Options(
    headers: {
      Headers.contentLengthHeader: postData.length, // 设置content-length
    },
  ),
);

以上片段来自于 Dio 官方文档:github.com/flutterchin…

更多细节请参考官网文档

切勿照抄这段官方文档

文档中的Stream.fromIterable(postData.map((e) => [e]))这段代码会导致上传非常慢,正确的写法应该是Stream.fromIterable([postData])

这里解释下为什么,官方文档这里是针对上传整个文件的二进制而编写的,postData.map((e) => [e]) 会将List<int> 转换成 List<List<int>>,也就是在进行分片操作,且每一片大小仅 1byte。这样当然会很慢。

而我们已经分好片了,所以这里直接把这一片放进去。当然,需要在外面套一层 List。因为 Dio 需要的是 List<List<int>>

接下来看根据文档编写上传代码

var data = Stream.fromIterable([postData]);
var headers = {
  Headers.contentLengthHeader: postData.length, // 如果要监听上传进度,则需要传文件长度
  'fileId': fileId, // 我们后台根据 fileId 来判断这一片是属于哪个文件
  'index': chunkIndex, // 告诉后台当前传到第几片了
};
if (isLast) {
  headers['chunkCount'] = chunkIndex + 1;
}
var res = await dio.post(
  'https://exp.com/chunk/upload',
  data: data,
  options: Options(headers: headers,contentType: 'application/octet-stream'),
  onSendProgress: (c, t) {
    var current = x + c; // x是已经上传成功的长度 c是当前请求已经上传的长度。
    var total = fileLength;
    // TODO 通知进度条
  },
);

使用 Isolate 上传

为了流畅性,我们选择使用 Isolate 进行上传。以免卡 UI。详细怎么转换,这里按下不表,就说一下我在其中遇到的问题。

Isolate 的错误处理机制

在 Isolate 的方法中执行 throw '上传失败' 语句无法被 catch 到。也就是说下面这种写法是无法捕捉到错误的。

try {
  Isolate.spawn(uploadVideoIsolate, param);
} catch (e, s) {
  print(e)
}

只能使用 Isolate.spawn() 的 onError 参数,传递一个 receivePort.sendPort 来接收错误信息。

代码说话

ReceivePort onError = ReceivePort();
onError.listen((message) {
  // message 是一个 List<String> 数组
  // 取出错误信息
  String message = message[0];
  StackTrace stack = StackTrace.fromString(message[1]);
});
Isolate.spawn(uploadVideoIsolate, param, onError: onError.sendPort);

uploadVideoIsolate 函数 throw 错误时,onError 便能接收到错误信息