当前博文未提及该问题,以下是基于C知道的知识内容为您回答。
针对您提出的 .NET 4.5.2 WinForm 文件上传需求,特别是在“日志实时保存至本地”这一点的优化,以及保持“运行内存占用小”的要求,以下方案对日志持久化机制进行了针对性改进。本方案采用**异步文件写入流**替代内存缓存,确保日志即刻落盘,同时结合**流式文件传输**来保证程序的高效与轻量。
### 1. 核心架构与优化策略
为了满足“日志实时保存”且“内存占用小”的双重目标,本方案采用了以下关键技术策略:
| 优化维度 | 典型问题 | 优化策略 |
| :--- | :--- | :--- |
| **日志持久化** | 内存缓存导致程序崩溃时丢失日志 | 使用 `StreamWriter` 异步直写文件,配合 AutoFlush [ref_3] |
| **内存控制** | UI 控件积累大量文本导致内存溢出 | TextBox 仅显示最新 N 行,历史日志仅存于文件 [ref_4] |
| **文件传输** | 大文件全量读取占用大量内存 | `FileStream` 分块读写,缓冲区控制在 81KB 左右 [ref_6] |
| **任务调度** | 多线程并发导致内存峰值 | 严格单任务串行执行,利用 `CancellationToken` 统一控制 [ref_5] |
### 2. 完整代码实现
#### 2.1 高性能日志组件(实时落盘版)
该组件实现了日志的“写文件”与“显示UI”的解耦。文件写入是实时的,而 UI 显示仅保留最新的少量信息,从而极大降低内存消耗。
```csharp
using System;
using System.IO;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Forms;
public class PersistentLogger : IDisposable
{
private readonly TextBox _uiTextBox;
private readonly StreamWriter _logWriter;
private readonly object _writeLock = new object();
// UI 仅保留最新的 20 行日志,防止内存膨胀 [ref_4]
private readonly int _maxUiLines = 20;
public PersistentLogger(TextBox textBox, string logDirectory)
{
_uiTextBox = textBox;
if (!Directory.Exists(logDirectory)) Directory.CreateDirectory(logDirectory);
string logPath = Path.Combine(logDirectory, $"UploadLog_{DateTime.Now:yyyyMMdd_HHmmss}.txt");
// 初始化文件写入流,设置为自动刷新,确保实时性 [ref_3]
_logWriter = new StreamWriter(logPath, true, System.Text.Encoding.UTF8) { AutoFlush = true };
}
public void Log(string message, bool isError = false)
{
string logEntry = $"[{DateTime.Now:HH:mm:ss.fff}] {(isError ? "[ERROR] " : "")}{message}";
// 1. 实时写入文件(加锁保证线程安全)
Task.Run(() =>
{
lock (_writeLock)
{
try
{
_logWriter.WriteLine(logEntry);
}
catch (ObjectDisposedException) { /* 忽略关闭时的写入异常 */ }
}
});
// 2. UI 异步更新(仅保留少量内容)
UpdateUiSafe(logEntry);
}
private void UpdateUiSafe(string text)
{
if (_uiTextBox == null || _uiTextBox.IsDisposed) return;
try
{
if (_uiTextBox.InvokeRequired)
{
_uiTextBox.Invoke(new Action(() => AppendTextToUi(text)));
}
else
{
AppendTextToUi(text);
}
}
catch { }
}
private void AppendTextToUi(string text)
{
_uiTextBox.AppendText(text + Environment.NewLine);
// 内存优化:控制 UI 行数,移除旧行 [ref_4]
string[] lines = _uiTextBox.Lines;
if (lines.Length > _maxUiLines)
{
// 重建 Lines 数组,保留最后 N 行
string[] newLines = new string[_maxUiLines];
Array.Copy(lines, lines.Length - _maxUiLines, newLines, 0, _maxUiLines);
_uiTextBox.Lines = newLines;
}
_uiTextBox.ScrollToCaret();
}
public void Dispose()
{
lock (_writeLock)
{
_logWriter?.Close();
_logWriter?.Dispose();
}
}
}
```
#### 2.2 上传服务核心逻辑(流式与重试)
继续沿用流式复制方案,确保在处理大量文件或大文件时内存占用平稳。
```csharp
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
public class StreamUploadService
{
private readonly PersistentLogger _logger;
private readonly int _maxRetryCount = 3;
private readonly int _timeoutMilliseconds = 15000; // 15秒超时
public bool IsBusy { get; private set; }
public CancellationTokenSource CurrentCts { get; private set; }
public event Action<int, int, string> ProgressChanged;
public StreamUploadService(PersistentLogger logger)
{
_logger = logger;
}
public async Task StartUploadAsync(string sourcePath, string targetPath)
{
if (IsBusy)
{
_logger.Log("系统正忙,任务执行时不可添加新任务。", true);
return;
}
IsBusy = true;
CurrentCts = new CancellationTokenSource();
try
{
_logger.Log($"开始扫描目录: {sourcePath}");
// 仅获取文件列表,不加载文件内容 [ref_1]
var files = Directory.GetFiles(sourcePath, "*.*", SearchOption.TopDirectoryOnly);
int totalFiles = files.Length;
int processedFiles = 0;
foreach (var file in files)
{
CurrentCts.Token.ThrowIfCancellationRequested();
string fileName = Path.GetFileName(file);
string destFile = Path.Combine(targetPath, fileName);
processedFiles++;
ProgressChanged?.Invoke(processedFiles, totalFiles, fileName);
await ProcessFileWithRetryAsync(file, destFile, CurrentCts.Token);
}
_logger.Log("所有文件处理完毕。");
}
catch (OperationCanceledException)
{
_logger.Log("用户取消了上传任务。", true);
}
catch (Exception ex)
{
_logger.Log($"发生未处理异常: {ex.Message}", true);
}
finally
{
IsBusy = false;
CurrentCts?.Dispose();
CurrentCts = null;
// 任务结束后建议手动 GC,释放文件句柄 [ref_4]
GC.Collect();
GC.WaitForPendingFinalizers();
}
}
private async Task ProcessFileWithRetryAsync(string sourceFile, string destFile, CancellationToken token)
{
int retryCount = 0;
bool success = false;
while (!success && retryCount <= _maxRetryCount)
{
try
{
token.ThrowIfCancellationRequested();
_logger.Log($"正在上传: {Path.GetFileName(sourceFile)} (尝试 {retryCount + 1}/{_maxRetryCount + 1})");
// 定义上传任务与超时控制 [ref_5]
var uploadTask = CopyFileWithStreamAsync(sourceFile, destFile, token);
var timeoutTask = Task.Delay(_timeoutMilliseconds, token);
var completedTask = await Task.WhenAny(uploadTask, timeoutTask);
if (completedTask == timeoutTask)
{
throw new TimeoutException("文件传输超时");
}
await uploadTask;
success = true;
_logger.Log($"成功: {Path.GetFileName(sourceFile)}");
}
catch (Exception ex)
{
retryCount++;
if (retryCount > _maxRetryCount)
{
_logger.Log($"失败: {Path.GetFileName(sourceFile)} (超过最大重试次数)。原因: {ex.Message}", true);
}
else
{
_logger.Log($"重试中... 错误: {ex.Message}", true);
await Task.Delay(2000, token);
}
}
}
}
// 流式复制,低内存占用 [ref_6]
private async Task CopyFileWithStreamAsync(string source, string destination, CancellationToken token)
{
await Task.Run(() =>
{
string destDir = Path.GetDirectoryName(destination);
if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir);
// 使用 FileStream,指定 81920 字节缓冲区,平衡速度与内存
using (FileStream sourceStream = new FileStream(source, FileMode.Open, FileAccess.Read, FileShare.Read, 81920, true))
using (FileStream destStream = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None, 81920, true))
{
sourceStream.CopyTo(destStream, 81920);
}
}, token);
}
public void CancelUpload()
{
CurrentCts?.Cancel();
}
}
```
#### 2.3 WinForm 界面集成
```csharp
using System;
using System.IO;
using System.Windows.Forms;
namespace EfficientUploader
{
public partial class MainForm : Form
{
private StreamUploadService _uploadService;
private PersistentLogger _logger;
// 配置路径
private readonly string _sourcePath = @"C:\Temp\UploadSource";
private readonly string _targetPath = @"\\192.168.1.100\PublicShare";
private readonly string _logDir = Path.Combine(Application.StartupPath, "Logs");
public MainForm()
{
InitializeComponent();
InitializeServices();
}
private void InitializeServices()
{
// 初始化日志组件
_logger = new PersistentLogger(txtLog, _logDir);
_uploadService = new StreamUploadService(_logger);
_uploadService.ProgressChanged += OnProgressChanged;
}
private void btnStart_Click(object sender, EventArgs e)
{
if (!Directory.Exists(_sourcePath))
{
MessageBox.Show("源路径不存在!");
return;
}
btnStart.Enabled = false;
btnCancel.Enabled = true;
// 异步启动任务
_uploadService.StartUploadAsync(_sourcePath, _targetPath);
}
private void btnCancel_Click(object sender, EventArgs e)
{
_uploadService.CancelUpload();
}
private void OnProgressChanged(int current, int total, string fileName)
{
if (this.InvokeRequired)
{
this.Invoke(new Action<int, int, string>(OnProgressChanged), current, total, fileName);
return;
}
progressBar1.Maximum = total;
progressBar1.Value = current;
lblStatus.Text = $"进度: {current}/{total} - {fileName}";
if (current == total)
{
btnStart.Enabled = true;
btnCancel.Enabled = false;
}
}
protected override void OnFormClosing(FormClosingEventArgs e)
{
// 释放日志资源,确保最后一条日志写入磁盘
_logger?.Dispose();
base.OnFormClosing(e);
}
}
}
```
### 3. 方案亮点说明
1. **日志实时落盘**:
通过 `StreamWriter` 并开启 `AutoFlush` 属性,每一条日志在生成时都会立即写入磁盘文件。这解决了传统方案中先将日志存在内存 StringBuilder 中,定期批量写入可能导致的程序崩溃时日志丢失问题 [ref_3]。
2. **UI 内存隔离**:
TextBox 控件仅作为“监视窗口”,只保留最新的 20 行日志。无论程序运行多久、产生多少日志,UI 层的内存占用始终是恒定的,不会因为日志堆积而膨胀 [ref_4]。
3. **流式传输稳定性**:
使用 `FileStream` 进行 80KB 分块传输,即使上传 1GB 的文件,内存占用也仅限于缓冲区大小,不会因为文件过大导致 OutOfMemoryException [ref_6]。
4. **健壮的错误处理**:
针对网络波动或公共盘不可用的情况,内置了超时检测(15秒)和自动重试机制(最多3次),并在日志中详细记录每一次失败和重试的原因,便于运维排查 [ref_5]。