Xamarin.Forms如何从FirebaseStorage下载非图片文件到内部存储并显示进度
Xamarin.Forms实现Firebase Storage文件下载+通知栏进度显示方案
日志说明
你贴出的两条输出并非程序报错,是Android系统的常规调试日志,翻译如下:
显式并发复制GC释放了3个(16KB)AllocSpace对象,0个(0B)大对象空间(LOS)对象,49%可用,3014KB/6028KB,暂停84微秒,总耗时21.880毫秒
[Surface] 操作服务为空,值为false
原有代码的实际问题如下:
- HttpClient的
GetByteArrayAsync会将整个文件加载到内存,大文件场景下容易出现内存溢出 - 存储路径选择了应用私有目录,用户无法在系统文件管理器中找到下载的文件
- WebClient的
DownloadFile是同步方法,放在异步方法中会阻塞主线程,且未指定完整存储路径 - 没有进度回调逻辑,无法获取实时下载进度更新通知栏
实现步骤
1. 提前申请必要权限
- Android端:需要申请存储权限,Android 13及以上版本需要申请
READ_MEDIA_DOCUMENTS权限,显示前台通知需要申请FOREGROUND_SERVICE权限 - iOS端:需要在Info.plist中配置
NSDocumentDirectoryUsageDescription权限说明,若要允许用户访问下载的文件需要开启iTunes文件共享
2. 定义跨平台下载服务接口
在共享项目中定义依赖服务接口,用于调用各平台的下载和通知逻辑:
public interface IDownloadService { /// <summary> /// 下载文件 /// </summary> /// <param name="url">文件下载地址</param> /// <param name="fileName">保存的文件名</param> /// <param name="progressCallback">进度回调,返回0-100的进度值</param> Task DownloadFileAsync(string url, string fileName, Action<int> progressCallback); /// <summary> /// 更新通知栏下载进度 /// </summary> /// <param name="progress">进度值</param> /// <param name="fileName">文件名</param> void UpdateNotification(int progress, string fileName); }
3. 按钮点击逻辑调整
修改你的按钮点击事件,获取Firebase Storage下载地址后调用依赖服务执行下载:
private async void DownloadButton_Clicked(object sender, EventArgs e) { // 先申请存储权限,权限通过后再执行下载 var status = await Permissions.CheckStatusAsync<Permissions.StorageWrite>(); if (status != PermissionStatus.Granted) { status = await Permissions.RequestAsync<Permissions.StorageWrite>(); if (status != PermissionStatus.Granted) { await DisplayAlert("提示", "需要存储权限才能下载文件", "确定"); return; } } try { var fc = new FirebaseStorage("你自己的firebase存储bucket地址.appspot.com"); var fileUrl = await fc.Child("NonImage").Child("Pdf").Child("test.pdf").GetDownloadUrlAsync(); var downloadService = DependencyService.Get<IDownloadService>(); await downloadService.DownloadFileAsync(fileUrl, "test.pdf", progress => { // 进度更新时同步更新通知栏 downloadService.UpdateNotification(progress, "test.pdf"); }); await DisplayAlert("提示", "文件下载完成,已保存到系统下载目录", "确定"); } catch (Exception ex) { await DisplayAlert("错误", $"下载失败:{ex.Message}", "确定"); } }
4. Android平台服务实现
在Android项目中实现IDownloadService接口,包含分块下载逻辑和通知更新逻辑:
[assembly: Dependency(typeof(DownloadService_Android))] namespace YourAppName.Droid { public class DownloadService_Android : IDownloadService { private const int DownloadNotificationId = 1001; private const string DownloadChannelId = "download_notification_channel"; public async Task DownloadFileAsync(string url, string fileName, Action<int> progressCallback) { using var httpClient = new HttpClient(); // 只读取响应头,不立即加载全部内容 using var response = await httpClient.GetAsync(url, HttpCompletionOption.ResponseHeadersRead); response.EnsureSuccessStatusCode(); var totalBytes = response.Content.Headers.ContentLength ?? -1L; var downloadedBytes = 0L; // 保存到系统公共下载目录,用户可在文件管理器中找到 var downloadDir = Android.OS.Environment.GetExternalStoragePublicDirectory(Android.OS.Environment.DirectoryDownloads).AbsolutePath; var filePath = Path.Combine(downloadDir, fileName); using var contentStream = await response.Content.ReadAsStreamAsync(); using var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 8192, true); var buffer = new byte[8192]; int bytesRead; while ((bytesRead = await contentStream.ReadAsync(buffer)) > 0) { await fileStream.WriteAsync(buffer.AsMemory(0, bytesRead)); downloadedBytes += bytesRead; if (totalBytes > 0) { var progress = (int)((downloadedBytes * 100) / totalBytes); progressCallback?.Invoke(progress); } } } public void UpdateNotification(int progress, string fileName) { var context = Android.App.Application.Context; var notificationManager = context.GetSystemService(Android.Content.Context.NotificationService) as NotificationManager; // Android 8.0及以上需要创建通知渠道 if (Android.OS.Build.VERSION.SdkInt >= Android.OS.BuildVersionCodes.O) { var channel = new NotificationChannel(DownloadChannelId, "下载通知", NotificationImportance.Low) { Description = "显示文件下载进度", VibrationEnabled = false, Sound = null }; notificationManager.CreateNotificationChannel(channel); } var builder = new NotificationCompat.Builder(context, DownloadChannelId) .SetSmallIcon(Resource.Drawable.ic_download) // 替换为你自己的通知图标 .SetContentTitle(progress >= 100 ? "下载完成" : $"正在下载 {fileName}") .SetOngoing(progress < 100); if (progress < 100) { builder.SetProgress(100, progress, false); builder.SetContentText($"{progress}%"); } else { builder.SetProgress(0, 0, false); builder.SetContentText($"文件已保存到下载目录"); } notificationManager.Notify(DownloadNotificationId, builder.Build()); } } }
5. iOS平台服务实现
iOS端实现逻辑类似,下载路径选择Documents目录,通知用UserNotifications框架实现即可。
注意事项
- 大文件下载建议搭配Android的前台服务、iOS的后台下载能力实现,避免应用切后台后下载被系统终止
- 可添加下载任务队列逻辑,支持同时下载多个文件
- 下载前可先判断目标文件是否已存在,避免重复下载
内容的提问来源于stack exchange,提问作者Steven-Carrot
相关产品推荐
相关产品推荐

