Programming Mac OS X with Cocoa for Beginners/Some Cocoa essential principles

From Wikibooks, open books for an open world
Jump to: navigation, search

Previous Page: Objective C, the language and its advantages | Next Page: Building a GUI

We have already seen the basics of Cocoa in action. Last chapter, we talked about inheriting from NSObject, and how we must make sure that we call an object's chain of init methods. Here, we'll go into more detail about this, as it's important. Then, we'll talk about memory management.

Creation[edit]

Initialising an object properly is your responsibility. Unlike C++, there is no automatic constructor method, nor an automatic destructor method. However, we can easily do without these as long as we are aware of a few simple rules. Following the rules is necessary for success in Cocoa.

When we instantiate an object:

id   myObject = [NSString alloc];

All we get back is a blob of uninitialised memory. If we were to use this object now, we'd be very unlikely to get the results we wanted. It could even crash. We MUST call the initialiser method:

id   myObject = [[NSString alloc] init];

Then we'll be OK. You'll see this alloc/init combination all the time. So why doesn't alloc simply call init as part of its internals and be done with it? Wouldn't that save us the time and reduce the chance of forgetting? Yes, except for one problem. Sometimes we need a different version of init, that requires a parameter. Since alloc can't know which one we'd like, it leaves the call to us.

Every object has a version of init that acts as the 'master' init - the one we must call when we make that object. Things are arranged so that any other variations of init call it, so everything works properly. The master init method is called the designated initialiser. In most cases this will be the vanilla 'init' method, so using any of the other variants will call init internally. Where this isn't the case, the documentation should indicate the designated initialiser.

'init' is just an ordinary method call, it has no special status like a C++ constructor. Thus it's your responsibility to write the code in init correctly, making sure you call your super's designated initialiser (or any of its initialiser methods that eventually call through the designated initialiser) first. Once you've done that, you should initialise your own part of the object.

Initialisation is permitted to fail. Your object might want to allocate a large block of memory for example. If there is no memory available, you should cleanup anything you've allocated already, then return nil from your init method. Since nil objects can be called safely, it means that running out of memory (for example) will not crash the program.

Destruction[edit]

When an object is no longer required, its 'dealloc' method will be called. If you allocated anything as part of your initialisation, or operation of your object, you need to override this method and deallocate it. Once done, you should call the super's dealloc method. You will be frequently overriding dealloc, so become familiar with doing it. However, dealloc is rarely called directly. Instead, it is called indirectly when an object is released.

Retain and Release[edit]

Every object that inherits from NSObject implements retain and release. This is a simple memory management technique that relies on reference counting. An object keeps track of how many references there are to it within the program. When there are no more references, the count is zero and it is deallocated. In Cocoa, the reference count is often referred to as the retain count. It's the same thing.

The retain method increments the count, and the release method decrements it. When the count reaches zero, dealloc is called automatically.

When an object is first created, its retain count is set to 1. Thus:

id  myObject = [[NSString alloc] init];

'myObject' has a retain count of 1, because there is only one reference to it.

If we then do:

[myObject release];

It will be immediately deallocated, because the count is decremented from 1 to 0, and dealloc gets called. By calling release, we are telling Cocoa we are no longer interested in this object here, and will not refer to it further.

Some people find retain and release confusing, but the rules are really quite simple. If you create an object, you are responsible for releasing it. If you create a new reference to an existing object, you are responsible for releasing it. You create an object using either alloc, or copy on another object, so any time you use these, you need to be aware of the retain/release responsibility. It is yours. If you forget to release, things will probably work out, but you have a memory leak, which can cause problems down the track.

Let's look at the second part, creating a second reference. Suppose you have an object which is part of another object. We saw this in our earlier example, where our GCHelloView had an NSString as a data member. When we are passed an NSString from outside, we don't know who made it, who is responsible for it - we only know it's not us! However, by storing it as a data member, we are creating a new reference to that same string, so we must retain it. What about any previous string that we had there? Well, we created a reference to it, so we must release it.

 - (void)               setText:(NSString*) text withSize:(int) size
 {
        [text retain];
        [_text release];
        _text = text;
        _size = size;
        [self setNeedsDisplay:YES];
 }

