协议梳理 一般情况下,下载的功能模块,至少需要提供如下基础功能:资源下载、取消当前下载、资源是否下载成功、资源文件的大小、清除缓存文件 。而断点续传主要体现在取消当前下载后,再次下载时能在之前已下载的基础上继续下载 。这个能极大程度的减少我们服务器的带宽损耗,而且还能为用户减少流量,避免重复下载,提高用户体验 。
前置条件:资源必须支持断点续传 。如何确定可否支持?看看你的服务器是否支持range请求即可 。
实现步骤 1.定好协议 。我们用的http库是dio;通过校验md5检测文件缓存完整性;关于代码中的subdir,设计上认为资源会有多种:音频、视频、安装包等,每种资源分开目录进行存储 。
import 'package:dio/dio.dart';typedef progresscallback = void function(int count, int total);typedef canceltokenprovider = void function(canceltoken canceltoken);abstract class assetrepositoryprotocol {/// 下载单一资源future<string> downloadasset(string url,{string? subdir,progresscallback? onreceiveprogress,canceltokenprovider? canceltokenprovider,function(string)? done,function(exception)? failed});/// 取消下载,dio中通过canceltoken可控制void canceldownload(canceltoken canceltoken);/// 获取文件的缓存地址future<string?> filepathforasset(string url, {string? subdir});/// 检查文件是否缓存成功,简单对比md5future<string?> checkcachedsuccess(string url, {string? md5str});/// 查看缓存文件的大小future<int> cachedfilesize({string? subdir});/// 清除缓存future<void> clearcache({string? subdir});}2.实现抽象协议,其中httpmanagerprotocol内部封装了dio的相关请求 。
class assetrepository implements assetrepositoryprotocol {assetrepository(this.httpmanager);final httpmanagerprotocol httpmanager;@overridefuture<string> downloadasset(string url,{string? subdir,progresscallback? onreceiveprogress,canceltokenprovider? canceltokenprovider,function(string)? done,function(exception)? failed}) async {canceltoken canceltoken = canceltoken();if (canceltokenprovider != null) {canceltokenprovider(canceltoken);}final savepath = await _getsavepath(url, subdir: subdir);try {httpmanager.downloadfile(url: url,savepath: savepath + '.temp',onreceiveprogress: onreceiveprogress,canceltoken: canceltoken,done: () {done?.call(savepath);},failed: (e) {print(e);failed?.call(e);});return savepath;} catch (e) {print(e);rethrow;}}@overridevoid canceldownload(canceltoken canceltoken) {try {if (!canceltoken.iscancelled) {canceltoken.cancel();}} catch (e) {print(e);}}@overridefuture<string?> filepathforasset(string url, {string? subdir}) async {final path = await _getsavepath(url, subdir: subdir);final file = file(path);if (!(await file.exists())) {return null;}return path;}@overridefuture<string?> checkcachedsuccess(string url, {string? md5str}) async {string? path = await _getsavepath(url, subdir: filetype.video.dirname);bool iscached = await file(path).exists();if (iscached && (md5str != null && md5str.isnotempty)) {// 存在但是md5验证不通过file(path).readasbytes().then((uint8list str) {if (md5.convert(str).tostring() != md5str) {path = null;}});} else if (iscached) {return path;} else {path = null;}return path;}@overridefuture<int> cachedfilesize({string? subdir}) async {final dir = await _getdir(subdir: subdir);if (!(await dir.exists())) {return 0;}int totalsize = 0;await for (var entity in dir.list(recursive: true)) {if (entity is file) {try {totalsize += await entity.length();} catch (e) {print('get size of $entity failed with exception: $e');}}}return totalsize;}@overridefuture<void> clearcache({string? subdir}) async {final dir = await _getdir(subdir: subdir);if (!(await dir.exists())) {return;}dir.deletesync(recursive: true);}future<string> _getsavepath(string url, {string? subdir}) async {final savedir = await _getdir(subdir: subdir);if (!savedir.existssync()) {savedir.createsync(recursive: true);}final uri = uri.parse(url);final filename = uri.pathsegments.last;return savedir.path + filename;}future<directory> _getdir({string? subdir}) async {final cachedir = await gettemporarydirectory();late final directory savedir;if (subdir == null) {savedir = cachedir;} else {savedir = directory(cachedir.path + '/$subdir/');}return savedir;}}3.封装dio下载,实现资源断点续传 。
这里的逻辑比较重点,首先未缓存100%的文件,我们以.temp后缀进行命名,在每次下载时检测下是否有.temp的文件,拿到其文件字节大小;传入在header中的range字段,服务器就会去解析需要从哪个位置继续下载;下载全部完成后,再把文件名改回正确的后缀即可 。
final downloaddio = dio();future<void> downloadfile({required string url,required string savepath,required canceltoken canceltoken,progresscallback? onreceiveprogress,void function()? done,void function(exception)? failed,}) async {int downloadstart = 0;file f = file(savepath);if (await f.exists()) {// 文件存在时拿到已下载的字节数downloadstart = f.lengthsync();}print("start: $downloadstart");try {var response = await downloaddio.get<responsebody>(url,options: options(/// receive response data as a streamresponsetype: responsetype.stream,followredirects: false,headers: {/// 加入range请求头,实现断点续传"range": "bytes=$downloadstart-",},),);file file = file(savepath);randomaccessfile raf = file.opensync(mode: filemode.append);int received = downloadstart;int total = await _getcontentlength(response);stream<uint8list> stream = response.data!.stream;streamsubscription<uint8list>? subscription;subscription = stream.listen((data) {/// write files must be synchronizedraf.writefromsync(data);received += data.length;onreceiveprogress?.call(received, total);},ondone: () async {file.rename(savepath.replaceall('.temp', ''));await raf.close();done?.call();},onerror: (e) async {await raf.close();failed?.call(e);},cancelonerror: true,);canceltoken.whencancel.then((_) async {await subscription?.cancel();await raf.close();});} on dioerror catch (error) {if (canceltoken.iscancel(error)) {print("download cancelled");} else {failed?.call(error);}}}
写在最后 这篇文章确实没有技术含量,水一篇,但其实是实用的 。这个断点续传的实现有几个注意的点:
- 使用文件操作的方式,区分后缀名来管理缓存的资源;
- 安全性使用md5校验,这点非常重要,断点续传下载的文件,在完整性上可能会因为各种突发情况而得不到保障;
- 在资源管理协议上,我们将下载、检测、获取大小等方法都抽象出去,在业务调用时比较灵活 。
-- 展开阅读全文 --
推荐阅读
- 法兰绒怎么清洗 法兰绒怎么洗才好?法兰绒正确的洗涤方法
- 什么是马甲线马甲线指的是什么
- 涿怎么读 涿的读法
- Flutter实现顶部导航栏功能
- 未绝育的公猫可以养在一起吗
- Flutter定义tabbar底部导航路由跳转的方法
- Android组件化原理详细介绍
- Flutter自定义底部导航栏的方法
- Android简单实现动态权限获取相机权限及存储空间等多权限
