监听器作为弱引用的优缺点

76

将监听器保留为弱引用的优缺点是什么?

显然最大的“优点”是:

将监听器作为弱引用添加,意味着监听器不需要费心“移除”自己。

对于那些担心监听器是对象唯一引用的人,为什么不能有两种方法:addListener()和addWeakRefListener()?

那些不关心移除的人可以使用后者。


2
@user:http://weblogs.java.net/blog/enicholas/archive/2006/05/understanding_w.html - Robert Harvey
1
除了混乱API之外,两种方法会给你带来什么好处?虽然如果可能的话,删除添加的所有侦听器仍然是良好的风格,但通常不这样做也不会有害:垃圾回收会处理除非在极少数情况下。如果您遇到由未释放的侦听器产生的内存泄漏,请仔细分析情况,并执行类似于AbstractButton对其Action的propertyChangeListener所做的操作。 - kleopatra
1
如果您有一个弱引用的监听器,您就不需要“仔细分析情况” ;) - pdeva
我确实使用弱监听器,但不作为接口的一部分,只在内部使用,这强制保持对侦听器的外部引用和注销(匿名类可能没有该引用)。否则,只会保留一个泄漏的引用。重要的是要记住,任何addXXXListener都必须跟随removeXXXListener以确保正常的生命周期。注销不是一个选项(除非两个对象具有相同的生命周期范围)。该模式既适用于Swing等,也适用于服务器开发。 - bestsss
@pdeva,如果您有一个弱引用的监听器,您就不需要“仔细分析情况”;)这是非常错误的,非常错误的,如果您有一个弱监听器,注册事件的那个将什么都收不到,因为没有额外的引用剩下,监听器将被GC回收。 - bestsss
3
这里有很多人的答案都想不出为什么弱引用会有任何意义,所以我想至少提供一个参考来解决它解决的典型问题:失效的监听器问题 - Don Hatch
14个回答

77
首先,在监听器列表中使用WeakReference会给您的对象带来不同的语义,与使用硬引用不同。在硬引用情况下,addListener(...)意味着“通知提供的对象特定事件,直到我明确停止为止,使用removeListener(..)”,而在弱引用情况下,则表示“通知提供的对象有关特定事件,直到此对象不再被任何其他人使用为止”(或使用removeListener明确停止)。请注意,在许多情况下,拥有某些事件的监听器对象,并没有其他引用将其保留免受GC。记录器可以是一个例子。
如您所见,使用WeakReference不仅解决了一个问题(“我应该记住在某个地方删除添加的侦听器”),而且还引发了另一个问题——“我应该记住我的侦听器可能随时停止侦听,当它没有引用时”。您并没有解决问题,只是用另一个问题交换了一个问题。看,无论如何,您都必须清楚地定义,设计和跟踪您侦听器的生命周期 - 或者这样或那样。
因此,就我个人而言,我同意提到在监听器列表中使用WeakReference更像是一种技巧而不是解决方案。这是一个值得了解的模式,有时可以帮助您使旧代码正常工作。但它不是首选模式:)
PS:还应注意WeakReference引入了额外的间接级别,在某些极高事件速率的情况下,可以降低性能。

3
使用WeakReference存在的另一个问题是,“我应该记住,由我的Listener引起的任何状态更改可能仍会在其超出范围后发生”。因此,使用WeakReference的Listener需要设计成知道其存在是不确定的。 - user545680
是的,你肯定是对的。但从我的经验来看,在许多情况下它并不是一个问题,这就是为什么会出现这个问题的原因 :) 而且,据我所记得,甚至在jdk代码中也可以看到使用WeakReference作为监听器的示例。 - BegemoT
你有关于WeakReference在jdk中作为监听器使用的任何参考资料吗?我很想看看它的使用情况。 - user545680
@Bringer128 嗯,看来我错了。我在swing周围看到了许多基于WR的监听器的用法,但是在查找源代码时,我找不到任何内部示例。JDK使用更有趣的技术--监听器列表本身使用普通引用,但具体的监听器作为适配器,仅持有对实际接收事件的对象的弱引用--并且它会将自己注销为目标GC-ed。有趣 :) - BegemoT
1
不能确定JDK是否使用,但Android SDK使用了一个——SharedPreference就是其中之一。它使用WaekHashMap来存储已注册的监听器。 http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/4.0.2_r1/android/app/SharedPreferencesImpl.java#167也许像addWeakListener(listener)这样利用weakrefs命名方法可能是一个好习惯,因为这个类的用户会知道内部发生了什么,以避免错过通知。 - vir us
不使用弱引用回调会导致每个对象都必须有“析构函数”的世界。这将击败拥有GC的整个目的。 - Dmitry Ryadnenko

