我那些频繁创建又销毁的对象,订阅了一个单例的事件却忘了退订,结果它们一个都没被回收、内存一路涨到 OOM:一次 C# 事件订阅未取消导致内存泄漏的深度复盘

我有一类频繁创建又销毁的对象(随每个请求创建的 Handler),创建时订阅了一个长生命周期单例 EventBus 的事件:bus.DataChanged += this.OnDataChanged。功能没问题,可线上内存一路缓慢上涨、只涨不降,跑久了 OOM。内存 dump 才看明白:那些本该被回收的 Handler 一个都没被 GC,全被 EventBus 的委托链引用着。根因是它们订阅了单例的事件却从没取消订阅——在 C# 里 += 会让事件源(长命单例)通过委托持有对订阅者(Handler)的引用,于是即便我不再用这个 Handler,长命的 bus 仍攥着它、GC 认为它可达不能回收,每创建一个就泄漏一个。这篇复盘从故障现场讲到事件订阅建立的引用方向(源→订阅者,反直觉)、为什么短订长不退订会泄漏,再到用完 -= 退订、实现 IDisposable 在 Dispose 里退订、弱事件模式的完整正解,以及对象的命握在引用它的人手里要从被引用方向分析生命周期、重建立轻解除的通病要建立时就安排好解除、连接是双向羁绊的认知。

我那些频繁创建又销毁的对象,订阅了一个单例的事件却忘了退订,结果它们一个都没被回收、内存一路涨到 OOM:一次 C# 事件订阅未取消导致内存泄漏的深度复盘

那个内存泄漏是服务跑久了内存只涨不降、最后 OOM 才暴露的:我有一类频繁创建又销毁的对象(比如随每个请求/每个窗口创建的处理器 Handler),它们在创建时订阅了一个长生命周期单例(比如全局的 EventBus 或配置中心)的事件:bus.DataChanged += this.OnDataChanged。功能没问题,可线上内存一路缓慢上涨、只涨不降,跑久了就 OOM。我做了内存分析(dump 看对象引用),才看明白,后背发凉:那些"本该被销毁回收"的 Handler 对象,一个都没被 GC 回收,全都还活着、越积越多。根因是它们订阅了单例的事件、却从没取消订阅(-=)在 C# 里,bus.DataChanged += this.OnDataChanged 这行,会让事件源(bus,长命单例)持有一个指向订阅者(this,Handler)的引用(通过那个委托/事件处理器,委托内部引用了 this);于是即便我这边"不再使用"这个 Handler、把它的其他引用都释放了,那个长命的 bus 仍然通过事件委托牢牢攥着对 Handler 的引用;GC 的规则是"只要还有可达的引用,对象就不能回收",而 Handler 正被一个'永远不死'的单例引用着,所以它永远无法被回收;我每创建一个 Handler 就这么"泄漏"一个,内存自然只涨不降。根本原因是:事件订阅(+=)建立了"事件源→订阅者"的引用,若订阅者生命周期比事件源短、又没在销毁前取消订阅(-=),事件源就会一直引用着订阅者、阻止它被 GC,造成内存泄漏。问题的根,是短命对象订阅了长命对象的事件却没退订,长命对象通过事件委托持有对短命对象的引用,使其无法被 GC。这篇就把这次"事件订阅未取消导致内存泄漏"的坑,从头到尾复盘一遍。

故障现场:订阅了事件,却没退订

问题在于短命对象订阅了长命单例的事件,销毁时没退订,被事件源一直引用:

// ✗ 出问题的代码: 订阅了单例的事件, 却没取消订阅
public class Handler   // 频繁创建又销毁的短命对象
{
    private readonly EventBus _bus;
    public Handler(EventBus bus)
    {
        _bus = bus;
        _bus.DataChanged += OnDataChanged;   // ✗ 订阅单例bus的事件
        // ✗ 没有在Handler不再使用时取消订阅(-=)!
    }
    private void OnDataChanged(object sender, EventArgs e) { /* ... */ }
}

// EventBus 是长生命周期的单例(全局存在):
public class EventBus { public event EventHandler DataChanged; /* ... */ }

// 现象: 频繁 new Handler(bus) 又"用完丢弃", 但内存只涨不降, 跑久了OOM;
//   内存dump看: 大量Handler对象还活着, 被 EventBus.DataChanged 的委托链引用着。

