当要上传的文件过大,又需要支持断点续传的话,则需要将一个大文件分成一片一片的,分多个请求依次或者同时上传。 为了简单方便,本文采用依次上传。
文件分片
首要需要定义好分片的大小,然后依次从文件中取出来。
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 便能接收到错误信息