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