// 为什么? 事件订阅建立了"事件源 → 订阅者"的引用:
// 1. _bus.DataChanged += OnDataChanged: 把"this.OnDataChanged"这个委托加进了bus的事件委托列表;
//    而这个委托【内部持有对 this(Handler) 的引用】(实例方法的委托=方法+目标对象this);
// 2. → 长命的 bus, 现在通过它的事件委托列表, 【引用着】这个 Handler;
// 3. 即使我这边不再用这个Handler、释放了所有其他引用:
//    → bus还引用着它 → 从GC Roots(单例bus)可达 → GC认为它"还在用", 【不能回收】;
// 4. → Handler永远活着; 每new一个就泄漏一个 → 内存只涨不降 → OOM。

// 关键: 事件 += 让"事件源"持有对"订阅者"的引用(委托引用了订阅者实例);
//   若订阅者比事件源短命、又不取消订阅(-=), 事件源会一直引用它、阻止GC回收 → 内存泄漏。
//   (反过来: 若事件源比订阅者短命/同命, 一般无碍——它俩一起被回收。问题在"长命源引用短命订阅者"。)

第一次想明白"是那个永生的单例,通过事件委托把我的 Handler 都攥住了不放"时,我又懊恼又恍然:"我以为 Handler 用完、没人引用它了,GC 自然会回收;完全没想到我订阅事件的那一下,就把它''到了一个永远不死的单例上,它再也走不了了。"这个坑最隐蔽的地方在于:不报任何错、功能完全正常,只是内存缓慢地、悄悄地涨;短时间、小流量下根本看不出来;只有长时间运行、大量创建订阅者后,泄漏累积到一定程度才 OOM;而且 += 那行代码看起来人畜无害,根本不会让人联想到"它建立了一个会阻止 GC 的引用"下面就来拆解,事件订阅与对象生命周期的关系、该怎么正确管理。

第一件事:搞懂事件订阅为什么会导致泄漏

我顺着这次事故,把 C# 事件、委托引用和 GC 的关系彻底理清了。

C# 事件订阅为什么会导致内存泄漏?

【核心: 事件+=让事件源持有对订阅者的引用(委托含目标实例); 长命源引用短命订阅者、不-=退订, 订阅者就被source钉住无法GC → 泄漏】

1. GC 回收的规则: 可达性
   - .NET GC 回收"从GC Roots(全局/静态/单例/栈上变量等)不可达"的对象;
   - 只要一个对象还能被某条引用链从Root触达, 它就被认为"还在用", 不能回收。

2. 事件订阅建立了什么引用?
   - obj.Event += handler.Method: 把一个委托加进 obj 的事件的委托调用列表;
   - 实例方法的委托 = (方法 + 目标对象target); 即这个委托【持有对 handler 实例的引用】;
   - → 结果: 事件源 obj 通过它的事件, 【引用着】订阅者 handler。
   - (注意方向: 是"源→订阅者", 不是"订阅者→源"; 这是反直觉的关键点。)

3. 泄漏怎么发生(长命源 + 短命订阅者 + 没退订):
   - 事件源是长命的(单例/全局/长期存在), 它本身从GC Root可达、永远不被回收;
   - 订阅者是短命的(频繁创建销毁), 本该用完就回收;
   - 但订阅者 += 订阅了长命源的事件 → 长命源引用着它;
   - 订阅者没在销毁前 -= 退订 → 长命源一直引用它 → 它从Root(经长命源)可达 → 不能回收;
   - → 每创建一个订阅者就泄漏一个, 越积越多。

4. 关键判断: 什么时候有泄漏风险?
   - 风险: 订阅者生命周期 < 事件源生命周期(短命订阅长命), 且不退订;
   - 无碍: 订阅者和事件源生命周期相同/订阅者更长(它俩一起回收, 或源先没了);
   - → 重点警惕"短命对象订阅长命对象(单例/静态/全局)的事件"。

5. 解决方向:
   - ① 用完取消订阅(-=): 在订阅者销毁/不再需要时, 把订阅取消掉, 断开引用;
   - ② 实现IDisposable, 在Dispose里-=退订, 用using/确定的时机释放;
   - ③ 用弱事件模式(WeakEventManager/弱引用), 让源对订阅者是弱引用, 不阻止GC;
   - ④ 重新审视设计: 是否真需要短命对象订阅长命源(可换成订阅者主动拉取等)。

