原文:http://www.duckrowing.com/2010/05/21/using-the-singleton-pattern-in-objective-c/
感谢作者
what the purpose of the +allocWithZone override is?
+allocWithZone is overridden to make sure you can only allocate a single instance of this object. All subsequent calls should return nil.
I don't think you'll have a situation where you initialize an object more than once with this implementation. The second call to alloc will return nil, not the object, so you'll be calling [nil init].
If you’re a working programmer, you’ve likely used or at least heard of design patterns. A design pattern is a solution to a design problem. It can be a small problem such as figuring out how two objects might collaborate effectively, or a larger problem such as how we should isolate layers in an enterprise application.
If you work with Cocoa, you’re working with some design patterns already, though these are so ingrained in the API you may not think of them as such. The most common one is the Delegate pattern. We use delegates all over the Cocoa API to get around having to extend built in classes, most of which have to do with creating UI elements. Instead of overriding UITextField in an iPhone application, we can assign the field a delegate object, allowing us to write only the minimal amount of custom behavior we need in some other class. When there’s a customization point in the program, an object will notify the delegate allowing us to take action or to send advice back to the calling object.
One very common pattern that doesn’t get discussed a lot in Cocoa circles, but I think is very useful, is the Singleton pattern. A singleton is a class for which there exists only a single instantiated object. This object is generally used to model a shared resource. The Cocoa API uses the singleton pattern though it may not be obvious if you’re not used to making your own singletons. The NSApplication and UIApplication classes implement the singleton pattern. If you want to interact with the application object, you need to get an instance to the single object that models the application:
// Some singletons in the CocoaTouch libraries
UIApplication *applicationSingleton = [UIApplication sharedApplication];
UIAccelerometer *accelerometerSingleton = [UIAccelerometer sharedAccelerometer];
In an iPhone application, there’s conceptually only one UIApplication instance – the application that’s currently executing. There’s only one UIAccelerometer instance – the one that governs access to the accelerometer on the device. In idiomatic Cocoa programming, we never instantiate a singleton object, we ask the class for a reference to the singleton, and the method name that gives it to us usually starts with the word “shared” to imply that it is a singleton instance.
Creating singletons in Objective-C
In some modern languages, support for the Singleton pattern is baked into the standard libraries or even into the language itself. Take for example this Ruby singleton:
require 'singleton'
class RadioTuner
include Singleton
def set_station(station)
@current_station = station
end
def current_station
@current_station
end
end
tuner = RadioTuner.instance
tuner.set_station(99.9)
puts tuner.current_station
The singleton module is available in the ruby standard library.
The Scala language takes this one step further by supporting singletons directly in the language:
object RadioTuner { private var _currentStation: Float = 0.0f; def setStation(s: Float) = synchronized { _currentStation = s } def currentStation: Float = synchronized { _currentStation; } } RadioTuner.setStation(99.9f); println(RadioTuner.currentStation);
In Scala, we use the keyword “object” to declare a singleton object. Scala goes as far as banning static members of classes from language. In the interest of object oriented purity, you need to use the singleton pattern.
While many of us may be new to Objective-C, it is not a new language. Unlike relative newcomers like Ruby and Scala, Objective-C doesn’t bake the singleton pattern into the language. In Objective-C we have to take care of the details of implementing the singleton ourselves. We need to create and manage the only instance of our singleton class, and we need to guarantee that there can be only one instance of the class in a running program.
Let’s assume that we had some hardware tuner in our computer that we wanted to expose as a shared singleton object. Let’s start by making a new Command Line Tool in Xcode:
Name the project “Singleton” or whatever makes you happy.
Let’s dive right in and define our singleton. Add a new Objective-C class to the project and name it “RadioTuner”. This will represent our imaginary radio tuner. If we look at our Scala and Ruby implementations, we see that we need to be able to do three things with our singleton:
- Get a reference to the singleton
- Set the desired station
- Get the current station
With that in mind, let’s create the header:
#import
/**
This class simulates access to a hardware radio tuner. In our example
we assume that our computer has a single radio tuner that must be shared.
*/
@interface RadioTuner : NSObject {
NSDecimalNumber *currentStation;
}
/**
Returns an instance to the shared singleton which governs access to the
hardware radio tuner.
@returns The shared tuner object
*/
+ (RadioTuner *)sharedTuner;
/**
Sets the tuner to the desired station
@param station the new station frequency
*/
- (void)setStation:(NSDecimalNumber *)station;
/**
Get the current station
@returns returns the current statiion to which the tuner is tuned.
*/
- (NSDecimalNumber *)currentStation;
@end
Following Cocoa naming conventions, we’re declaring a class method named +sharedTuner. This will allow us to get a reference to the singleton object. The -setStation: and -currentStation methods will be called on the singleton itself to allow use to change and access the state of the tuner.
To store the currentStation, we’ll use an instance of NSDecimalNumber.
This is all we need for the header. For the real meat of things, we need to look at how we implement this. Most of the work is will be in the +sharedTuner method and methods that support the singleton behavior. We need to guarantee that when code calls [RadioTuner sharedTuner] we always return the same object, and that this is the only way possible to get a reference to a RadioTuner object.
To start off, we need a way to store the shared instance. Remember that since this is a singleton, we need to have a instance of the object to hand out. In many languages we’d use a class attribute. Objective-C doesn’t have this concept, so we have to make our own fun.
In RadioTuner.m, add the following line to create a static variable to hold our shared tuner instance:
#import "RadioTuner.h"
static RadioTuner *sharedInstance = nil;
@implementation RadioTuner
...
There’s nothing magic here; this is your basic C language static variable declaration. A static variable lives for the scope of the entire program.
Now we write the +sharedTuner method:
+ (RadioTuner *)sharedTuner
{
@synchronized (self) {
if (sharedInstance == nil) {
[[self alloc] init];
}
}
return sharedInstance;
}
There are a few things to notice here. First, this method is synchronized. The reason for this is pure paranoia. We don’t want to find ourselves in a situation where more than one thread is calling this method at the same time. It’s possible, however remote, that we could end up with more than one instance of our singleton which is bad. This is significantly more paranoid than Apple’s advice on creating singletons and The Big Nerd Ranch’s method (seeiPhone Programming, 1st ed. page 186). My assumption is that in most cases you can get away without the synchronization, but I’m going to play paranoid in this example.
The second odd thing about this method is we never assign a value to the sharedInstance variable! This seems counter intuitive, but there is a good reason for this. To illustrate why things work this way, let’s take a look at the next method we need to implement, +allocWithZone:
+ (id)allocWithZone:(NSZone *)zone
{
@synchronized(self) {
if (sharedInstance == nil) {
sharedInstance = [super allocWithZone:zone];
return sharedInstance;
}
}
return nil;
}
First off, we’re not overriding +alloc, we’re overriding +allocWithZone:. This is because +alloc will call +allocWithZone: behind the scenes. When we allocate the object, we’re requesting memory from the system for our object. When we call +allocWithZone:, we’re asking for memory with a specific zone. With out getting into too much detail, a zone is a range of virtual memory. The idea behind using zones is to allow us to group related objects in the same virtual memory page in an attempt to reduce page thrashing. For the more curious, you can check out Apple’s documentation on memory zones.
What we care about here is that our implementation of +allocWithZone: only allocates memory if sharedInstance is nil. If sharedInstance has already been set, we return nil. What this means is that only the first call to [RadioTuner alloc] will allocate memory. All subsequent calls will return nil. This guarantees that if some piece of code attempts to manually allocate an instance, it will likely fail. In the case that the code is the first call to +alloc, it will get a reference to the sharedInstance.
Basically, this implementation guarantees that you can only ever instantiate one copy of RadioTuner. Because we need to be defensive about allocation, this is where the assignment of the sharedInstance variable needs to occur.
This is a good start, but we’re not out of the woods yet. We need to override -copyWithZone: in case we ever try to make a copy of our singleton instance:
- (id)copyWithZone:(NSZone *)zone
{
return self;
}
Notice that our implementation doesn’t make a copy at all; it returns itself. We do this because the singleton pattern dictates that there can only ever be one instance. So you can try to copy the object, but you’re only ever going to get a reference to the same object.
We’re almost done with the boilerplate code that will support the singleton pattern. There is one more problem to consider. Assuming we’re working with reference counting, we need to make sure that the Objective-C runtime never deallocates our object. After all, a singleton models some shared resource that lives for the scope of our application. Imagine if you were able to deallocate the shared NSApplication instance – it doesn’t make sense.
Let’s override some methods to protect our object from deallocation:
- (id)retain
{
return self;
}
- (void)release
{
// do nothing
}
- (id)autorelease
{
return self;
}
- (NSUInteger)retainCount
{
return NSUIntegerMax; // This is sooo not zero
}
If you’re not familiar with retain counts, you should check out Apple’s documentation on basic memory management, or you can take a look at my previous post on memory management.
Our implementation of -retain doesn’t alter the retain count of the object. Also, -release doesn’t decrement the retain count, so it’s not possible to reduce it to zero (which would cause deallocation). Our implementation of -autorelease maintains the external behavior, but it does not add our object to an autorelease pool. Finally we have our implementation of -retainCount which returns the biggest integer we can model on the hardware. On my machine, this returns 4,294,967,295. That’s really really not zero. This protects our singleton from ever being deallocated.
I suspect in practice that this is overkill. Apple’s documentation suggests we override all of these methods, though Conway and Hillegas advise us to only override -release. My advice is to be paranoid. It doesn’t hurt to override these methods.
Now that we’ve guaranteed that we can only create a single instance of our class that can never be deallocated, we can implement the actual logic of our make believe radio tuner:
- (id)init
{
@synchronized(self) {
[super init];
currentStation = [[NSDecimalNumber alloc] initWithString:@"0.0"];
return self;
}
}
- (void)setStation:(NSDecimalNumber *)station
{
@synchronized(self) {
if (currentStation != station) {
[currentStation release];
currentStation = [station retain];
}
}
}
- (NSDecimalNumber *)currentStation
{
@synchronized(self) {
return currentStation;
}
}
First off we have a basic -init method. This sets up a default value. -setStation: and -currentStation are fairly straight forward. Technically we could have skipped implementing -setStation: and -currentStation and synthesized them as Objective-C 2.0 properties, but I’ve included implementations so you could see the details. Notice that all of these methods are synchronized. I recommend synchronizing any method in your singletons that accesses or alters shared state unless you don’t have thread safety as a goal.
One last thing to note is that there is no -dealloc method. Since we’ve gone through all the trouble of making sure -dealloc will never be called, we don’t get anything from implementing the method. Our singleton object will be evicted from memory when the program terminates. This presents something of a problem for cleanup. In general, you can never count on using -dealloc for cleanup as it might not ever be called, not just for our singleton. When a program terminates, there is no guarantee that every remaining object will get a -dealloc message; it’s much more efficient for the operating system to evict them from memory and go on its way. So, if your singleton needs to be cleaned up, you should create a method to do that cleanup operation and make sure you invoke it when your application quits. For example, if you were writing an Mac OS X application, you could implement the -applicationShouldTerminate: method in your NSApplication delegate and call the cleanup method.
To make use of our new singleton, add the following code to Singleton.m:
#import "RadioTuner.h"
int main (int argc, const char * argv[]) {
NSAutoreleasePool * pool = [[NSAutoreleasePool alloc] init];
RadioTuner *tuner = [RadioTuner sharedTuner];
NSDecimalNumber *station = [NSDecimalNumber decimalNumberWithString:@"99.9"];
[tuner setStation:station];
NSLog(@"%@", [tuner currentStation]);
[pool drain];
return 0;
}
Run the code, and you’ll see our current station printed to the console. Perhaps this isn’t the most exciting singleton, but you can build any singleton you need using this technique.
Click here to download the completed project.
Eager vs. Lazy Singletons
We can further differentiate singletons as either eager or lazy. An eager singleton is one that is instantiated before your first use of it. A lazy singleton is not instantiated until your first request to use it. We’ve implemented a lazy singleton. Our singleton instance will not exist until the first call to [RadioTuner sharedTuner].
If for some reason it took a long time to create our singleton instance, we might prefer an eager singleton. People are more forgiving of an application taking a while to load than applications slowing down during use for no apparent reason.
You might think this is a viable way to implement an eager singleton in Objective-C:
#import "RadioTuner.h"
static RadioTuner *sharedInstance = [RadioTuner sharedTuner];
@implementation RadioTuner
Unfortunately we can’t set the initial value of sharedInstance in RadioTuner.m. The compiler will complain that you’re trying to initialize the variable with a non-constant value. Much like we would have to hook into the application lifecycle to cleanup our singleton, we’d also have to eagerly create singleton instances in a similar fashion. The easiest route is to call [RadioTuner sharedTuner] in your application delegate’s -applicationDidFinishLaunching: method or something similar.
When should you use the Singleton pattern?
You want to create a singleton when you are certain that there should only ever be one instance of a particular class and it needs to be accessible by a well-known access point. NSApplication is a great example of this. You can access the shared application from anywhere in a Cocoa program using [NSApplication sharedApplication]. Furthermore, you’d only ever want to model the running application once, and you want access to that model for the entire life of the application. If what you’re attempting to model fits this pattern, then a singleton is a good way to go.
In many respects our application delegate objects acts as a sort of de-facto singleton. Generally we only have one instance of our application delegate class, it lives for the duration of the application, and it’s accessible via [[NSApplication] delegate]. In fact, we tend to put a lot of shared resources into the application delegate simply because it is so easily accessible and there is generally only one instance. The delegate of course is not a true singleton. In fact, it’s quite possible to alter the application’s delegate while the application is executing, though I don’t think that’s common in practice. In my experience, when you find that you’re embedding a lot of shared resources into your application delegate simply because it’s a convenient place to access them, that’s a good place to start thinking about singletons.
While creating singletons in Objective-C might be more involved than in more modern languages, it’s not very difficult. We do need to create some boilerplate code to support the proper behavior, but once you know what’s required and understand what it’s doing, you can get down to the work of implementing the logic of your singletons, and the result is better organized code.
Using the Singleton Pattern in Objective-C Part 2
A while back I described a method for creating singletons in Objective-C. Since I wrote that, Apple has released automatic reference counting. My original example demonstrated how to prevent reference counts for singletons from decreasing. While this is still a valid approach, it would be nice to not have to get involved with reference counting at all. Also, the basic template for a singleton was complicated. In this post I’ll show another method using Grand Central Dispatch.
Before going any further, I strongly suggest you read my first post as I will build upon the example presened there.
Let’s assume we’re implementing the same RadioTuner singleton from the previous post. The interface for the tuner will be the same:
/**
This class simulates access to a hardware radio tuner. In our example
we assume that our computer has a single radio tuner that must be shared
*/
@interface RadioTuner : NSObject {
NSDecimalNumber *currentStation;
}
/**
Returns an instance to the shared singleton which governs access to the
hardware radio tuner.
@returns The shared tuner object
*/
+ (RadioTuner *)sharedTuner;
/**
Sets the tuner to the desired station
@param station the new station frequency
*/
- (void)setStation:(NSDecimalNumber *)station;
/**
Get the current station
@returns returns the current statiion to which the tuner is tuned.
*/
- (NSDecimalNumber *)currentStation;
For the implementation we’re going to use a Grand Central Dispatch queue to ensure that calling the +sharedTuner only creates the singleton instance once:
static RadioTuner *sharedInstance = nil;
@implementation RadioTuner
+ (RadioTuner *)sharedTuner {
static dispatch_once_t onceQueue;
dispatch_once(&onceQueue, ^{
sharedInstance = [[RadioTuner alloc] init];
});
return sharedInstance;
}
@end
Just as before, we’re creating a static variable, sharedInstance, to hold our singleton instance. What differs is that we’re using the dispatch_once function to ensure that we only ever create the singleton once. To do this we must create a static variable of type dispatch_once_t. We pass the dispatch_once function the address of onceQueue and a block to execute. We initialize the sharedInstance inside the block. Grand Central Dispatch guarantees that subsequent calls to +sharedTuner will not invoke this block, so only the first call to +sharedTuner will create an object.
Since we’re using automatic reference counting, we’re not going to bother with implementing -retain, -release, or -autorelease in this example.
Just to make sure this works, we can test that calling +sharedTuner returns the same object:
RadioTuner *tuner = [RadioTuner sharedTuner];
RadioTuner *tuner2 = [RadioTuner sharedTuner];
NSLog(@"%@", tuner);
NSLog(@"%@", tuner2);
When this is executed, we get:
<RadioTuner: 0x1001140e0>
<RadioTuner: 0x1001140e0>
So far so good- calls to +sharedTuner are in fact giving us the same object. We can also use our new infatuation with Grand Central Dispatch to get rid of @synchronize in all of our methods. Instead of using locks we can achieve the same effect using serial queues.
First let’s define a serial queue we can use instead of a lock. A serial queue accepts blocks and ensures that they are executed in a first-in-first-out order.
static RadioTuner *sharedInstance = nil;
static dispatch_queue_t serialQueue;
@implementation RadioTuner
+ (void)initialize {
serialQueue = dispatch_queue_create("com.duckrowing.radiotuner.SerialQueue", NULL);
}
+ (RadioTuner *)sharedTuner {
static dispatch_once_t onceQueue;
dispatch_once(&onceQueue, ^{
sharedInstance = [[RadioTuner alloc] init];
});
return sharedInstance;
}
@end
Since we only need one serial queue for use by the RadioTuner class, I’m declaring it as a static variable. Since we can’t initialize a static variable with a nonconstant value, we need to use the +initialize method to perform the initialization. The +initialize method is guaranteed to be called once before a class is used, so it should be safe to initialize the queue here. Using this queue, we can rewrite the -init, -setStation, and -currentStation methods.
- (id)init {
id __block obj;
dispatch_sync(serialQueue, ^{
obj = [super init];
if (obj) {
currentStation = [[NSDecimalNumber alloc] initWithString:@"0.0"];
}
});
self = obj;
return self;
}
- (void)setStation:(NSDecimalNumber *)station {
dispatch_sync(serialQueue, ^{
if (currentStation != station) {
currentStation = station;
}
});
}
- (NSDecimalNumber *)currentStation {
NSDecimalNumber __block *cs;
dispatch_sync(serialQueue, ^{
cs = currentStation;
});
return cs;
}
Instead of locking code with @syncrhronize we can instead use dispatch_sync with the serial queue. The queue will ensure that blocks get executed one at a time in the order they were called, and using dispatch_sync will have our methods wait until their block has been executed. Essentially we get the same effect as locking; only one thread at a time will be able to execute the code in the block.
Note that in the -init and in -currentStation methods we need to use local variables with the storage qualifier __block so that we can record the results of our actions inside blocks. Code inside of blocks cannot modify local variables outside of those blocks unless we use the __block qualifier.
I’m not sure that using serial queues would be noticeably faster than using locks in this case. Apple’s documentation suggests that in general using a queue has benefits over traditional locks:
For threaded code, locks are one of the traditional ways to synchronize access to resources that are shared between threads. However, the use of locks comes at a cost. Even in the noncontested case, there is always a performance penalty associated with taking a lock. And in the contested case, there is the potential for one or more threads to block for an indeterminate amount of time while waiting for the lock to be released.
Replacing your lock-based code with queues eliminates many of the penalties associated with locks and also simplifies your remaining code. Instead of using a lock to protect a shared resource, you can instead create a queue to serialize the tasks that access that resource. Queues do not impose the same penalties as locks. For example, queueing a task does not require trapping into the kernel to acquire a mutex.
We could stop right here and we would have a working class, but we don’t have a true singleton. While we’ve guaranteed that a single instance is created when calling +sharedTuner, we haven’t guarded against direct calls to +alloc. We can prove this with a simple test:
RadioTuner *tuner = [RadioTuner sharedTuner];
RadioTuner *tuner2 = [RadioTuner sharedTuner];
RadioTuner *tuner3 = [[RadioTuner alloc] init];
NSLog(@"%@", tuner);
NSLog(@"%@", tuner2);
NSLog(@"%@", tuner3);
When this is executed, we get:
<RadioTuner: 0x1001140e0>
<RadioTuner: 0x1001140e0>
<RadioTuner: 0x1001166f0>
The third tuner is a different instance than the first two. You could document that you should only call +sharedTuner, and this would probably be acceptable for private classes, but we can go a step further to prevent the creation of more than one instance of RadioTuner by overriding the +allocWithZone method.
+ (id)allocWithZone:(NSZone *)zone {
static dispatch_once_t onceQueue;
dispatch_once(&onceQueue, ^{
serialQueue = dispatch_queue_create("com.duckrowing.radiotuner.SerialQueue", NULL);
if (sharedInstance == nil) {
sharedInstance = [super allocWithZone:zone];
}
});
return sharedInstance;
}
This implementation of +allocWithZone uses dispatch_once to ensure that block that allocates the singleton instance could only be executed once. All calls to +allocWithZone will always return the same value. Notice that I’ve also moved the initialization of serialQueue out of the +initialize method and moved it to the +allocWithZone method. This is probably a better way to go; we only initialize serialQueue if we actually create an instance of RadioTuner, and we can drop the +initialize method all together.
With this improvement, the previous test now yields the same object regardless of how it was created:
<RadioTuner: 0x1001143a0>
<RadioTuner: 0x1001143a0>
<RadioTuner: 0x1001143a0>
For completeness, here’s the full implementation of the RadioTuner singleton:
#import "RadioTuner.h"
static RadioTuner *sharedInstance = nil;
static dispatch_queue_t serialQueue;
@implementation RadioTuner
+ (id)allocWithZone:(NSZone *)zone {
static dispatch_once_t onceQueue;
dispatch_once(&onceQueue, ^{
serialQueue = dispatch_queue_create("com.duckrowing.radiotuner.SerialQueue", NULL);
if (sharedInstance == nil) {
sharedInstance = [super allocWithZone:zone];
}
});
return sharedInstance;
}
+ (RadioTuner *)sharedTuner {
static dispatch_once_t onceQueue;
dispatch_once(&onceQueue, ^{
sharedInstance = [[RadioTuner alloc] init];
});
return sharedInstance;
}
- (id)init {
id __block obj;
dispatch_sync(serialQueue, ^{
obj = [super init];
if (obj) {
currentStation = [[NSDecimalNumber alloc] initWithString:@"0.0"];
}
});
self = obj;
return self;
}
- (void)setStation:(NSDecimalNumber *)station {
dispatch_sync(serialQueue, ^{
if (currentStation != station) {
currentStation = station;
}
});
}
- (NSDecimalNumber *)currentStation {
NSDecimalNumber __block *cs;
dispatch_sync(serialQueue, ^{
cs = currentStation;
});
return cs;
}
@end
You can download the source here.
So that’s my take on creating singletons with Grand Central Dispatch. There are a lot of different ways of implementing singleton pattern in Objective-C and this is just one of them. Using the method I described in my first post should be just as valid as this one. As I’m still getting the hang of GCD myself, and I always like to hear opinions on best practices, I’d appreciate any feedback in the comments.