
一. 引言
在日常开发中,我们几乎每天都要和单利打交道。文件管理器、数据库、网络请求、下载管理、配置中心......
很多类天生就只应该有一个实例。于是我们经常随手就写了一个单利。
但是可能就会出现:
- 内存泄漏
- 线程安全问题
- 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,而是为了让资源、生命周期和责任都清清楚楚。