一句话: 事件+=让事件源持有对订阅者的引用(委托含目标实例); 短命订阅者订阅长命源又不-=退订, 就会被源
   一直引用、无法被GC回收而泄漏; 解法是用完-=退订(或IDisposable里退订/弱事件), 重点防"短订长"。

这套认知,是整个坑的根。GC 回收的规则:可达性——GC 回收从 GC Roots 不可达的对象,只要还能被某条引用链从 Root 触达就不能回收事件订阅建立了什么引用:obj.Event += handler.Method 把委托加进 obj 的事件列表,实例方法的委托=方法+目标对象,委托持有对 handler 的引用,于是事件源 obj 引用着订阅者 handler(方向是源→订阅者,反直觉)泄漏怎么发生:长命源永远可达不被回收,短命订阅者 += 订阅了它、又没 -= 退订,源一直引用订阅者→订阅者从 Root 可达→不能回收,每创建一个就泄漏一个关键判断:风险在"短命订阅者订阅长命源(单例/静态/全局)且不退订";订阅者和源同命/更长则无碍解决方向:①用完 -= 退订②实现 IDisposable 在 Dispose 里退订③用弱事件模式(WeakEventManager)让源弱引用订阅者④重新审视设计一句话:事件 += 让事件源持有对订阅者的引用(委托含目标实例);短命订阅者订阅长命源又不 -= 退订,就会被源一直引用、无法被 GC 回收而泄漏;解法是用完 -= 退订(或 IDisposable 里退订/弱事件),重点防"短订长"。

第二件事:正解——用完取消订阅(-=)、IDisposable 退订、或弱事件

搞懂了原理,正解就清晰了:订阅者在不再需要时取消订阅(-=);实现 IDisposable、在 Dispose 里退订并用 using/确定时机释放;或用弱事件模式让事件源对订阅者是弱引用

// ====== 正解一: 实现 IDisposable, 在 Dispose 里取消订阅 ======
public class Handler : IDisposable
{
    private readonly EventBus _bus;
    public Handler(EventBus bus)
    {
        _bus = bus;
        _bus.DataChanged += OnDataChanged;   // 订阅
    }
    private void OnDataChanged(object sender, EventArgs e) { /* ... */ }

    public void Dispose()
    {
        _bus.DataChanged -= OnDataChanged;   // ★ 取消订阅! 断开事件源对自己的引用
    }
}
// 使用: 用完确定地释放(using 或显式 Dispose), 让订阅被取消、Handler可被GC
using (var handler = new Handler(bus)) { /* ... */ }   // 离开using自动Dispose→退订→可回收

// ====== 正解二: 订阅和退订要"成对" ======
//   有 += 的地方, 就要想清楚"在哪里 -="; 像 申请/释放 一样成对管理。
// ====== 正解三: 弱事件模式(订阅者生命周期不好控制时) ======
// 让事件源对订阅者持"弱引用", 不阻止GC回收订阅者:
// - WPF/某些框架: WeakEventManager;
// - 或自己用 WeakReference 包装订阅者;
// 弱引用: 订阅者没有其他强引用时, GC可以回收它(事件源的弱引用不算数);
//   → 即使忘了-=, 订阅者也能被回收(事件源那边的失效委托可惰性清理)。
// (弱事件实现较复杂, 一般优先用IDisposable+确定退订; 生命周期实在难控时才上弱事件。)

// ====== 经验法则 ======
// 1. += 和 -= 成对: 凡是订阅了事件, 就要在合适的时机(销毁/不再需要)取消订阅;
// 2. 短订长要警惕: 短命对象订阅长命对象(单例/静态/全局)的事件, 是泄漏高发区, 务必退订;
// 3. 用IDisposable管理: 把退订放进Dispose, 配合using/DI容器的生命周期管理确定地释放;
// 4. 用具名方法订阅(而非匿名lambda): 匿名lambda没法-=(拿不到同一个委托引用), 难退订;
//    若用lambda, 要把委托存下来才能-=;
// 5. 生命周期实在难控: 用弱事件; 或重新设计(改主动拉取/中介者等), 避免强耦合的长期订阅。

