Android中的单例与应用程序上下文?


362

回顾这篇文章,列举了使用单例的几个问题, 并看到了几个使用单例模式的Android应用程序的示例,我想知道使用单例而不是通过全局应用程序状态共享的单个实例(将android.os.Application子类化并获取它)是否是一个好主意。通过context.getApplication())。

两种机制都有哪些优点/缺点?

老实说,我希望在此后的Singleton模式与Web应用程序中得到相同的答案,这不是一个好主意!但适用于Android。我对么?否则DalvikVM有什么不同?

编辑:我想对涉及的几个方面有意见:

  • 同步化
  • 可重用性
  • 测试中

Answers:


295

我非常不同意Dianne Hackborn的回应。我们会逐步从项目中删除所有单例,以支持轻量级的任务范围对象,可以在您实际需要它们时轻松地重新创建它们。

单例是测试的噩梦,如果延迟初始化,则会引入“状态不确定性”,并带有细微的副作用(当将调用getInstance()从一个范围移到另一个范围时,可能会突然浮出水面)。可见性已被提及为另一个问题,并且由于单例意味着对共享状态的“全局”(=随机)访问,因此当在并发应用程序中未正确同步时,可能会出现细微的错误。

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

回到您的问题:

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


131
如果建议使用Application,则建议使用单例。老实说,没有办法解决。应用程序单例,具有更糟糕的语义。我不会涉及关于单例的宗教论点,而您不应使用。我更喜欢实用-在某些地方,它们是维护每个进程状态的不错选择,并且可以通过这样做简化事情,并且您也可以在错误的情况下使用它们,以防万一。
Hackbod 2011年

18
是的,我确实提到“应用程序上下文本身可以视为单例”。不同之处在于,对于应用程序实例,用脚掌射击自己要困难得多,因为其生命周期是由框架处理的。诸如Guice,Hivemind或Spring之类的DI框架也使用了单例,但这是开发人员无需关心的实现细节。我认为通常依靠正确实现的框架语义而不是您自己的代码更为安全。是的,我承认我愿意!:-)
Matthias

93
老实说,它不会像单身汉那样阻止您向脚射击。这有点令人困惑,但是没有应用程序的生命周期。它是在您的应用启动时创建的(在实例化其任何组件之前)并在那时调用其onCreate(),...仅此而已。它坐在那里并永远生活,直到被杀死。就像一个单身汉。:)
Hackbod 2011年

30
可能使人困惑的一件事-Android的设计是围绕流程中运行应用程序以及管理这些流程的生命周期而设计的。因此,在Android上单例是利用该流程管理的一种非常自然的方法-如果您要在流程中缓存某些内容,直到平台需要为其他内容回收该进程的内存,则将该状态放在一个单例中即可做到这一点。
hackbod 2011年

7
好吧,公平。我只能说自从我们摆脱自我管理的单身人士以来,我从未回过头。现在,我们选择一种轻量级的DI样式的解决方案,在该解决方案中,我们确实保留了一个工厂单例(RootFactory),该单例又由应用程序实例管理(如果愿意,则是委托)。该单例管理所有应用程序组件所依赖的公共依赖关系,但是实例化在一个位置(应用程序类)中进行管理。尽管采用这种方法,但仍然只剩下一个单例,而只限于Application类,因此没有其他代码模块知道该“详细信息”。
Matthias

231

我非常推荐单身人士。如果您有需要上下文的单例,请执行以下操作:

MySingleton.getInstance(Context c) {
    //
    // ... needing to create ...
    sInstance = new MySingleton(c.getApplicationContext());
}

与应用程序相比,我更喜欢单例,因为它有助于使应用程序更加组织化和模块化-而不是在一个地方需要维护应用程序的所有全局状态,每个单独的地方都可以照顾好自己。同样,单身人士懒惰地初始化(应要求)而不是引导您沿着Application.onCreate()进行所有初始化的路径这一事实也很好。

使用单例并没有本质上的错误。只要有意义,请正确使用它们。Android框架实际上有很多,因为它可以维护加载资源和其他类似东西的按进程缓存。

同样对于简单的应用程序,单例也不会出现多线程问题,因为通过设计,应用程序的所有标准回调都在进程的主线程上分派,因此除非您通过线程或线程明确引入多线程,否则不会发生多线程事件。通过将内容提供者或服务IBinder发布到其他进程来隐式地进行。

只要考虑一下自己在做什么。:)


1
如果过了一段时间我想听一个外部事件,或者在一个IBinder上共享(我想那不是一个简单的应用程序),我将不得不添加双重锁定,同步,易失性,对吗?感谢您的回答:)
mschonaker

2
不用于外部事件-在主线程上也调用BroadcastReceiver.onReceive()。
hackbod 2010年

2
好的。您是否可以指向一些阅读材料(我更喜欢代码),在其中可以看到主线程分配机制?我认为这将立即为我澄清几个概念。提前致谢。
mschonaker

2
这是应用程序侧的主要调度代码:android.git.kernel.org/?
p=platform/frameworks/…