So now we can see what's going on here. We retain 'text' because we are creating a new reference to it, our _text data member, and we are releasing the previous value as we are no longer concerned with it. We don't care where the string came from originally or who made it, we are done with it, so we release it. If someone else is still retaining it, it will continue to exist, but if not, it will be deallocated. Now, what if 'text' and '_text' are in fact the same string? This can easily happen. If we release the old one first, it might get deallocated. The following retain will be acting on a stale reference, which is likely to crash (note, unlike the case of accessing nil, accessing a deallocated object is unsafe). Thus by retaining before we release, we avoid that potential snag. "retain before release" is a common and sensible approach.

Note that the retention of objects is efficient, because only a simple count needs to be incremented. If you were required to make a copy of every object, not only would it be much slower, but you'd have the potential for different copies to get out of step with each other, as well as using up much more memory than necessary.

Autorelease[edit]

What happens when we make an object, but we want to hand it over to someone else? How can we ensure that it stays valid long enough to be retained by the other object, before we release it? Look at this:

id myObject = [[NSString alloc] init];
return myObject;

Does the receiver know that this object needs to be released? If no-one releases this object, then we have a memory leak. To eliminate the burden of releasing, we want to mark it for release, when the receiver has done with it. Autorelease designates the object to be released later, it says 'retain the object until the next memory pool is drained'. If you are not explicitly draining the pool, then you can be assured it the return value will remain for use in the receiver's immediate scope.

id myObject = [[[NSString alloc] init] autorelease];
return myObject;

This code is OK, since the returned object is valid, but responsibility for ownership has been passed to the pool.

When control returns to the main event loop, the pool can be sure that any objects that have an interest in the object will have retained it by then, and so it releases all of the objects it has. Any that have no references will be deleted, any that do have references will become owned by the objects retaining them. Everything remains neat and tidy.

Most factory methods return autoreleased objects. This is entirely sensible, and if you write a factory method, you should do the same. For example:

NSColor* red = [NSColor redColor];

NSColor's redColor class method return an autoreleased colour object. If you want to keep this object, you should retain it. If you just want to use it for a few lines, then you don't need to do anything - it will get deleted automatically later, but only after you've finished.

In practice, retain, release and autorelease are straightforward. Some people seem to find it confusing but perhaps they think it's more complicated than it is. To keep an object you're given, retain it. Objects you make are already retained. To throw it away and refer to it no more, release it. If you write a factory method, autorelease it. If you are simply making use of an object returned by a factory method for a short while, do nothing.

Object values[edit]

What about when a property of your object is another object, and someone else wants to read that property? Should you retain, autorelease, what? No, you should do nothing. You are already retaining it as it's your property. If the caller wants to retain it, that's their business, and autorelease doesn't come into it because it's not a factory method. So simply return the object verbatim. So:

- (NSString*) text
{
   return _text;
}

is absolutely correct.

Note that if the caller did retain it, you are still completely free to release it when you're done with it. What would be an error would be for the caller NOT to retain it if it wanted to use the value for an extended time (being a time longer than the current event). This is not a new rule - if the caller creates a reference to an object, it must retain it. But if all it needs to do is use that object for a short time, within a single function say, it can simply do so and not worry that it will get released by someone else.

Event loops[edit]

We have touched on the event loop without explaining what it is. In a Cocoa application, user activities result in events. These might be mouse clicks or drags, typing on the keyboard, choosing a menu item, and so on. Other events can be generated automatically, for example a timer firing periodically, or something coming in over the network. For each event, Cocoa expects there to be an object or group of objects ready to handle that event appropriately. The event loop is where such events are detected and routed off to the appropriate place. Whenever Cocoa is not doing anything else, it is sitting in the event loop waiting for an event to arrive. (In fact, Cocoa doesn't poll for events as suggested, but instead its main thread goes to sleep. When an event arrives, the OS wakes up the thread and event processing resumes. This is much more efficient than polling and allows other applications to run more smoothly).

Each event is handled as an individual thing, then the event loop gets the next event, and so on. If an event causes an update to be required, this is checked at the end of the event and if needed, and window refreshes are carried out.

Previous Page: Objective C, the language and its advantages | Next Page: Building a GUI