34

这不是一个完整的答案,但你提到的优点也可能是其主要缺点。如果弱化实现操作监听器,请考虑会发生什么:

button.addActionListener(new ActionListener() {
    // blah
});

那个动作监听器随时都可能被垃圾回收!这并不罕见,只有事件与其相关联的匿名类才是对它的唯一引用。


1
为什么不能有两种方法,addListener()和addWeakRefListener()?那些不关心移除的人可以使用后者。 - pdeva
@pdeva,是的,那样做可以。但是这有点危险,因为人们必须记住要调用哪个方法。我认为这种模式不一定是你应该总是避免的,但它最好有一个很好的理由——而且在我看来,这个理由不能仅仅是“防止健忘程序员犯错”。 - Kirk Woll
2
“防止健忘程序员犯错。”这不是垃圾回收机制被发明的原因吗? :) 另外,它可以使您的代码更简单,因为您不必记得在处理对象时删除侦听器。 - pdeva
6
@pdeva,我并不是说维护这个原则有什么问题。但是我认为这种方法至少和症状一样糟糕——如果他们忘记正确使用这种方法(使用错误的方式添加侦听器),他们会比正常解决方案更糟糕。 - Kirk Woll

12

我看到过大量的代码没有正确注销监听器。这意味着它们仍然被不必要地调用以执行不必要的任务。

如果只有一个类依赖于监听器,那么清理就很容易,但是当25个类都依赖于它时会发生什么?正确注销它们变得更加棘手。事实上,您的代码可能从引用一个监听器对象开始,最终在未来版本中引用了同一个监听器的25个对象。

不使用 WeakReference 相当于冒着消耗不必要的内存和 CPU 的风险。在复杂的代码中,它更加复杂、棘手并且需要更多的工作来处理硬引用。

WeakReferences 充满优点,因为它们会自动清理。唯一的缺点是,在代码的其他地方必须记得保留一个硬引用。通常,这将在依赖此监听器的对象中进行。

我讨厌创建监听器的匿名类实例的代码(正如 Kirk Woll 所提到的),因为一旦注册,你就无法注销这些监听器了。你没有对它们的引用。我认为这是糟糕的编程。

当你不再需要监听器时,你也可以将对监听器的引用置为 null。这样你就不必再担心它了。


您可以获取给定组件的侦听器数组。匿名类可以扩展专业化。给该专门的侦听器类一个uid属性,这样您就可以选择性地删除匿名侦听器了。 - alphazero
1
只有当匿名侦听器永远不需要注销时,才应该注册它。如果您将其保存到变量/字段/集合以供稍后注销,那么您会认为这是可以接受的吗? - user545680
1
虽然我同意匿名监听器是双刃剑,但对于使用弱引用也是如此:将引用设置为空以删除侦听器,在某种程度上是反语义的。您需要在该行中添加注释,以表明这意味着侦听器将来任何时间都将被注销。在我看来,程序员负责注册侦听器,因此他也负责移除它们。 - Manuel Leuenberger
@maenu 我完全同意程序员有责任注册和注销监听器。这一点毋庸置疑。我想争论的是使用弱引用更简单、风险更小... - Jérôme Verstrynge
这里是依赖弱引用的另一个缺点——假设我有1000个监听器(视图)在一个不断演化的模拟(模型)上,而这些视图可以非常动态地出现和消失。假设某个用户操作(例如交互式窗口调整大小)会导致所有视图消失并重新创建,比如说100次。那么我就有了100,000个过期的监听器...如果观察者将它们保存为WRs,它们最终会被GC清理掉...但在此之前,它们都将保持监听状态并执行可能昂贵的工作,导致显著的(暂时性)减速。 - Don Hatch

