由两个bug引发的对Java类加载时机的思考

有一个单例类是这么写的:

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的类加载,先看看类初始化的时机:

  1. 使用new关键字实例化对象的时候,引用一个类的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)或调用静态方法的时候。
  2. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。
  3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
  4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含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虚拟机保证线程安全的,无需额外的同步,这种单例模式值得推荐。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章