說到單例,在Objective-C中我們很容易就能想到用dispatch_once
來構建一個單例的物件,然而最近因為給一個目前維護的老專案增加新的功能的時候,卻不小心踩到了dispatch_once
的坑裡面去了。
簡單的說明一下遇到的問題:公司測試在安裝完APP一段時間後,重新進入會一直黑屏閃退,並且發生時間不確定,測試的時候也只是一個機型會發生這樣的情況。
當時我匯出了崩潰日誌,發現崩潰前是這樣的(公司的專案,稍微打了一下碼):
看到上面顯示trying to lock recursively
,說明是被死鎖了。然後我注意到最靠近崩潰的符號化資訊是NBSDispatch_once
,這是某第三方監測app的庫的一個方法,然後我替換了最新的SDK,發現新版本的SDK已經去除了這個方法,測試發現沒再出現類似的情況,於是鬆了一口氣,PM讓釋出到APP Store。
本以為這件事就這樣解決了,然而在我前一天晚上釋出到APP Store後,有客戶反饋會出現進入不了APP的問題,但是這次跟上次的又略有不同,上面那個是開啟APP後一直黑屏然後閃退,而這個是在APP載入了Launch
介面後出現的問題。
讓我們再看一下上面的崩潰日誌,有一個方法[AppViewControllerManager getAppViewControllerManager]
這個方法出現多次,AppViewControllerManager
是負責統管整個專案試圖控制器的類,而這個方法是獲取這個管理類的單例物件。在該單例方法內部的dispatch_once
裡面又包含了後續執行方法,在後續執行方法中有一段會再次呼叫[AppViewControllerManager getAppViewControllerManager]
,從而造成了死鎖,又因為造成死鎖的方法是非同步網路請求,在返回結果過快的時候,第一次的dispatch_once
的block還沒有執行結束的就再次進入該dispatch_once
的block,導致死鎖。
下面讓我們構造一個類似的Demo工程進行說明,為了保證出現該死鎖,我們全部用同步執行緒執行。(注:模擬器上死鎖不會crash,真機上因為系統保護機制才會強制crash)。
編譯環境: 系統: macOS 10.12 Xcode版本:8.0
我們在Demo中新建一個管理類AppManager
,在裡面新增一個單例方法:
+ (AppManager *)getAppManager {
static AppManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[AppManager alloc] init];
ViewController *vc = [[ViewController alloc] init];
UIWindow *window = [UIApplication sharedApplication].keyWindow;
window.rootViewController = vc;
[vc doSomeThing];
});
return manager;
}
複製程式碼
在AppDelegate.m
中:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self.window makeKeyWindow];
_manager = [AppManager getAppManager];
[self.window makeKeyAndVisible];
return YES;
}
複製程式碼
我們在ViewController.m
中新增方法doSomeThing
:
- (void)doSomeThing {
[AppManager getAppManager];
self.view.backgroundColor = [UIColor cyanColor];
}
複製程式碼
不考慮其他因素,按步驟下來,預期螢幕背景應該是[UIColor cyanColor]; 然而實際執行結果是:
我們檢視執行的堆疊資訊:
發現app程式停在[ViewController doSomeThing]
中,並且在呼叫了[AppManager getAppManager]
後進入訊號等待,因為此時[AppManager getAppManager]
中dispatch_once
的block並未執行完成,處於lock
狀態,等待完全執行完成該block,而在該block中又進行了一次[AppManager getAppManager]
的呼叫,從而造成死鎖。
我們註釋掉[ViewController doSomeThing]
中的[AppManager getAppManager];
後發現背景色如期變成[UIColor cyanColor]:
如何避免這類問題的發生?
1.慎用單例模式; 2.在
dispacth_once
包裹的block中,儘量避免與其他類的耦合。
推薦寫法:
將dispatch_once
中與其他類耦合的地方移出:
+ (AppManager *)getAppManager {
static AppManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[AppManager alloc] init];
});
return manager;
}
複製程式碼
放置到完全執行完初始化單例的後續方位執行:
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds];
[self.window makeKeyWindow];
_manager = [AppManager getAppManager];
ViewController *vc = [[ViewController alloc] init];
UIWindow *window = [UIApplication sharedApplication].keyWindow;
window.rootViewController = vc;
[vc doSomeThing];
[self.window makeKeyAndVisible];
return YES;
}
複製程式碼
Demo地址在: kisekied/dispatch_once_lock_demo 以上是該次問題的總結,歡迎大家指教。