6
没有实际优点。weakrefrence通常用于“可选”数据,例如缓存,您不希望阻止垃圾回收。您不希望您的监听器被垃圾回收,而是希望它继续监听。
更新:
好的,我想我可能已经明白了你的意思。如果您将短期侦听器添加到长期对象中,则使用weakReference可能会有益处。例如,如果您将PropertyChangeListeners添加到域对象以更新不断重建的GUI的状态,则域对象将保留GUI,这可能会增加负担。想象一个不断被重新创建的大型弹出对话框,通过PropertyChangeListener通过监听器引用回到Employee对象。请纠正我,但我认为整个PropertyChangeListener模式已经不太流行了。
另一方面,如果您谈论GUI元素之间的监听器或具有域对象监听GUI元素,则不会获得任何好处,因为当GUI消失时,监听器也将消失。
下面是几篇有趣的阅读材料:

http://www.javalobby.org/java/forums/t19468.html

如何解决Swing监听器的内存泄漏问题?


2
对于像缓存这样的“可选”数据,适当的引用类型是SoftReference,而不是WeakReference。WeakReference通常用于防止引用泄漏,明确地获得比SoftReferences更高的终结优先级,以便它们在GC扫描期间不会干扰缓存项的存储。 - user515655

4
说实话,我并不真的相信这个想法,也不知道你期望通过addWeakListener做什么。也许只有我觉得这是一个错误的好主意。一开始它很诱人,但它可能带来的问题不可忽视。
使用弱引用,当监听器本身不再被引用时,你不能确定监听器是否不再被调用。垃圾收集器可能会在几毫秒后或永远释放内存。这意味着它可能会继续消耗CPU,并出现奇怪的行为,例如抛出异常,因为不应该调用监听器。
以Swing为例,如果要尝试执行只能在UI组件实际附加到活动窗口时才能执行的操作,这可能会导致异常,并影响通知程序使其崩溃,并阻止通知有效的监听器。
第二个问题如前所述是匿名监听器,它们可能会过早释放,从未被通知或仅被通知几次。
你试图实现的目标是危险的,因为你不能再控制何时停止接收通知。它们可能会持续很长时间或过早停止。

你试图实现的内容是危险的,因为当你停止接收通知时,你无法再控制它。WeakReferences允许自动删除特别编写的监听器,但仍然只有在GC感觉合适的时候才会被删除,这可能永远不会发生,并且经常如此。 - user515655
我希望人们能够省略这个论点中的“或者停止得太早”部分——我认为那部分是一个干扰因素,因为那只是一个需要小心避免的编程错误,而不是一个根本性的问题。当你提到它时,人们在浏览你的答案时可能会倾向于忽略你整个论点。你正确指出了“可能持续无限长时间”的部分才是真正的问题。 - Don Hatch
停止过早出现在不同的情况下都是问题。如果它只是一个监听器,你希望它在应用程序的生命周期或创建事件的组件的相同生命周期内保持活动状态,但是如果你的监听器在创建后立即被垃圾回收,这也是一个大问题。 - Nicolas Bousquet

4
因为您正在添加WeakReference监听器,我假设您正在使用自定义的Observable对象。
在以下情况下使用一个弱引用指向一个对象是非常合理的: - 在Observable对象中有一系列的监听器。 - 您已经在别处持有了对这些监听器的强引用。(您必须确信这点) - 您不希望垃圾收集器停止清理监听器,只因为它们在Observable中有引用。 - 在垃圾回收期间,这些监听器将被清除。在通知监听器的方法中,您应该从通知列表中清除WeakReference对象。

3

在我看来,在大多数情况下这是一个好主意。释放监听器的代码与注册它的代码位于同一位置。

实际上,我看到很多软件会永久保留监听器。通常程序员甚至不知道他们应该注销它们。

通常可以返回一个包含对监听器引用的自定义对象,以便可以控制注销的时间。例如:

listeners.on("change", new Runnable() {
  public void run() {
    System.out.println("hello!");
  }
}).keepFor(someInstance).keepFor(otherInstance);

这段代码将注册监听器并返回一个封装了监听器的对象,该对象具有keepFor方法,该方法将侦听器添加到具有实例参数作为键的静态weakHashMap中。这将确保在someInstance和otherInstance未被垃圾回收时至少注册了侦听器。
还可以有其他方法,如keepForever()、keepUntilCalled(5)、keepUntil(DateTime.now().plusSeconds(5))或unregisterNow()。
默认情况下可以永久保留(直到取消注册)。
这也可以使用不触发侦听器移除的虚引用来实现。
编辑:创建了一个小型库,实现了此方法的基本版本:https://github.com/creichlin/struwwel

