Android 中的单例模式:从“看起来很简单”到“真的不会出事”(详细解读包含依赖Context的单利)

一. 引言

在日常开发中,我们几乎每天都要和单利打交道。文件管理器、数据库、网络请求、下载管理、配置中心......

很多类天生就只应该有一个实例。于是我们经常随手就写了一个单利。

但是可能就会出现:

  • 内存泄漏
  • 线程安全问题
  • Lint 黄线警告
  • 偶现的线上 Crash

其实问题可能并不在单利本身,而在Android场景下的单利。

这篇文章我们就从最基础的单例讲起,一步一步聊清楚:Android 中,单例到底应该怎么写,才算安全、优雅、可维护。

二. 什么是单例模式?它解决了什么问题?

单例模式(Singleton Pattern)的目标很简单:

  • 一个类 全局只有一个实例
  • 提供一个 统一的访问入口
  • 避免重复创建带来的资源浪费
  • 保证全局访问的数据都是同一份数据

在 Android 中,单例通常用于:

  • 文件管理(FileHelper)
  • 数据库(Room / SQLite)
  • 网络层(OkHttpClient / Retrofit)
  • 全局配置、缓存管理

本质上,单例是在帮我们管理 "资源 + 生命周期"。

三. 单利的实现

单利的写法有很多种,在kotlin中更是提供了又方便又安全的写法,我们还是先从最纯粹的Java开始讲起。

3.1 饿汉式(最简单,也最安全)

先解释一下"饿汉式"这三个字吧,就是着急,上来就要有这个实例。

实现如下:

java 复制代码
public class FileHelper {
    private static final FileHelper INSTANCE = new FileHelper();

    private FileHelper() {}

    public static FileHelper getInstance() {
        return INSTANCE;
    }
}

使用:

java 复制代码
FileHelper.getInstance()

它在类加载时就已经创好了实例,天然的线程安全,实现又简单。

但是呢 它不支持延迟加载,如果单利的创建成本比较高(比如加载资源,读取数据),可能会出现卡顿等现象。

3.2 懒汉式(看起来更合理,但有坑)

"懒汉式" 对应"饿汉式" 就是我不着急,而且呢又比较懒,你要是不用我就不创建了,啥时候用啥时候我就创建。

实现如下:

java 复制代码
public class FileHelper {
    private static FileHelper instance;

    private FileHelper() {}

    public static FileHelper getInstance() {
        if (instance == null) {
            instance = new FileHelper();
        }
        return instance;
    }
}

使用:

java 复制代码
FileHelper.getInstance()

这段代码大家可能都写过。看起上去OK,无论怎么获取都是原来那一个。

但是:它保证不了线程安全,在单利没有被创建之前,如果两个线程同时调用getInstance()方法,都有可能判断 instance == null 成立,从而创建出 两个实例。

3.3 懒汉式 + synchronized (简单但不优雅)

既然是线程不安全,那么解决起来倒也简单,直接加锁就完了。

实现如下:

java 复制代码
public class FileHelper {
    private static FileHelper instance;

    private FileHelper() {}

    public static synchronized FileHelper getInstance() {
    if (instance == null) {
        instance = new FileHelper();
    }
    return instance;
}
}

这种写法虽然保证了线程安全,但是每次调用都要加锁,性能开销较大,并不适合高频访问。

3.3 懒汉式 + 双重检查锁定(DCL)

java 复制代码
public class FileHelper {
    private static volatile FileHelper instance;

    private FileHelper() {}

    public static FileHelper getInstance() {
        if (instance == null) {
            synchronized (FileHelper.class) {
                if (instance == null) {
                    instance = new FileHelper();
                }
            }
        }
        return instance;
    }
}

这是一个正确但复杂的写法。

  • volatile 防止指令重排
  • 只在第一次创建时加锁
  • 性能 OK

3.4 最推荐的 Java 单例:静态内部类

实现如下:

java 复制代码
public class FileHelper {

    private FileHelper() {}

    private static class Holder {
        private static final FileHelper INSTANCE = new FileHelper();
    }

    public static FileHelper getInstance() {
        return Holder.INSTANCE;
    }
}

为什么推荐它?

  • 延迟加载(调用时才创建)
  • JVM 保证线程安全
  • 无需 synchronized
  • 写法干净、易维护

如果你的单例 不依赖外部参数静态内部类几乎是最优解。

四. 重灾区:依赖 Context 的单例

前面所有写法,在 Android 场景下都还不够。因为 ------ Context。

为什么说依赖Context的单利很危险?

因为 Context 有多种类型:

  • Activity
  • Fragment
  • View
  • Application

如果我们把 Activity / Fragment 的 Context 放进单例里:

单利的生命周期是与Application一致的,那么就导致Activity或者Fragment永远也回收不了。直接内存泄漏。

4.1 带 Context 单利实现

既然单利与Application的生命周期一致,那么我们就把 Application 的 Context给单利,而不是Activity或者Fragment的。

实现如下:

java 复制代码
public class FileHelper {
    private static FileHelper instance;
    private Context context;

    private FileHelper(Context context) {
        this.context = context.getApplicationContext();
    }

    public static FileHelper getInstance(Context context) {
        if (instance == null) {
            instance = new FileHelper(context);
        }
        return instance;
    }
}

核心原则只有一句:单例只能持有 Application Context

4.2 Kotlin 中的单利

而在 Kotlin 中,单例的答案非常直接。

Kotlin 复制代码
@SuppressLint("StaticFieldLeak")
object FileHelper {

    private lateinit var context: Context

    fun init(context: Context) {
        this.context = context.applicationContext
    }
}

为什么这是推荐写法?

  • object 天生线程安全
  • JVM 层面只会初始化一次
  • 代码极简
  • 语义清晰

通常来讲我们会在Applicant中进行单利的初始化。

自定义AssistantApp 继承自Application:

Kotlin 复制代码
class AssistantApp : Application() {

    override fun onCreate() {
        super.onCreate()

        FileHelper.init(this)
        CoreDataHelper.init(this)

        Log.d("AssistantApp_log", "单例初始化完成")
    }
}

这样做的好处:

  • 初始化时机统一
  • 生命周期清晰
  • 不再需要到处传 Context

使用:

Kotlin 复制代码
 FileHelper.unzipFile(file)

任何地方都可以直接使用。

五. 结语

  • 单例模式本身并不复杂
  • Android 中的单例,复杂在 生命周期
  • Context 是最大也是最常见的坑
  • Java 推荐静态内部类
  • Kotlin 推荐 object + Application init

单例不是为了少写一个 new,而是为了让资源、生命周期和责任都清清楚楚。

相关推荐
代码or搬砖3 小时前
设计模式之单例模式
单例模式·设计模式
会员果汁1 天前
17.设计模式-单例模式(Singleton)
单例模式·设计模式
小码过河.2 天前
设计模式——单例模式
单例模式·设计模式
jghhh014 天前
C#中实现不同进程(EXE)间通信的方案
java·单例模式·c#
点云SLAM4 天前
C++依赖注入(Dependency Injection DI)vs单例设计模式(Singleton)
开发语言·c++·单例模式·设计模式·日志配置·依赖注入di·大项目系统
apolloyhl4 天前
Singleton 单例模式
单例模式
le1616165 天前
设计模式之单例模式
单例模式·设计模式
Knight_AL5 天前
从单例模式说起:Java 常见设计模式的理解与实践
java·单例模式·设计模式
txinyu的博客5 天前
C++ 单例模式
c++·单例模式