介绍

Singleton 是一种非常直接且有用的设计模式。然而,Android 应用程序开发有一些其他平台没有的问题,并且在不理解的情况下使用 Singleton 可能会让人头疼。我想写一下Android特有的情况。

什么是特定于 Android 的?

据我所知,在使用 Singleton 时,有两种主要的 Android 特定情况可能会导致问题。

  • 类卸载可以初始化静态变量
  • 进程退出后应用程序状态可能会持续存在

前者是一种很少发生的现象,所以在大多数情况下都不是问题,但后者发生的比较频繁。
因此,在下文中,我将简要解释前一种现象,这不是一个真正的问题,然后深入研究后一种现象,后者的影响更大。

静态变量可以在类卸载时初始化

Android VM 有时会卸载类,因此静态变量可能会丢失其存储的值(在下次读取时初始化)。这是我很久以前在实际机器上经历过的现象。以下页面也提到了类似的内容。

类引用、字段 id 和方法 id 保证在类被卸载之前是有效的。仅当与类加载器关联的所有类都可以被垃圾回收时,才会卸载一个类。这种情况在安卓上很少见,但也不是不可能。

但是,正如您在上面的页面中看到的那样,这种现象似乎很少发生。事实上,我只遇到过一次上述的这种现象(虽然这可能是因为我在执行过程中变得更加小心)。

所以在这方面,我认为可以判断,不应该为了很少发生的事情而牺牲开发效率。但是,正如我之前所写的,我认为在 Android 上使用 Singleton 时需要小心,因为即使在进程结束后应用程序的状态也可能保持不变。我将在下面解释原因。

进程退出后应用程序状态可能会持续存在

对于不熟悉Android应用开发的朋友,我觉得是“什么鬼”,但Android应用是由Android框架管理的,所以普通的Java应用(Java SE、Java EE等)的生命周期略有不同。

对于典型的 Java 应用程序,应用程序终止和进程终止几乎是同义词。
但对于安卓应用来说,两者不一定相同。即使进程已终止,应用程序可能仍处于活动状态(在系统上)。例如,让我们看看下面的代码。

class MainActivity : AppCompatActivity() {
    private var state = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        state = savedInstanceState?.getInt("state", 0) ?: 0
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)
        outState.putInt("state", state)
    }
}

此代码提供state 作为活动状态变量。而这个状态变量通过onSaveInstanceState()方法保存,通过onCreate()方法恢复。这意味着“应用程序的状态由 Android 框架管理”。

我们为什么要做这个?即使不这样做,state 的值也应该在 MainActivity 实例存在时保留,并且只要应用程序正在运行,MainActivity 实例就应该存在,对吧?

这可能是重要的一点。首先,即使应用程序没有终止,Activity 类的实例也可以被销毁。这主要发生在应用程序处于后台并且内存不足时。由于类的实例被销毁,实例变量state的内容也会消失。在这种情况下,Android 框架将在下次应用程序进入前台时自动生成一个新的Activity 实例。而此时,保存在onSaveInstanceState() 的数据会传递给onCreate()。该机制用于维护状态。

更重要的是,不仅Activity 类的实例被破坏,这意味着应用程序进程本身可能会被终止并作为新进程启动。即使在那种情况下,onSaveInstanceState()在旧进程中保存的数据在新进程中传递给onCreate()因此,即使进程已终止,应用程序的状态也会保留。用户会看到“此应用程序从未关闭”。这就是为什么我说“应用程序终止和进程终止并不总是相同”的原因。

即使进程已终止,状态仍保持不变,这太愚蠢了! ! !

我敢肯定,有些人认为...我以前也是。但是,这在官方文档中有很好的记录。

用户希望 Activity 的 UI 状态在配置更改(例如旋转或切换到多窗口模式)时保持不变。 (...) 但是,当用户离开并且活动停止时,系统可能会破坏应用程序的进程。
如果您的 Activity 由于系统限制而被销毁,您应该使用 ViewModel、onSaveInstanceState() 和本地存储的组合来持久化用户的瞬态 UI 状态。请参阅保存 UI 状态以比较用户期望与系统行为,以及如何最好地跨系统启动的活动和进程退出保存复杂的 UI 状态数据。

下面的文档清楚地指出,即使在进程终止后,savedInstanceState 也会保留。

换句话说,Android 作为一个平台积极地整合了一种跨进程维护状态的机制,我认为应用程序开发人员必须相应地实现它。