8
使用单例并没有本质上的错误。只要有意义,请正确使用它们。..当然,准确地说。Android框架实际上有很多,因为它可以维护加载资源和其他类似东西的按进程缓存。就像你说的那样。从iOS世界中的朋友那里,iOS中的“一切都是单例” ..在物理设备上,没有什么比单例概念更自然的了:gps,时钟,陀螺仪等-从概念上讲,您将如何设计这些组件?作为单身人士?嗯是的。
Fattie

22

来自:开发人员>参考-应用

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


1
而且,如果您为单例编写接口,而使getInstance保持非静态状态,则甚至可以使使用单例的类的默认构造函数通过非默认构造函数注入生产单例,这也是您用来创建在其单元测试中使用单例的类。
android.weasel

11

申请与单身人士不同,原因如下:

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

1
注意:您可以在任何线程上实例化Handler。来自doc:“当您创建新的处理程序时,它将绑定到正在创建它的线程的线程/消息队列中”
Christ

1
@Christ谢谢!刚才我学习了“ Looper的机制”。如果在无UI线程上实例化处理程序而没有代码“ Looper.prepare()”,系统将报告错误“ java.lang.RuntimeException:可以在未调用Looper.prepare()的线程内创建处理程序”。
sunhang 2014年

11

我遇到了同样的问题:Singleton还是使子类成为android.os.Application?

首先,我尝试使用Singleton,但有时我的应用会调用浏览器

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

问题是,如果手机没有足够的内存,则会清理大多数类(甚至是Singletons)以获取一些内存,因此,当从浏览器返回到我的应用程序时,它每次都崩溃。

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


1
我经常遇到人们指出可能发生这种情况的帖子。因此,我只是简单地将对象(如具有延迟加载的单例)附加到应用程序,以确保记录和知道了生命周期。只需确保不要将数百张图像保存到您的应用程序对象中即可,因为据我了解,如果您的应用程序在后台并且所有活动都被破坏以释放内存供其他进程使用,则不会从内存中清除该图像。
Janusz

好了,在应用程序重新启动后进行Singleton延迟加载不是让对象被GC清除的正确方法。弱引用是吧?
mschonaker

15
真?Dalvik卸载类并丢失程序状态?您确定不是在垃圾收集那种与生命周期有限的活动相关的对象,而这些对象本来不应该放在单例中的?您必须提供clrar示例来说明这种非凡的要求!
android.weasel

1
除非有我不知道的变化,否则Dalvik会这么做 卸载类。曾经 他们看到的行为是其进程在后台被杀死,从而为浏览器腾出空间。他们可能会在其“主要”活动中初始化变量,当从浏览器返回时,该变量可能不是在新流程中创建的。
Groxx

5

同时考虑两者:

  • 将单例对象作为类中的静态实例。
  • 具有一个公共类(Context),该类返回应用程序中所有singelton对象的单例实例,这具有以下优点:Context中的方法名称将是有意义的,例如:context.getLoggedinUser()而不是User.getInstance()。

此外,我建议您扩展Context,使其不仅包括对单例对象的访问,还包括一些需要全局访问的功能,例如:context.logOffUser(),context.readSavedData()等。可能将Context重命名为那时,立面是有意义的。


4

它们实际上是相同的。我可以看到一个区别。使用Application类,可以在Application.onCreate()中初始化变量,并在Application.onTerminate()中销毁它们。使用Singleton,您必须依靠VM初始化和销毁​​静态数据。


16
onTerminate的文档说,它只能由模拟器调用。在设备上,可能不会调用该方法。developer.android.com/reference/android/app/…–
danb

3

我的2美分:

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

我的情况很简单:我只有一个私有文件“ init_done”和一个从activity.onCreate()调用的静态方法“ init”。我注意到方法初始化是在重新创建活动时重新执行自身。

虽然我无法证明我的肯定,但是这可能与第一次创建/使用单例/类的时间有关。当该活动被销毁/回收时,似乎只有该活动引用的所有类也都被回收了。

我将单例实例移至Application的子类。我从应用程序实例访问它们。从那以后,再也没有发现问题。

我希望这可以帮助某人。


3

从众所周知的马口中

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

编写Android应用时,可以确保只有android.app.Application类的一个实例,因此可以安全地(并由Google Android团队推荐)将其视为单例。也就是说,您可以安全地将静态getInstance()方法添加到Application实现中。像这样:

public class AndroidApplication extends Application {

    private static AndroidApplication sInstance;

    public static AndroidApplication getInstance(){
        return sInstance;
    }

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

2

我的活动调用了finish()(不会立即完成,但最终会完成)并调用Google Street Viewer。当我在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中并没有丢失,据我了解,这表明内存恢复机制在某些时候已经改变。” .....我想您的设备没有相同数量的可用内存,也没有安装相同的应用程序。因此您的结论可能不正确
基督

@克里斯特:是的,你一定是正确的。如果版本之间的内存恢复机制发生了变化,这将很奇怪。可能不同的内存使用情况导致了不同的行为。
Piovezan 2014年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.