// 核心: 订阅事件(+=)就要在订阅者销毁时取消订阅(-=), 成对管理; 用IDisposable把退订放进Dispose;
//   重点防"短命对象订阅长命源"; 生命周期难控时用弱事件; 别让一个+=把对象钉死在长命源上。

修复的核心,是"+= 和 -= 成对,用完退订,断开事件源对自己的引用"正解一:实现 IDisposable、在 Dispose 里取消订阅——_bus.DataChanged -= OnDataChanged 断开引用,配合 using/确定时机释放,Handler 就能被 GC正解二:订阅和退订要成对(有 += 就想清在哪 -=,像申请/释放一样管理)。正解三:弱事件模式(让事件源对订阅者弱引用、不阻止 GC,生命周期难控时用,但较复杂)。经验法则:+= 和 -= 成对、短订长要警惕、用 IDisposable 管理退订、用具名方法订阅(匿名 lambda 难 -=)、生命周期难控时用弱事件或重新设计归根结底:订阅事件(+=)就要在订阅者销毁时取消订阅(-=),成对管理;用 IDisposable 把退订放进 Dispose;重点防"短命对象订阅长命源";生命周期难控时用弱事件;别让一个 += 把对象钉死在长命源上。

第三件事:C# / 托管语言中其他常见的内存泄漏

排查后我把 C#(以及有 GC 的托管语言)中其他容易被忽略的内存泄漏也系统梳理了一遍。

托管语言中其他常见的"内存泄漏"(对象该回收却没回收)

# 1. 事件订阅没退订(本文): 长命源引用短命订阅者。→ -=退订/IDisposable/弱事件。

# 2. 静态/单例集合只加不删: 往静态List/Dictionary里塞对象不清理, 永远可达。→ 及时移除/限大小。

# 3. 缓存无上限无过期: 缓存只进不出, 撑爆内存。→ 设容量上限(LRU)+TTL。

# 4. 闭包/lambda捕获了大对象: 委托/事件里的lambda捕获外部大对象, 跟着委托一起活。→ 留意捕获。

# 5. 非托管资源没释放: 文件/连接/句柄(IDisposable)没Dispose。→ using/Dispose。

# 6. 定时器/后台任务持有引用: Timer回调引用对象, Timer不停对象不回收。→ 停掉Timer/取消任务。

# 7. ThreadLocal/AsyncLocal没清理: 线程池线程长命, 挂的数据不清理。→ 用完清理。

# 8. 静态事件(static event): 订阅了静态事件几乎等于永久持有。→ 尤其要退订。

# 共同根源: 有GC不代表"不会内存泄漏"; GC只回收"不可达"的对象, 而"泄漏"恰恰是
#   "对象逻辑上不再需要、却仍被某条引用链(常是长命的全局/静态/事件/缓存)意外地持有着、保持可达";
#   GC帮你回收垃圾, 但帮不了你"切断那些不该存在的引用"。

# 核心: 托管语言的内存泄漏 = "对象逻辑上该死、却被长命引用链钉住而可达"; 排查泄漏就是找"谁还在引用它";
#   重点查 事件/静态集合/缓存/定时器/闭包 等"长命的持有者"; 及时切断不再需要的引用。

排查让我把托管语言的其他内存泄漏也梳理清了。一、事件订阅没退订(本文)。二、静态/单例集合只加不删三、缓存无上限无过期四、闭包捕获大对象五、非托管资源没释放六、定时器/后台任务持有引用七、ThreadLocal/AsyncLocal 没清理八、静态事件它们的共同根源是:有 GC 不代表不会内存泄漏;GC 只回收"不可达"的对象,而泄漏恰恰是"对象逻辑上不再需要、却仍被某条引用链(常是长命的全局/静态/事件/缓存)意外地持有着、保持可达";GC 帮你回收垃圾,但帮不了你切断那些不该存在的引用核心是:托管语言的内存泄漏 = "对象逻辑上该死、却被长命引用链钉住而可达";排查泄漏就是找"谁还在引用它";重点查事件/静态集合/缓存/定时器/闭包等"长命的持有者";及时切断不再需要的引用下面这张图,是这次事件泄漏坑的成因与解法:

