有一个单例类是这么写的:
public class BluetoothManager {
private static BluetoothManager sInstance;
public static BluetoothManager getInstance() {
if (sInstance == null) {
synchronized (BluetoothManager.class) {
if (sInstance == null) {
sInstance = new BluetoothManager();
}
}
}
return sInstance;
}
private final Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
private final LeScanCallback mLeScanCallback = new LeScanCallback() {
@Override
public void onLeScan(BluetoothDevice device, int rssi, byte[] scanRecord) {
}
};
}
这个类在实际运行的过程中偶然会崩溃,原因在于调用getInstance可能在主线程,也可能在子线程,第一次调用getInstance会触发BluetoothManager单例对象的创建,由于mHandler是对象内部的成员变量,所以会跟着一起初始化,由于没有传入Looper,所以会默认传入当前调用者所在线程的Looper,倘若该线程没有Looper就会抛出异常。所以这个问题会出现在首次调用getInstance在没有Looper的子线程中,解决的办法是构造Handler时要传入一个有效的Looper。
第二个问题是LeScanCallback,这个类是用于蓝牙BLE扫描的回调,需要在API 18及以上使用,否则会崩溃。在API 18以下即便不扫描,没有用到这个mLeScanCallback,只要有任何操作触发了mLeScanCallback的初始化,都会导致崩溃。
那什么情况下会触发BluetoothManager的这个内部成员变量的初始化呢?由于不是静态的,所以只有创建BluetoothManager对象的时候会初始化mLeScanCallback。倘若mLeScanCallback是静态的,则只要BluetoothManager类初始化时就会被初始化。
接下来总结一下Java的类加载,先看看类初始化的时机:
- 使用new关键字实例化对象的时候,引用一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)或调用静态方法的时候。
- 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
注意上面第一条,被final修饰的静态变量,如果是类似于String这种基本类型会被放入常量池,引用这种字段是和类没关系的,所以不会触发类的初始化。
顺便提一下,判断一个类是否被初始化可以考虑的办法是在类的全局static代码块中打日志。
当一个类初始化时,先调用类的static代码块,然后初始化类中的静态成员(不包括静态内部类),如果是创建类对象,则接下来还要初始化类中的非静态成员,最后才会调用类的构造函数。
来看看如下的单例模式:
public class BluetoothManager {
private BluetoothManager() {
}
private static class BluetoothManagerHolder {
private static BluetoothManager instance = new BluetoothManager();
}
public static BluetoothManager getInstance() {
return BluetoothManagerHolder.instance;
}
}
当BluetoothManager类初始化时不会触发BluetoothManagerHolder的初始化,只有调用getInstance时引用到了BluetoothManagerHolder的内部静态成员变量时才会触发BluetoothManagerHolder的初始化,这样可以达到延迟加载的效果。假如有多个线程同时调用getInstance,由于instance是在BluetoothManagerHolder类初始化时跟着初始化的,所以是由Java虚拟机保证线程安全的,无需额外的同步,这种单例模式值得推荐。