SoFunction
Updated on 2025-04-14

Detailed analysis of singleton usage issues in iOS development tutorial

Introduction

Singletons are one of the core models of Cocoa. On iOS, singletons are very common, such as: UIApplication, NSFileManager, etc. Although they are very convenient to use, they actually have many problems to pay attention to. So when you automatically complete the dispatch_once code snippet next time, think about what the consequences will be.

What is a single case

The definition of singleton is given in the book "Design Pattern":

Singleton pattern: Ensure that a class has only one instance and provides a global access point to access it.

Singleton pattern provides an access point for the client class to generate a unique instance of the shared resource and access the shared resource through it. This pattern provides flexibility.

In objective-c, you can create a singleton using the following code:

+(instancetype)sharedInstance
{
 static dispatch_once_t once;
 static id sharedInstance;
 dispatch_once(&once, ^{
 sharedInstance = [[self alloc]init];
 });
 return sharedInstance;
}

Using singletons is very convenient when a class can only have one instance and must be accessed from one access point, because using singletons ensures that the access point is unique, consistent and well-known.

Problems in singleton

Global status

First of all, we should all reach a consensus that "global variable state" is dangerous, because this will make the program difficult to understand and debug. In order to reduce the state code, object-oriented programming should be learned from functional programming.

For example, the following code:

@implementation Math{
 NSUInteger _a;
 NSUInteger _b;
}

-(NSUInteger)computeSum
{
 return _a + _b;
}

This code wants to calculate the sum of the addition of _a and _B and return it. But in fact, there are many problems with this code:

  • _a and _b are not used as parameters in the computeSum method. Compared to finding the interface and knowing the output of which variable control method, looking for implementation to understand it seems more hidden, and hidden means that errors are prone to occur.
  • When preparing to modify the values ​​of _a and _b to make them call the computeSum method, programmers must be clear that modifying their values ​​will not affect the correctness of other codes containing the two values, and making such judgments in multi-threading situations is particularly difficult.

Compare the following code:

+(NSUInteger)computeSumOf:(NSUInteger)a plus:(NSUInteger)b
{
 return a + b;
}

In this code, the subordinates of a and b appear very clear, and there is no need to change the state of the instance to call this method, and there is no need to worry about the side effects of calling this method.

So what does this example have to do with a singleton? In fact, singleton is the global state of wearing sheepskin. A singleton can be used anywhere without clearly declaring subordinates. Any module in the program can simply call [MySingleton sharedInstance] and then get the access point of this singleton, which means that any side effects caused when interacting with the singleton may affect a random piece of code in the program, such as:

@interface MySingleton : NSObject

+(instancetype)sharedInstance;

-(NSUInteger)badMutableState;
-(void)setBadMutableState:(NSUInteger)badMutableState;

@end

@implementation ConsumerA

-(void)someMethod
{
 if([[MySingleton sharedInstance] badMutableState]){
 //do something...
 }
}

@end

@implementation ConsumerB

-(void)someOtherMethod
{
 [[MySingleton sharedInstance] setBadMutableState:0];
}

In the above code, ConsumerA and ComsumerB are two completely independent modules in the program, but the methods in ComsumerB will affect the behavior in ComsumerA because this state change is passed through the past through a singleton.

In this code, it is precisely because of the globality and state of the singleton that the implicit coupling between two modules, ComsumerA and ComsumerB, seems to have no relationship.

Object life cycle

Another major problem with singletons is their life cycle.

For example, suppose an app needs to implement the function that allows users to see their friend list, and each friend has his own avatar. At the same time, we also hope that this app can download and cache the avatars of these friends. At this time, by learning the knowledge of singletons before, we are likely to write the following code:

@interface MyAppCache : NSObject

+(instancetype)sharedCMyAppCache;

-(void)cacheProfileImage:(NSData *)imageData forUserId:(NSString *)userID;
-(NSData *)cachedProfileImageForUserId:(NSString *)userId;

@end

This code looks completely fine and runs well, so the app continues to develop until one day, we decided to help the app add the "logout" function. Suddenly we found that user data is stored in a global singleton. When a user logs out, we want to clear the data, and when a new user logs in, we create a new MyAppCache for him.

But the problem lies in the singleton, because the definition of a singleton is: "create once, survive permanently". In fact, there are many ways to solve the above problem. We may be able to destroy this singleton when the user logs out:

static MyAppCache *myAppCache;

+(instancetype)sharedMyAppCache
{
 if(!myAppCache)
 {
 myAppCache = [[self alloc] init];
 }
 return myAppCache;
}

+(void)tearDown
{
 myAppCache = nil;
}

The above code distorts the singleton pattern, but it can work.

In fact, this method can be used to solve this problem, but it is too expensive. The most important point is that we have given up dispatch_once, which ensures thread safety when calling method. Now all codes calling [MyAppCache shareMyAppCache] will get the same variable, so we need to clearly use the MyAppCache code to execute. Imagine when the user logs out, happens to call this method in the background to save the image.

On the other hand, implementing this method requires ensuring that the tearDown method will not be called when the background task has not been executed, or ensuring that the background task will be cancelled when the tearDown method is executed. Otherwise another new MyAppCache will be created and the stale data will be saved in.

But since singletons do not have a clear owner (because singletons manage their own life cycle), it is very difficult to destroy a singleton.

So at this time you may think, "Then don't make MyAppCache a singleton!" In fact, the problem is that the life cycle of an object may not be well determined in the early stage of the project. If you assume that the life cycle of an object will match the life cycle of the entire program, this will greatly limit the scalability of the code, which will be very painful when product requirements change.

So everything above is to clarify a point of view: "Singletons should only maintain a global state, and the life cycle of that state is consistent with the life cycle of the program." Critical review is required for singletons that already exist in the program.

Not conducive to testing

The original text of this part is mentioned in the previous chapter, but I think testing is a very important part in software development, so I will open a separate chapter on this part and add some personal insights.

Since singletons have been alive throughout the life cycle of the app, even when the test is executed, this leads to the fact that one test may affect another test, which is a big taboo in unit testing.
Therefore, it is necessary to effectively destroy a singleton when performing unit testing and maintain the singleton thread-safe characteristics. But in the above I mentioned:

"But it is very difficult to destroy a singleton because there is no clear owner for a singleton (because the singleton manages its own lifecycle).

It seems that the two are contradictory. In fact, it is not the case. You can choose to simplify singletons. Instead of having various singletons, it is better to have only one "real" singleton ServiceRegistry and refer other "potential" singletons to the ServiceRegistry. In this way, other singletons have an owner, which can destroy singletons in time when unit testing, ensuring the independence of unit testing.

On the other hand, the existence of ServiceRegistry makes other "singletons" no longer singletons, which makes it easier for mocks when TDD.

in conclusion

We all know that global mutable state is bad, but when using singletons, we inadvertently turn it into the global mutable state we hate.
In object-oriented programming, we need to minimize the scope of variable states as much as possible, and singletons run counter to this idea. I hope that when using singletons next time, we can think more about whether this variable is truly worthy of becoming a singleton. If not, please use "dependency injection mode" instead.

Translated and modified from

Original link:Avoiding Singleton Abuse

Summarize

The above is the entire content of this article. I hope that the content of this article has certain reference value for your study or work. Thank you for your support.