Android中的单例模式 vs. 应用程序上下文?

377
回想起这篇文章列举了使用单例模式的几个问题,再看到一些使用单例模式的Android应用程序示例,我想知道是否使用单例模式代替共享全局应用状态的单个实例是一个好主意(通过子类化android.os.Application并通过context.getApplication()获取该实例)。
这两种机制各有什么优劣?
说实话,我期望在这篇针对Web应用程序的Singleton模式,不是一个好主意!但应用于Android上会得到相同的答案。 我对吗?除此之外DalvikVM有什么不同?
编辑:我想听取涉及的几个方面的意见:
  • 同步
  • 可重用性
  • 测试
10个回答

298

我非常不同意Dianne Hackborn的回复。我们正在逐步从我们的项目中删除所有单例,转而使用轻量级、任务作用域对象,当您实际需要它们时可以轻松重新创建。

单例对于测试来说是一场噩梦,如果懒惰初始化,会引入“状态不确定性”,产生微妙的副作用(当将调用getInstance()从一个作用域移动到另一个作用域时可能会突然出现)。可见性被提及为另一个问题,由于单例意味着对共享状态进行全局(=随机)访问,因此在并发应用程序中未正确同步时可能会出现微妙的错误。

我认为这是一种反模式,它是一种糟糕的面向对象风格,本质上相当于维护全局状态。

回到你的问题:

虽然应用上下文本身可以被视为单例,但它是由框架管理的,并具有明确定义的生命周期、范围和访问路径。因此,我认为如果您确实需要管理应用程序全局状态,则应该使用此处而不是其他地方。对于其他任何事情,请重新考虑是否真正需要单例对象,或者是否也可以重写您的单例类以实例化执行手头任务的小型、短暂对象。


132
如果你在推荐应用程序,那么你正在推荐使用单例模式。说实话,这是无法避免的。Application 就是 一个单例,语义更糟糕。我不会涉及关于单例是否应该使用的宗教争论。我更喜欢实用主义 - 在维护进程级状态和简化问题方面,它们是一个不错的选择,并且你也可以在错误的情况下使用它们并自取灭亡。 - hackbod
18
没错,而且我也提到了“应用程序上下文本身可以被视为单例模式”。不同之处在于,使用应用程序实例时,由于其生命周期由框架处理,所以很难自己搞砸。像Guice、Hivemind或Spring这样的DI框架也使用单例,但这是开发人员不必关心的实现细节。我认为通常更安全的做法是依赖于正确实现的框架语义,而不是你自己的代码。是的,我承认! :-) - mxk
96
说实话,它不会比单例更有效地防止你自己“射击自己的脚”。有点混乱,但是Application没有生命周期。它在应用程序启动时创建(在其任何组件实例化之前),并在那个时候调用其onCreate()方法... 这就是所有的了。 它一直驻留并永远存在,直到进程被终止。就像一个单例一样。 :) - hackbod
30
有一件事可能会让人感到困惑,那就是 Android 的设计非常注重运行应用程序的进程管理和生命周期。因此,在 Android 上,单例是一种非常自然的方式,可以利用这种进程管理机制。如果您想在进程中缓存某些状态,直到平台需要将进程的内存回收用于其他用途,那么将该状态放入单例中就可以做到。 - hackbod
7
好的,完全可以。我只能说自从我们迈出了远离自管理单例的步伐以来,我就再也没有回头过。现在我们选择了一个轻量级的DI风格解决方案,其中我们保留了一个工厂单例(RootFactory),该单例又由应用实例进行管理(如果您愿意,它是一个委托)。这个单例管理所有应用程序组件所依赖的公共依赖项,但实例化是在一个单一的位置——应用程序类中进行管理的。虽然在这种方法中仍然有一个单例存在,但它被限制在Application类中,因此其他代码模块不知道这个“细节”。 - mxk
显示剩余21条评论

234

我非常推荐使用单例模式。如果你有一个需要上下文的单例,可以这样写:

