Speaking of singletons, it’s easy to think of using dispatch_once to build a singleton object in Objective-C, but recently we stumbled into the Dispatch_once trap while adding new functionality to an old project we are currently maintaining. A brief explanation of the problem: In the company’s test, after the APP is installed for a period of time, the black screen will always flash back when you re-enter the APP, and the time of occurrence is uncertain. Such a situation can only occur in one model during the test. When I exported the crash log, it looked like this before the crash (the company’s project, slightly coded) :
Trying to lock recursively enabled on the screen. Then I noticed that the symbolic message closest to the crash was NBSDispatch_once, which is a method used by a third party to monitor the app’s library. Then I replaced the latest SDK and found that this method has been removed in the new VERSION of SDK. The test found that the similar situation did not occur again, so I was relieved. PM to publish to the APP Store.
I thought the problem would be solved in this way. However, after I released the APP to the APP Store the night before, some customers reported that I could not enter the APP. However, this time it was slightly different from the previous one.
Let’s have a look at the crash logs, there is a way [AppViewControllerManager getAppViewControllerManager] this method appears many times, AppViewControllerManager is the class responsible for managing the entire project’s attempt controller, and this method is the singleton object that gets this management class. Inside the singleton method internal dispatch_once and contains a sequential execution method, in the subsequent execution method will have a call again in the [AppViewControllerManager getAppViewControllerManager], resulting in the deadlock, In addition, the deadlock is caused by an asynchronous network request. When the result of dispatch_once is returned too fast, the dispatch_once block is entered again before the execution of the first dispatch_once block is completed, resulting in a deadlock.
Let’s construct a similar Demo project to illustrate this. In order to guarantee the occurrence of a damned lock, we will execute it all with synchronous threads. (Note: a deadlock on an emulator does not crash, but a system protection mechanism forces a crash on a real machine.)
Compilation environment: System: macOS 10.12 Xcode version: 8.0
Let’s create a new management class AppManager in the Demo and add a singleton method to it:
+ (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;
}
Copy the code
In the 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;
}
Copy the code
We add the method doSomeThing to viewController.m:
- (void)doSomeThing {
[AppManager getAppManager];
self.view.backgroundColor = [UIColor cyanColor];
}
Copy the code
Following the steps, the expected screen background should be [UIColor cyanColor], regardless of other factors; However, the actual results are as follows:
Let’s look at the running stack:
The app process stops in [ViewController doSomeThing] and enters signal waiting after calling [AppManager getAppManager] [AppManager getAppManager] [dispatch_once] block has not been executed and is in lock state waiting for completion of the block. Another call to [AppManager getAppManager] is made within the block, resulting in a deadlock.
We comment out [AppManager getAppManager] in [ViewController doSomeThing]; [UIColor cyanColor]:
How to avoid this kind of problem?
1. Use singleton mode with caution; 2. Try to avoid coupling with other classes in the block wrapped by dispacth_once.
Recommended writing:
Remove from dispatch_once where there is coupling with other classes:
+ (AppManager *)getAppManager {
static AppManager *manager = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
manager = [[AppManager alloc] init];
});
return manager;
}
Copy the code
Place to the position following the completion of the initialization singleton execution:
- (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;
}
Copy the code
Demo address: kisekied/dispatch_once_lock_demo The above is the summary of this problem, welcome your advice.