由兩個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虛擬機保證線程安全的,無需額外的同步,這種單例模式值得推薦。

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