如果在调用on()之后但在keepFor()之前发生垃圾回收怎么办?假设keepFor()调用了weakReference.get(),此时可能已经返回null了? - glebm
@glebm,是的,你说得对,那是一个问题。虽然很不可能发生,但这使问题变得更糟。 默认情况下永久保留它,直到调用另一种方法来解决这个问题。或者只需将返回对象对监听器的引用设置为非弱引用。这样,在返回对象被使用时,它就不会被垃圾回收。 - Christian

2
我想不出任何合法的使用WeakReferences作为监听器的用例,除非您的用例涉及到在下一次GC周期之后明确不应存在的监听器(当然,这种用例将是特定于VM/平台的)。
可以想象一种稍微更合法的用例,即监听器是可选的,但占用大量堆空间,并且在空闲堆大小开始变得棘手时应该是第一个被去除的。我想,某种类型的可选缓存或其他类型的辅助监听器可能是候选的。即使如此,似乎也应该让监听器的内部使用SoftReferences,而不是监听器和被监听者之间的链接。
通常,如果您正在使用持久性监听器模式,则监听器是不可选的,因此提出这个问题可能是您需要重新考虑架构的症状。
这是一个学术问题还是您有一个需要解决的实际情况?如果这是一个实际情况,我很想听听它是什么 - 您可能可以获得更多、更不抽象的建议来解决它。

如何看待:当发生某些事情时,对象会引发事件。您希望拥有一个具有字段或方法的对象,该字段或方法报告已发生多少次。如果事件外部没有对该对象的引用来增加该字段,则该对象应停止存在。如果构建并放弃许多监视长期对象的对象,则除非事件被弱订阅,否则对象将继续无限制地累积。 - supercat

2
我对原帖作者有三个建议。很抱歉挖掘这个旧帖子,但我认为我的解决方案之前没有在这个帖子中讨论过。
第一, 考虑模仿JavaFX库中的javafx.beans.values.WeakChangeListener示例。
第二, 我通过修改我的Observable的addListener方法超越了JavaFX模式。新的addListener()方法现在为我创建了相应的WeakXxxListener类的实例。
"fire event"方法很容易被修改以取消对XxxWeakListeners的引用,并在WeakReference.get()返回null时将它们移除。
删除方法现在有点棘手,因为需要迭代整个列表,这意味着需要同步。
第三, 在实施这个策略之前,我采用了一种不同的方法,您可能会发现有用。当(硬引用)监听器获得一个新事件时,它们会进行现实检查,判断它们是否仍在使用中。如果没有,则它们会取消对观察者的订阅,从而允许它们被GCed。对于订阅长期存在的Observables的短期存在的Listeners来说,检测过时性非常容易。
为了尊重那些规定“好的编程习惯是每次取消订阅您的侦听器”的人们,在侦听器自己取消订阅时,我确保创建了一个日志条目,并在稍后的代码中纠正了这个问题。

这是一个三年前的问题,而你的“答案”似乎并不是一个答案。 - Jan Chrbolka
2
我明确为恢复一个三年前的帖子而道歉。但当我阅读这个帖子时,它对我来说是新的,我相信如果他们在谷歌上搜索“弱监听器”,其他人也会第一次发现它。我认为这些人将从我的贡献中受益,特别是我引用了一个现代Java API中使用弱监听器的具体示例(已经被提出)。我还通过建议使用“自我删除需要记录日志条目”的策略来减轻使用弱监听器的“风险”。 - Teto

1
WeakListeners在编程中非常有用,特别是当你想让GC控制监听器的生命周期时。正如之前所述,与通常的addListener/removeListener情况相比,这确实是不同的语义,但在某些情况下是有效的。例如,考虑一个非常大的树,它是稀疏的 - 一些节点级别没有明确定义,但可以从更高层次的父节点推断出来。隐含定义的节点侦听那些已定义的父节点,以便保持其隐含/继承值最新。但是,这个树很大 - 我们不希望隐含节点永远存在 - 只要它们被调用代码使用,再加上几秒钟的LRU缓存,以避免反复使用相同的值。在这种情况下,弱监听器使得子节点能够监听父节点,同时又能通过可达性/缓存来决定它们的生命周期,从而使结构不会将所有隐含节点保留在内存中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接