MySingleton.getInstance(Context c) {
    //
    // ... needing to create ...
    sInstance = new MySingleton(c.getApplicationContext());
}
我更喜欢使用单例模式而不是Application类,因为它可以使应用程序更加有条理和模块化。相比于需要在整个应用程序中维护所有全局状态的一个地方,每个单独的部分都可以自己管理。此外,单例模式的延迟初始化(按需初始化)让你不必在Application.onCreate()中做所有的初始化工作。
使用单例模式本身并没有什么本质上的问题。只要在适当的时候使用它就好了。实际上,Android框架本身就有很多单例模式,用于维护每个进程的缓存加载资源和其他一些东西。
对于简单的应用程序,使用单例模式不会引起多线程的问题,因为标准回调都是在进程的主线程上进行的,除非显式地通过线程或隐式地发布内容提供程序或服务IBinder到其他进程中引入多线程。
只要你考虑周全,就可以愉快地使用单例模式 :)

2
不是针对外部事件 - BroadcastReceiver.onReceive() 也在主线程上调用。 - hackbod
2
好的。您能否指向一些阅读材料(最好是代码),让我可以看到主线程调度机制?我认为这将一次性澄清几个概念。提前致谢。 - Martín Schonaker
2
这是主要的应用程序调度代码:http://android.git.kernel.org/?p=platform/frameworks/base.git;a=blob;f=core/java/android/app/ActivityThread.java - hackbod
8
使用单例模式本身并没有任何问题。只要在合适的情况下使用它们即可。安卓框架实际上有很多这样的单例,用于维护进程级别的资源缓存等。正如你所说的。来自iOS世界的朋友说,在iOS中“一切都是单例”。在物理设备上,单例概念毫无疑问更为自然:GPS、时钟、陀螺仪等等——从概念上讲,你还能用其他方式来设计它们吗?所以没错。 - Fattie
1
我不知道是否有人注意到这一点,但是如果每次调用 getInstance() 都创建一个新实例,那么它并不是真正的单例,只是一个静态工具方法。话虽如此,我的理解可能存在缺陷,请纠正我。 - Sanket Berde
显示剩余8条评论

22

来自:开发者 > 参考 - Application

通常情况下,无需子类化 Application。在大多数情况下,静态单例可以以更模块化的方式提供相同的功能。如果您的单例需要全局上下文(例如注册广播接收器),则可以给检索它的函数传递一个 Context,该函数在首次构建单例时内部使用 Context.getApplicationContext()。


1
如果你为单例编写一个接口,将getInstance设置为非静态的,甚至可以通过非默认构造函数使单例使用类的默认构造函数注入生产单例。这也是你在单元测试中用于创建单例使用类的构造函数。 - android.weasel

11

我也遇到了同样的问题:在Android中应该使用单例还是制作一个子类android.os.Application?

一开始我尝试使用Singleton,但是我的应用程序在某个时刻需要调用浏览器。

Intent myIntent = new Intent(Intent.ACTION_VIEW, Uri.parse("http://www.google.com"));

问题是如果手机内存不足,大多数类(即使是单例)都会被清除以获得一些内存,因此当从浏览器返回我的应用程序时,它每次都会崩溃。

解决方法:将所需数据放在Application类的子类中。


1
我经常看到人们发帖说可能会出现这种情况。因此,我会像使用延迟加载的单例对象一样将对象简单地附加到应用程序中,以确保生命周期得到记录和知晓。但请确保不要将数百张图片保存到应用程序对象中,因为如果您的应用程序在后台运行并且所有活动都被销毁以释放内存以供其他进程使用,则它将无法从内存中清除。 - Janusz
单例模式延迟加载在应用程序重启后不是让对象被垃圾收集器扫描的正确方法。弱引用才是,对吧? - Martín Schonaker
15
真的吗?Dalvik会卸载类并且丢失程序状态?你确定不是因为它在进行垃圾回收,清理那些有限生命周期的与Activity相关的对象,这些对象本来就不应该放在单例中吗?你需要举出明确的例子来支持这样一个非同寻常的说法! - android.weasel
1
除非有我不知道的更改,否则Dalvik不会卸载类。从来没有。他们看到的行为是他们的进程在后台被杀死以腾出空间给浏览器。他们可能正在初始化变量在他们的“主”活动中,当从浏览器回来时,这个活动可能还没有在新的进程中创建。 - Groxx

11