可以理解为,Android之所以被设计成这样,是因为一个应用程序的生死和一个进程的生死被当成是分开的东西。服务和活动都有类似的噱头。 Android 服务在内存不足时相对容易终止,但它们会由 Android 框架自动重新生成(Service#onStartCommand()方法返回START_STICKY 等)。从这一点,我们可以看出Android有这样的想法。 (嗯,我觉得这就是Android的做法的难点……)

这(应用程序状态跨进程维护)也会影响单例的使用。例如,考虑以下类。

class MySingleton private constructor() {
    companion object {
        private var instance: MySingleton? = null

        fun getInstance(): MySingleton = instance ?: MySingleton().also {
            instance = it
        }
    }

    var state = 0
}

我认为这是您经常看到的 Singleton 的实现。我有一个静态变量instance,它保存了这个类的一个实例,所以我可以在我的应用程序的任何地方引用它。但是,正如您现在可能已经猜到的那样,存储在此 Singleton 中的数据会在应用进程终止时消失,并且不会被新进程继承。

也就是说,Android框架是为了跨app进程维护app状态而设计的,而Singleton是为了只在app当前进程内维护app状态而设计的,我想你可以这么说。因此,Android框架管理的状态和Singleton管理的状态可能存在差异或不一致。

我想读到这里的你们中的一些人可能会想,“完全不使用这些特定于 Android 的功能不是更好吗?”也就是说,如果你不覆盖ActivityonSaveInstanceState()方法,并且不恢复onCreate()方法中的状态,上面的“Android框架管理的状态”应该会消失,所以这个问题是我不有些人认为它不会发生,这并不奇怪。或者更确切地说,它是旧的我。

但我认为这种方式不太现实。这是因为 Android 的现代架构在设计和实现时就考虑到了这一特性。比如常用的Fragment或者导航组件使用这个函数来保留状态,所以即使app进程结束,屏幕和屏幕转换的状态也会被再现。因此,如果您将(部分)应用程序状态存储在 Singleton 中,则该状态与屏幕状态之间可能存在不一致。应该避免这种情况。

示例应用

我们准备了一个示例应用程序,可让您实际检查上述问题。

导航组件这将是一个简单的应用程序,使用 .用户输入的数据保存在一个 Singleton 对象中,您可以看到数据是如何消失的。应用程序规范和复制方法在存储库中罕见的D眼。医学博士请阅读

使用 DI 容器时也会出现类似的问题

我认为有时您会从 DI 容器中获取 Singleton 对象并使用它,但当然会出现同样的问题。
尝试将上面的示例代码修改为刀柄注入 Singleton 对象(我在同一个存储库中有一个分支use_hilt)。

代码看起来像这样。

interface InputData {
    var name: String
    var email: String
}

class InputDataImpl @Inject constructor() : InputData {
    override var name: String = ""
    override var email: String = ""
}

@Module
@InstallIn(SingletonComponent::class)
abstract class InputDataModule {
    @Singleton
    @Binds
    abstract fun bindInputData(
        inputDataImpl: InputDataImpl
    ): InputData
}

// InputData の注入方法
@Inject
lateinit var inputData: InputData

结果和不使用 Hilt 的时候一样,如果你把它放在屏幕 C 的后台,内存耗尽后返回到前台,名字和电子邮件地址将为空。

那么我们应该怎么做呢?

对于需要长期保存的数据,我认为使用savedInstanceState 是安全的。根据需要将存储在 Singleton 中的数据保存到 savedInstanceState。但是,从本质上讲,奇怪的是,Singleton 的内容是由特定的组件(活动)“保存”和“恢复”的,而那不是 Singleton!我是这样的感觉。与其做这样的事情,不如将数据保存为 ViewModel 似乎是一种更现代的设计。如果需要在屏幕之间交换数据,安全参数等等,让我们使用框架准备的方法。

此外,虽然它有点超出了本文的范围,但在使用 ViewModel 时应该小心一点。 ViewModel持有的数据也会在进程重新生成时消失,所以需要根据需要保存到savedInstanceState。详情请参考↓这里。

那么Singleton应该用来做什么呢?

看起来您完全否认 Singleton,但事实并非如此。简而言之,就是用在哪里的问题,应该用在进程结束时即使清空状态也没有问题的应用。立即想到的事情是日志记录和与资源管理相关的处理。可能还有许多其他用途。

综上所述

我见过一些大公司的应用程序从后台返回到前台时出现问题的情况,所以我认为这可能是原因。如果有人可以参考它,我将不胜感激。

另外,我打算在查看官方文档的同时写下这篇文章的内容,但如果有任何错误,如果您能发表评论,我会很高兴。


原创声明:本文系作者授权爱码网发表,未经许可,不得转载;

原文地址:https://www.likecs.com/show-308627122.html

相关文章:

  • 2021-10-23
  • 2021-11-03
  • 2021-10-26
  • 2021-11-05
  • 2022-01-10
  • 2021-09-09
  • 2021-12-09
猜你喜欢
  • 2021-10-02
  • 2021-07-15
  • 2021-08-26
  • 2021-11-17
  • 2021-11-15
  • 2021-10-04
  • 2017-12-09
  • 2021-11-23
相关资源
相似解决方案