關於dispatch_once的坑及注意點

那時天很藍發表於2018-02-01

說到單例,在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 以上是該次問題的總結,歡迎大家指教。

相關文章