Application和Singleton不同的原因在于:

  1. Application的方法(例如onCreate)在UI线程中调用;
  2. Singleton的方法可以在任何线程中调用;
  3. 在Application的“onCreate”方法中,您可以实例化Handler;
  4. 如果Singleton在非UI线程中执行,则无法实例化Handler;
  5. Application有管理应用程序活动生命周期的功能。它具有“registerActivityLifecycleCallbacks”的方法。但是单例没有这个能力。

1
注意:您可以在任何线程上实例化Handler。来自文档的说明:“当您创建一个新的Handler时,它会绑定到创建它的线程/消息队列的线程上”。 - Christ
1
@Christ 谢谢!刚刚我学到了“Looper机制”。如果在非UI线程上实例化处理程序而没有使用代码“Looper.prepare()”,系统将报告错误“java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()”。 - sunhang

5
同时考虑以下两点:
  • 将单例对象作为类内的静态实例。
  • 创建一个公共类(Context),为应用程序中所有单例对象返回单例实例,这样的好处是Context中的方法名称会更有意义,例如:context.getLoggedinUser() 而不是 User.getInstance()。
此外,我建议您扩展Context以包括不仅访问单例对象而且需要全局访问的一些功能,例如:context.logOffUser()、context.readSavedData()等。可能将Context重命名为Facade会更有意义。

4

来自权威消息…

在开发应用程序时,您可能需要在整个应用程序中全局共享数据、上下文或服务。例如,如果您的应用程序具有会话数据,如当前已登录的用户,则可能希望公开此信息。在Android中,解决此问题的模式是使android.app.Application实例拥有所有全局数据,然后将您的Application实例视为一个单例,并使用静态访问器访问各种数据和服务。

编写Android应用程序时,您保证只有一个android.app.Application类的实例,因此(并且这也是Google Android团队推荐的),可以将其视为单例。也就是说,您可以安全地向Application实现添加一个静态getInstance()方法。如下所示:

public class AndroidApplication extends Application {

    private static AndroidApplication sInstance;

    public static AndroidApplication getInstance(){
        return sInstance;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        sInstance = this;
    }
}

4

它们实际上是相同的。 我能看到一个区别。使用Application类,您可以在Application.onCreate()中初始化变量,并在Application.onTerminate()中销毁它们。而使用单例模式,您必须依赖VM初始化和销毁静态变量。


16
onTerminate文档称它只会被模拟器调用,在设备上该方法可能不会被调用。http://developer.android.com/reference/android/app/Application.html#onTerminate() - danb

3

我的建议:

我注意到当我的活动被销毁时,一些单例/静态字段会被重置。我在某些低端2.3设备上注意到了这一点。

我的情况非常简单:我只有一个私有字段“init_done”和一个静态方法“init”,我从activity.onCreate()中调用它。我注意到,在某些重新创建活动的情况下,方法init会重新执行。

虽然我无法证明我的说法,但这可能与单例/类何时首次创建/使用有关。当活动被销毁/回收时,似乎所有仅由此活动引用的类也会被回收。

我将单例实例移动到Application的子类中。我从应用程序实例访问它们。自那以后,我没有再注意到这个问题。

希望这可以帮助某些人。


2

我的活动调用了finish()方法(这并不会立即结束它,但最终会结束),然后调用Google街景视图。当我在Eclipse上进行调试时,当调用Street Viewer时,我的应用程序连接断开,我理解为整个应用程序被关闭,以释放内存(因为单个活动的结束不应该导致此行为)。尽管如此,我能够通过onSaveInstanceState()方法将状态保存在Bundle中,并在堆栈中下一个活动的onCreate()方法中恢复它。无论是使用静态单例还是子类化Application,我都面临着应用程序关闭和状态丢失的问题(除非我将其保存在Bundle中)。因此,从我的经验来看,它们在状态保留方面是相同的。我注意到在Android 4.1.2和4.2.2中会丢失连接,但在4.0.7或3.2.4中不会,这表明在某个时候内存恢复机制发生了变化。


我发现在Android 4.1.2和4.2.2中连接丢失了,但在4.0.7或3.2.4上没有出现这种情况。我的理解是,这表明内存恢复机制在某些时候已经发生了变化。 我认为你的设备可用内存和安装的应用程序不同,因此你的结论可能是不正确的。 - Christ
@Christ:是的,你一定是对的。如果内存恢复机制在不同版本之间发生了变化,那就很奇怪了。可能是不同的内存使用方式导致了不同的行为。 - Piovezan

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