事件循环(Event Loop)是事件驱动编程模型的核心机制,用于管理和分派事件,确保程序能够响应外部或内部触发的事件(如用户输入、硬件状态变化、定时器等)
事件循环(Event Loop)是事件驱动编程模型的核心机制,用于管理和分派事件,确保程序能够响应外部或内部触发的事件(如用户输入、硬件状态变化、定时器等)。结合代码(ControlMonitor 和 ControlHardWareStaus),我们将深入探讨事件循环的原理、在 Windows Forms 环境中的实现、与你的硬件状态监控场景的关系,以及如何通过事件驱动模型优化代码,解决状态不一致问题(“UNCONNECTED”文本与 scralballpane资源_54.png 背景不匹配)并提升性能。
事件循环的核心概念
1. 什么是事件循环?
事件循环是一个持续运行的进程,负责监听事件队列中的事件,并在适当的时机调用相应的事件处理器(回调函数)。
它通常包含以下步骤:
- 检查事件队列:查看是否有新的事件(如用户点击、硬件状态变化)。
- 分派事件:将事件交给注册的事件处理器执行。
- 处理回调:执行事件处理器中的逻辑(如更新 UI、处理数据)。
- 等待新事件:如果队列为空,等待新事件到达。
在 Windows Forms 应用中,事件循环由 UI 线程的消息循环(Message Loop)实现,负责处理 Windows 消息(如 WM_PAINT、WM_MOUSECLICK)和用户定义的事件(如 C# 的 event)。2. Windows Forms 中的事件循环
- 消息循环:Windows Forms 应用运行在一个单线程 UI 模型中,UI 线程维护一个消息队列,由 Windows 操作系统通过消息泵(message pump)驱动。消息队列包含:
- 系统消息:如鼠标点击、键盘输入、窗口重绘。
- 用户定义消息:如通过 Control.Invoke 或 BeginInvoke 发送的委托。
- 消息处理:UI 线程通过 Application.Run 启动消息循环,调用 GetMessage(或类似 API)获取消息,并分派给窗口过程(Window Procedure)处理。
- 异步与同步:
- Control.Invoke:同步调用,阻塞调用线程直到 UI 线程处理完成。
- Control.BeginInvoke:异步调用,将委托放入消息队列,调用线程立即返回。
ControlHardWareStaus 使用 BeginInvoke 将 UI 更新(如设置 labelContent.Text 和 BackgroundImage)放入消息队列,由 UI 线程的事件循环处理。
3. 事件循环与你的问题你的代码中,ControlMonitor 使用一个后台线程(_MonitorHardWareStatus)轮询硬件状态,每秒调用 UpdataBIBStatus 等方法更新 ControlHardWareStaus 控件的 Content 和 HardwareConnectStatus。这些更新通过 BeginInvoke 异步发送到 UI 线程的消息队列。
由于事件循环按顺序处理消息队列,快速的状态变化可能导致以下问题:
- 消息顺序错乱:Content = "UNCONNECTED" 和 HardwareConnectStatus = InUsing 的更新可能被拆分为多个 BeginInvoke 调用,UI 线程可能先处理 HardwareConnectStatus(导致背景显示 scralballpane资源_54.png),后处理 Content(显示“UNCONNECTED”)。
- 轮询延迟:每秒轮询可能错过硬件状态的快速变化,导致 UI 显示滞后。
- 性能开销:轮询即使状态未变化也重复更新,增加消息队列负担。
事件驱动模型通过将硬件状态变化转化为事件,直接推送到事件循环,可以解决这些问题。
事件循环在你的场景中的应用在你的代码中,事件循环主要由 Windows Forms 的 UI 线程消息循环驱动,处理 UI 更新和用户交互。
以下是如何利用事件循环实现事件驱动模型,替换轮询机制:
1. 事件循环的工作方式
- 事件源:HardwareMgr 检测硬件状态变化(如 Tc.BIBStatus 从 0 变为 1),生成事件。
- 事件队列:事件通过 C# 的 event 机制触发,事件处理器(如 OnBIBStatusChanged)将 UI 更新委托(通过 Invoke)放入 UI 线程的消息队列。
- 事件处理:UI 线程的事件循环从消息队列中取出委托,执行 UI 更新(如设置 ControlHardWareStaus.Content 和 HardwareConnectStatus)。
- 线程安全:使用 ConcurrentDictionary 和 lock 确保事件处理器和控件访问是线程安全的。
2. 问题根因与事件循环状态不一致问题(“UNCONNECTED”与 scralballpane资源_54.png 不匹配)与事件循环的关系:
- 异步更新:ControlHardWareStaus 的 Content 和 HardwareConnectStatus 使用 BeginInvoke 异步更新,多个委托可能在消息队列中乱序执行。
- 快速状态变化:后台线程轮询可能捕获到硬件状态的中间状态,导致事件循环处理不一致的更新。
- 消息队列压力:频繁的 BeginInvoke 调用增加事件循环的负担,可能导致延迟或错序。
通过事件驱动模型,可以:
- 同步更新:使用 Invoke 确保 Content 和 HardwareConnectStatus 原子性地更新。
- 实时触发:硬件状态变化立即触发事件,减少延迟。
- 减少消息:仅在状态变化时发送更新,减轻事件循环负担。
优化方案:基于事件循环的事件驱动模型以下是详细的优化方案,将你的代码从轮询转换为事件驱动,利用 Windows Forms 的事件循环解决问题。
1. 修改 HardwareMgr 以触发事件扩展 HardwareMgr 定义事件,并在硬件状态变化时触发。
优化代码:csharp
public class HardwareMgr
{
public event EventHandler<(string Id, HardwareConnectStatus Status, string Content)> BIBStatusChanged;
public event EventHandler<(string Id, HardwareConnectStatus Status, string StatusStr, string Content)> ChannelStatusChanged;
public event EventHandler<(string Id, HardwareConnectStatus Status, string StatusStr, string Content)> PowerStatusChanged;
public event EventHandler<(string Id, HardwareConnectStatus Status, string StatusStr, string Content)> ChamberStatusChanged;
public event EventHandler<(string Id, HardwareConnectStatus Status, string StatusStr, string Content)> HeatBoardStatusChanged;
public event EventHandler<(string Id, HardwareConnectStatus Status, string StatusStr, string Content)> AUXStatusChanged;
public ConcurrentDictionary<string, EvnChamber> ChamberMap { get; } = new ConcurrentDictionary<string, EvnChamber>();
public AuxCtrlBoard AuxCtrlBoard { get; set; }
public void UpdateBIBStatus(string id, int bibStatus)
{
HardwareConnectStatus status;
string content;
switch (bibStatus)
{
case 0:
status = HardwareConnectStatus.Idle;
content = "UNCONNECTED";
break;
case 1:
status = HardwareConnectStatus.InUsing;
content = "CONNECTED";
break;
default:
return;
}
Console.WriteLine($"HardwareMgr: BIB {id} updated to Status={status}, Content={content}");
BIBStatusChanged?.Invoke(this, (id, status, content));
}
public void UpdateChannelStatus(string id, HardwareConnectStatus status, string errCode)
{
string statusStr = status.ToString();
Console.WriteLine($"HardwareMgr: Channel {id} updated to Status={status}, Content={errCode}");
ChannelStatusChanged?.Invoke(this, (id, status, statusStr, errCode));
}
public void UpdatePowerStatus(string id, HardwareConnectStatus status, string errCode)
{
string statusStr = status.ToString();
Console.WriteLine($"HardwareMgr: Power {id} updated to Status={status}, Content={errCode}");
PowerStatusChanged?.Invoke(this, (id, status, statusStr, errCode));
}
public void UpdateChamberStatus(string id, HardwareConnectStatus status, string errCode)
{
string statusStr = status.ToString();
Console.WriteLine($"HardwareMgr: Chamber {id} updated to Status={status}, Content={errCode}");
ChamberStatusChanged?.Invoke(this, (id, status, statusStr, errCode));
}
public void UpdateHeatBoardStatus(string id, HardwareConnectStatus status, double boardTemp)
{
string statusStr = status.ToString();
string content = $"{boardTemp}℃";
Console.WriteLine($"HardwareMgr: HeatBoard {id} updated to Status={status}, Content={content}");
HeatBoardStatusChanged?.Invoke(this, (id, status, statusStr, content));
}
public void UpdateAUXStatus(string id, HardwareConnectStatus status, string errCode)
{
string statusStr = status.ToString();
Console.WriteLine($"HardwareMgr: AUX {id} updated to Status={status}, Content={errCode}");
AUXStatusChanged?.Invoke(this, (id, status, statusStr, errCode));
}
// 示例硬件监控线程
public HardwareMgr()
{
Thread hardwareMonitor = new Thread(MonitorHardware)
{
IsBackground = true
};
hardwareMonitor.Start();
}
private void MonitorHardware()
{
while (true)
{
foreach (EvnChamber chamber in ChamberMap.Values)
{
foreach (TestSection ts in chamber.TestAreaMap.Values)
{
foreach (TestChannel tc in ts.TestChannelMap.Values)
{
int newBIBStatus = GetBIBStatusFromHardware(tc.Id);
UpdateBIBStatus(tc.Id, newBIBStatus);
HardwareConnectStatus channelStatus = GetChannelStatusFromHardware(tc.Id);
string errCode = GetChannelErrCodeFromHardware(tc.Id);
UpdateChannelStatus(tc.Id, channelStatus, errCode);
}
foreach (PowerSupply ps in ts.PowerSupplyMap.Values)
{
HardwareConnectStatus powerStatus = GetPowerStatusFromHardware(ps.Id);
string errCode = GetPowerErrCodeFromHardware(ps.Id);
UpdatePowerStatus(ps.Id, powerStatus, errCode);
}
foreach (HeatBoard hb in ts.HeatBoardMap.Values)
{
HardwareConnectStatus heatBoardStatus = GetHeatBoardStatusFromHardware(hb.Id);
double boardTemp = GetHeatBoardTempFromHardware(hb.Id);
UpdateHeatBoardStatus(hb.Id, heatBoardStatus, boardTemp);
}
}
HardwareConnectStatus chamberStatus = GetChamberStatusFromHardware(chamber.Id);
string chamberErrCode = GetChamberErrCodeFromHardware(chamber.Id);
UpdateChamberStatus(chamber.Id, chamberStatus, chamberErrCode);
}
HardwareConnectStatus auxStatus = GetAUXStatusFromHardware(AuxCtrlBoard.Id);
string auxErrCode = GetAUXErrCodeFromHardware(AuxCtrlBoard.Id);
UpdateAUXStatus(AuxCtrlBoard.Id, auxStatus, auxErrCode);
Thread.Sleep(100); // 每 100ms 检查一次
}
}
// 假设的硬件接口方法
private int GetBIBStatusFromHardware(string id) => /* 硬件接口 */;
private HardwareConnectStatus GetChannelStatusFromHardware(string id) => /* 硬件接口 */;
private string GetChannelErrCodeFromHardware(string id) => /* 硬件接口 */;
private HardwareConnectStatus GetPowerStatusFromHardware(string id) => /* 硬件接口 */;
private string GetPowerErrCodeFromHardware(string id) => /* 硬件接口 */;
private HardwareConnectStatus GetChamberStatusFromHardware(string id) => /* 硬件接口 */;
private string GetChamberErrCodeFromHardware(string id) => /* 硬件接口 */;
private HardwareConnectStatus GetHeatBoardStatusFromHardware(string id) => /* 硬件接口 */;
private double GetHeatBoardTempFromHardware(string id) => /* 硬件接口 */;
private HardwareConnectStatus GetAUXStatusFromHardware(string id) => /* 硬件接口 */;
private string GetAUXErrCodeFromHardware(string id) => /* 硬件接口 */;
}
说明:
- 定义事件,传递硬件 ID、状态、状态字符串和内容。
- 使用 ConcurrentDictionary 确保 ChamberMap 的线程安全性。
- 示例 MonitorHardware 方法模拟硬件状态监控,实际实现需替换为硬件接口。
2. 修改 ControlMonitor 以利用事件循环移除轮询机制,订阅 HardwareMgr 的事件,通过 UI 线程的事件循环处理更新。优化代码:csharp
public partial class ControlMonitor : UserControl, ILanguage
{
private readonly object _uiLock = new object();
private readonly ConcurrentDictionary<string, ControlHardWareStaus> EvnChamberControls = new ConcurrentDictionary<string, ControlHardWareStaus>();
private readonly ConcurrentDictionary<string, ControlHardWareStaus> PowerSupplyControls = new ConcurrentDictionary<string, ControlHardWareStaus>();
private readonly ConcurrentDictionary<string, ControlHardWareStaus> TestChannelControls = new ConcurrentDictionary<string, ControlHardWareStaus>();
private readonly ConcurrentDictionary<string, ControlHardWareStaus> BIBControls = new ConcurrentDictionary<string, ControlHardWareStaus>();
private readonly ConcurrentDictionary<string, ControlHardWareStaus> HeatBoardControls = new ConcurrentDictionary<string, ControlHardWareStaus>();
private readonly ConcurrentDictionary<string, ControlHardWareStaus> AUXBoardControls = new ConcurrentDictionary<string, ControlHardWareStaus>();
private bool Monitor = false;
public ControlMonitor()
{
InitializeComponent();
SetStyle(ControlStyles.UserPaint, true);
SetStyle(ControlStyles.AllPaintingInWmPaint, true);
SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
Monitor = true;
if (!DesignMode)
{
CreateHardWareStatusControls();
HardwareMgr.BIBStatusChanged += OnBIBStatusChanged;
HardwareMgr.ChannelStatusChanged += OnChannelStatusChanged;
HardwareMgr.PowerStatusChanged += OnPowerStatusChanged;
HardwareMgr.ChamberStatusChanged += OnChamberStatusChanged;
HardwareMgr.HeatBoardStatusChanged += OnHeatBoardStatusChanged;
HardwareMgr.AUXStatusChanged += OnAUXStatusChanged;
// 初始化状态
UpdataAUXStatus();
UpdataChamberStatus();
UpdataPowerStatus();
UpdataChannelStatus();
UpdataBIBStatus();
UpdataHeatBoardStatus();
}
}
private void OnBIBStatusChanged(object sender, (string Id, HardwareConnectStatus Status, string Content) e)
{
try
{
lock (_uiLock)
{
if (BIBControls.TryGetValue(e.Id + "_BIB", out var control))
{
if (control.InvokeRequired)
{
control.Invoke((MethodInvoker)delegate
{
control.HardwareConnectStatus = e.Status;
control.Content = e.Content;
Console.WriteLine($"UI Updated: BIB {e.Id}, Status={e.Status}, Content={e.Content}");
});
}
else
{
control.HardwareConnectStatus = e.Status;
control.Content = e.Content;
Console.WriteLine($"UI Updated: BIB {e.Id}, Status={e.Status}, Content={e.Content}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error updating BIB {e.Id}: {ex.Message}");
}
}
private void OnChannelStatusChanged(object sender, (string Id, HardwareConnectStatus Status, string StatusStr, string Content) e)
{
try
{
lock (_uiLock)
{
if (TestChannelControls.TryGetValue(e.Id, out var control))
{
if (control.InvokeRequired)
{
control.Invoke((MethodInvoker)delegate
{
control.HardwareConnectStatus = e.Status;
control.Status = e.StatusStr;
control.Content = e.Content;
Console.WriteLine($"UI Updated: Channel {e.Id}, Status={e.Status}, Content={e.Content}");
});
}
else
{
control.HardwareConnectStatus = e.Status;
control.Status = e.StatusStr;
control.Content = e.Content;
Console.WriteLine($"UI Updated: Channel {e.Id}, Status={e.Status}, Content={e.Content}");
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error updating Channel {e.Id}: {ex.Message}");
}
}
// 类似方法 for Power, Chamber, HeatBoard, AUX
private void UpdataBIBStatus()
{
foreach (EvnChamber Chamber in HardwareMgr.ChamberMap.Values)
{
foreach (string secKey in Chamber.TestAreaMap.Keys)
{
TestSection Ts = Chamber.TestAreaMap[secKey];
foreach (string key in Ts.TestChannelMap.Keys)
{
TestChannel Tc = Ts.TestChannelMap[key];
HardwareConnectStatus channelStatus;
string bibStr;
switch (Tc.BIBStatus)
{
case 0:
channelStatus = HardwareConnectStatus.Idle;
bibStr = "UNCONNECTED";
break;
case 1:
channelStatus = HardwareConnectStatus.InUsing;
bibStr = "CONNECTED";
break;
default:
continue;
}
if (BIBControls.TryGetValue(Tc.Id + "_BIB", out var control))
{
if (control.InvokeRequired)
{
control.Invoke((MethodInvoker)delegate
{
control.HardwareConnectStatus = channelStatus;
control.Content = bibStr;
});
}
else
{
control.HardwareConnectStatus = channelStatus;
control.Content = bibStr;
}
}
}
}
}
}
// 其他 UpdataXXXStatus 方法保持不变
}
说明:
- 移除 _MonitorHardWareStatus 和 Application.DoEvents(),依赖事件循环处理更新。
- 使用 ConcurrentDictionary 确保控件集合的线程安全性。
- 使用 Invoke 确保 UI 更新同步,lock 保护事件处理器。
- 保留 UpdataXXXStatus 方法用于初始化。
3. 优化 ControlHardWareStaus使用同步 Invoke 确保 UI 更新一致。优化代码:csharp
public partial class ControlHardWareStaus : UserControl
{
private string m_HardName = "";
private string m_Content = "";
private string m_Status = "";
private HardwareConnectStatus m_HardwareConnectStatus = HardwareConnectStatus.Idle;
public ControlHardWareStaus()
{
InitializeComponent();
}
public string HardName
{
get => m_HardName;
set
{
if (m_HardName == value) return;
m_HardName = value;
if (!string.IsNullOrEmpty(value))
{
UpdateLabel(labelName, value);
}
}
}
public string Content
{
get => m_Content;
set
{
if (m_Content == value) return;
m_Content = value;
if (!string.IsNullOrEmpty(value))
{
UpdateLabel(labelContent, value);
}
}
}
public string Status
{
get => m_Status;
set
{
if (m_Status == value) return;
m_Status = value;
if (!string.IsNullOrEmpty(value))
{
UpdateLabel(labelStatus, value);
}
}
}
public HardwareConnectStatus HardwareConnectStatus
{
get => m_HardwareConnectStatus;
set
{
if (m_HardwareConnectStatus == value) return;
m_HardwareConnectStatus = value;
UpdateBackgroundImage();
}
}
private void UpdateLabel(Label label, string value)
{
if (InvokeRequired)
{
Invoke((MethodInvoker)(() => label.Text = value));
}
else
{
label.Text = value;
}
}
private void UpdateBackgroundImage()
{
if (InvokeRequired)
{
Invoke((MethodInvoker)(() =>
{
BackgroundImage = m_HardwareConnectStatus switch
{
HardwareConnectStatus.Idle => Properties.Resources.scralballpane资源_53,
HardwareConnectStatus.InUsing => Properties.Resources.scralballpane资源_54,
HardwareConnectStatus.Malfunction => Properties.Resources.scralballpane资源_55,
_ => BackgroundImage
};
}));
}
else
{
BackgroundImage = m_HardwareConnectStatus switch
{
HardwareConnectStatus.Idle => Properties.Resources.scralballpane资源_53,
HardwareConnectStatus.InUsing => Properties.Resources.scralballpane资源_54,
HardwareConnectStatus.Malfunction => Properties.Resources.scralballpane资源_55,
_ => BackgroundImage
};
}
}
private void labelContent_Click(object sender, EventArgs e)
{
if (m_HardwareConnectStatus == HardwareConnectStatus.Malfunction)
{
string errHardName = HardName;
string errSeason = "Unknown error";
Dictionary<string, object> ErrorCodeMap = HardwareMgr.ErrorCodeMap;
Hardware hardware = HardwareMgr.GetHardWare(HardName);
if (hardware != null && ErrorCodeMap.ContainsKey(hardware.HardClass))
{
if (ErrorCodeMap[hardware.HardClass] is Dictionary<string, object> CodeMap && CodeMap.ContainsKey(m_Content))
{
errSeason = GlobalCache.Language == "CN"
? (CodeMap[m_Content] as Dictionary<string, object>)["CN"].ToString()
: (CodeMap[m_Content] as Dictionary<string, object>)["EN"].ToString();
}
}
CustomMsgBox.Show(errSeason, errHardName, MessageBoxButtons.OK);
}
}
}
说明:
- 使用 Invoke 替代 BeginInvoke,确保 UI 更新同步。
- 抽取 UpdateLabel 和 UpdateBackgroundImage,简化代码。
4. 添加状态缓存在 HardwareMgr 中缓存状态,仅在变化时触发事件,减少事件循环负担。优化代码:csharp
public class HardwareMgr
{
private readonly Dictionary<string, (int BIBStatus, HardwareConnectStatus Status, string Content)> _bibStatusCache = new Dictionary<string, (int, HardwareConnectStatus, string)>();
// 类似缓存 for Channel, Power, Chamber, HeatBoard, AUX
public void UpdateBIBStatus(string id, int bibStatus)
{
HardwareConnectStatus status;
string content;
switch (bibStatus)
{
case 0:
status = HardwareConnectStatus.Idle;
content = "UNCONNECTED";
break;
case 1:
status = HardwareConnectStatus.InUsing;
content = "CONNECTED";
break;
default:
return;
}
if (!_bibStatusCache.ContainsKey(id) || _bibStatusCache[id] != (bibStatus, status, content))
{
Console.WriteLine($"HardwareMgr: BIB {id} updated to Status={status}, Content={content}");
_bibStatusCache[id] = (bibStatus, status, content);
BIBStatusChanged?.Invoke(this, (id, status, content));
}
}
// 类似缓存逻辑 for UpdateChannelStatus, UpdatePowerStatus, etc.
}
说明:
- 使用 _bibStatusCache 缓存 BIB 状态,仅在变化时触发事件。
- 减少不必要的事件触发,降低事件循环的处理压力。
中文详解事件循环的角色在你的场景中,事件循环由 Windows Forms 的 UI 线程消息循环实现,负责处理:
- 用户交互:如 labelContent_Click 事件。
- UI 更新:通过 Invoke 或 BeginInvoke 触发的控件更新(如 labelContent.Text 和 BackgroundImage)。
- 自定义事件:通过 HardwareMgr 的事件(如 BIBStatusChanged)触发的更新。
当前代码的问题是,轮询机制通过 BeginInvoke 向事件循环发送大量异步消息,导致 Content 和 HardwareConnectStatus 的更新可能乱序,造成状态不一致。
优化方案的核心
- 事件驱动模型:
- 移除 _MonitorHardWareStatus 的轮询循环。
- HardwareMgr 在硬件状态变化时触发事件(如 BIBStatusChanged),将更新委托给 UI 线程的事件循环。
- 同步 UI 更新:
- 使用 Invoke 替代 BeginInvoke,确保 Content 和 HardwareConnectStatus 的更新是原子性的。
- 线程安全:
- 使用 ConcurrentDictionary 存储控件集合,lock 保护事件处理器。
- 状态缓存:
- 在 HardwareMgr 中缓存状态,仅在变化时触发事件,减少事件循环负担。
- 日志记录:
- 添加日志,追踪事件触发和 UI 更新,便于调试。
实施步骤
- 立即实施:
- 修改 ControlHardWareStaus 使用 Invoke(方案 3)。
- 移除 _MonitorHardWareStatus 和 Application.DoEvents()。
- 中期优化:
- 实现 HardwareMgr 的事件触发(方案 1)。
- 订阅事件并实现处理器(方案 2)。
- 使用 ConcurrentDictionary(方案 2)。
- 长期优化:
- 实现硬件状态监控(方案 1 中的 MonitorHardware)。
- 添加状态缓存(方案 4)。
- 测试验证:
- 模拟快速状态变化(Tc.BIBStatus 从 0 到 1),检查 UI 是否一致。
- 使用日志验证事件触发和更新顺序。
预期效果
- 状态一致性:Content 和 HardwareConnectStatus 同步更新,解决不一致问题。
- 实时性:硬件状态变化立即反映到 UI。
- 性能提升:事件驱动减少消息队列负担,缓存减少不必要更新。
- 可维护性:代码结构清晰,日志便于调试。
通过利用 Windows Forms 的事件循环和事件驱动模型,你的程序将更高效、可靠,且易于扩展。
深入了解消息队列
异步编程模型
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)