第四件事:事件订阅的引用方向对比表

这次踩坑后,我把"谁引用谁、谁能被回收"这件最容易搞反的事整理成一张表。

场景 引用方向 谁被钉住 泄漏?
短命订阅者 += 长命源(本文) 长命源 → 短命订阅者 订阅者被源钉住 ✗ 泄漏
长命订阅者 += 短命源 短命源 → 长命订阅者 源被订阅者钉住(但源本就短命) 一般无碍
同生命周期互订 互相 一起回收 一般无碍
+= 后 -= 退订 退订后断开 不泄漏

这张表把引用方向钉清了。核心是:这个坑最反直觉的地方,是"引用的方向"——直觉上我们觉得"是我(订阅者)去订阅了它(源),所以是我引用它";可实际上,事件订阅让""反过来引用了"订阅者"(因为委托里装着订阅者);所以"谁能被回收",取决于"谁被谁引用、那个引用方是不是长命的"——而这个方向,和直觉是相反的它给我的最大启发是:分析"一个对象能不能被回收/会不会泄漏"时,关键不是"它引用了谁",而是"谁引用了它、那个引用者是不是'长命/从根可达'的"——对象的"",握在"引用它的人"手里,而不是"它引用的人"手里;搞反这个方向(以为"我不用它了它就该死"),就会忽略"还有别人(长命的)攥着它"这给了我一种分析对象生命周期的清醒:判断一个对象会不会泄漏,要养成"反向追问"的习惯——不是问"它引用了什么",而是问"还有谁(尤其是长命的全局/静态/事件/缓存)在引用着它?这些引用在它'该死'时会被切断吗?";顺着"被引用"的方向、从 GC Root 往下查,才能找到那条钉住它的链;"从'谁引用它'而非'它引用谁'的方向分析对象生命周期",是排查内存泄漏的正确视角认清对象的命握在引用它的人手里、从被引用方向分析生命周期——是这个坑带给我的认知。

第五件事:这次事故暴露的"建立关系容易、解除关系易忘"

这次让我反思更深一层:订阅(+=)只写了、退订(-=)却忘了——这是一类"只建立、不解除"的通病。我把"成对的建立/解除操作"整理成表。

建立(容易记得做) 解除(容易忘) 忘了解除的后果
事件 += 事件 -= 内存泄漏(本文)
打开文件/连接 关闭 资源泄漏
加锁 解锁 死锁
注册回调/监听 注销 泄漏/重复触发
往集合里加 从集合里移除 集合膨胀

这张表道出了一个普遍的人性弱点。核心是:我忘了 -=,本质是一种"重建立、轻解除"的通病——"建立一个关系/获取一个资源"是为了达成当下目标,我们动机明确、不会忘;而"解除关系/释放资源"是收尾,它不直接服务于当下目标、且发生在'未来某个时刻',所以极容易被忽略;可"有建立无解除",积累起来就是泄漏、死锁、膨胀它给我的深刻启发是:编程(乃至做事)里有大量"成对的、有始有终的"操作(获取/释放、订阅/退订、加锁/解锁、打开/关闭、进入/退出);而错误往往不在"开始"那一半(它服务于目标、不会忘),而在"结束"那一半(它是收尾、易被遗忘);"善始"容易, "善终"难——而很多问题正出在"没有善终"上这给了我一种处理"成对操作"的纪律:每当我做一个"建立/获取/进入"型的操作时,要当场就想清楚、并安排好它对应的"解除/释放/退出"——而不是把收尾留给"以后再说"(往往就忘了);善用语言提供的"自动配对"机制(using/try-finally/RAII/defer)把"解除"和"建立"绑在一起,让善终成为自动的;"建立关系时就安排好解除、用配对机制保证善终",是避免一整类'有头无尾'式 bug 的根本纪律认清重建立轻解除的通病、建立时就安排好解除并用配对机制保证善终——是这个事件泄漏坑带给我的工程态度。

第六件事:订阅事件时,我现在的自检习惯

现在每当我要写一个事件订阅(+=),我都会先按这张图问自己:

这张图的精髓,是"短订长必须管退订,+= 就安排好 -=,用 IDisposable 或弱事件"短命订阅长命必须退订、能控生命周期IDisposable+Dispose 退订、难控弱事件、别用没法退订的匿名 lambda这套习惯,让我从"订阅了就不管"变成了"订阅时就想清在哪退订"——核心始终是:订阅事件(+=)就要在订阅者销毁时取消订阅(-=),重点防短命对象订阅长命源,用 IDisposable 管理退订或用弱事件,别让 += 把对象钉死在长命源上。

我立下的几条规矩

这场"订阅了事件没退订、对象全泄漏到 OOM"的事故,换来了我写 C# 时,刻进骨子里的几条铁律:

  1. 事件 += 让事件源持有对订阅者的引用(委托含目标实例)。方向是源→订阅者,反直觉。
  2. 短命订阅者订阅长命源(单例/静态/全局)又不退订,会被源钉住无法 GC。
  3. 有 += 就要有 -=,成对管理,在订阅者销毁时取消订阅。
  4. 实现 IDisposable,把退订放进 Dispose,配合 using/DI 生命周期确定地释放。
  5. 生命周期难控时用弱事件模式(源对订阅者弱引用)。
  6. 别用匿名 lambda 订阅(没法 -=),用具名方法或存下委托引用。
  7. GC 不防泄漏:泄漏是"对象该死却被长命引用链钉住而可达",排查就找谁还引用它。

写在最后

回头看,这场由"订阅了事件却忘了退订"引发的、对象集体泄漏的事故,真正教给我的,远不止"用完 -= 退订"这一个技巧。它让我对"我们建立一段'连接/关系'时, 往往以为它只是'我去连了它'的单向动作; 却没意识到, 这段连接同时也让对方攥住了我——而这份'被攥住', 会在我'想离开'时, 把我拖住",有了一次刻骨的体会。我栽跟头,是因为我把"订阅事件"理解成了一个单向的、轻飘飘的动作——"我(Handler)去关注一下它(bus)的动静而已"。我以为这只是我单方面"搭上"了它,我想走的时候,松手就行;可我没意识到:这一"",同时也让它(那个永生的单例)反过来'抓住'了我——它的事件列表里,存下了一根指向我的线;于是当我"想走"(被销毁回收)时,那根我没解开的线,把我死死拽在了它身上,让我走不掉;我以为的"单向关注",其实是一根"双向的绳"这让我领悟到一个关于"连接即双向羁绊"的深刻认知:任何"建立连接/依赖/关系"的行为,几乎都不是单向的——它在让"你接触到对方"的同时,也让"对方以某种方式持有了你";"建立一段关系" 往往意味着 "双方都被这段关系约束/牵连";而这份"被牵连", 在你想"独立/离开/被释放"时, 就会显现为一种"解不开就走不掉"的羁绊这给了我一种处理"连接与依赖"的清醒:建立任何"连接、依赖、订阅、注册"时,要清醒地意识到"这不仅是我连了它, 也是它持有了我; 这段关系是双向的牵连",并从一开始就想清楚"这段关系将来要怎么干净地解除"——而不是只享受连接、不考虑如何全身而退;"视连接为双向羁绊、建立时就规划好如何解除",是管理好系统中错综复杂的依赖关系、避免被它们拖死的关键意识认清连接是双向的羁绊、建立时就规划好如何干净解除——这,是我用一次事件泄漏的事故,换来的、关于 C#、也关于如何看待和管理一切连接依赖的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次写下 += 订阅事件时,手指顿一下、立刻想到"-= 写在哪",那我对着那一路涨到 OOM 的内存曲线排查的这段时间,就值了。

—— 别看了 · 2026
声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理 邮箱1846861578@qq.com。
技术教程

我用 Redis 加了分布式锁保护临界区,以为万无一失,结果偶发两个进程同时进了临界区、一个进程还把别人的锁给删了:一次分布式锁实现陷阱的深度复盘

2026-6-2 21:45:07

技术教程

我用数字枚举定义订单状态,以为类型安全了,结果一个不存在的数字 99 也能传进去、遍历枚举时还莫名多出一倍的项:一次 TypeScript 数字枚举两个坑的深度复盘

2026-6-2 21:56:13

0 条回复 A文章作者 M管理员
    暂无讨论,说说你的看法吧
个人中心
购物车
优惠劵
今日签到
有新私信 私信列表
搜索