Developers of frameworks have to be more careful than other developers in how they write their code. Many client applications could link in their framework and, because of this wide exposure, any deficiencies in the framework might be magnified throughout a system. The following items discuss programming techniques you can adopt to ensure the efficiency and integrity of your framework.
Note: Some of these techniques are not limited to frameworks. You can productively apply them in application development.
The following suggestions and recommendations cover framework initialization.
The initialize
class method gives you a place to have some code executed once, lazily, before any other method of the class is invoked. It is typically used to set the version numbers of classes (see “Versioning and Compatibility”).
The runtime sends initialize
to each class in an inheritance chain, even if it hasn’t implemented it; thus it might invoke a class’s initialize
method more than once (if, for example, a subclass hasn’t implemented it). Typically you only want the initialization code to be executed only once. One way to ensure this happens is to perform the following check:
if (self == [NSFoo class]) { |
// the initializing code |
} |
You should never invoke the initialize
method explicitly. If you need to trigger the initialization, invoke some harmless method, for example:
[NSImage self]; |
A designated initializer is an init
method of a class that invokes an init
method of the superclass. (Other initializers invoke the init
methods defined by the class.) Every public class should have one or more designated initializers. As examples of designated initializers there is NSView’s initWithFrame:
and NSResponder’s init
method. Where init
methods are not meant to be overridden, as is the case with NSString and other abstract classes fronting class clusters, the subclass is expected to implement its own.
Designated initializers should be clearly identified because this information is important to those who want to subclass your class. A subclass can just override the designated initializer and all other initializers will work as designed.
When you implement a class of a framework, you often have to implement its archiving methods as well: initWithCoder:
and encodeWithCoder:
. Be careful not to do things in the initialization code path that doesn’t happen when the object is unarchived. A good way to achieve this is to call a common routine from your designated initializers and initWithCoder:
(which is a designated initializer itself) if your class implements archiving.
A well-designed initialization method should complete the following steps to ensure the proper detection and propagation of errors:
Reassign self by invoking super
's init
method.
Check the returned value for nil
, which indicates that some error occurred in the superclass initialization.
If an error occurs while initializing the current class, free the object and return nil
.
Listing 1 illustrates how you might do this.
Listing 1 Error detection during initialization
- (id)init { |
if ((self = [super init]) != nil) { // call a designated initializer here |
// initialize object ... |
if (someError) { |
[self release]; // [self dealloc] or [super dealloc] might be |
self = nil; // better if object is malformed |
} |
} |
return self; |
} |
When you add new classes or methods to your framework, it is not usually necessary to specify new version numbers for each new feature group. Developers typically perform (or should perform) Objective-C runtime checks such as respondsToSelector:
to determine if a feature is available on a given system. These runtime tests are the preferred and most dynamic way to check for new features.
However, you can employ several techniques to make sure each new version of your framework are properly marked and made as compatible as possible with earlier versions.
When the presence of a new feature or bug fix isn’t easily detectable with runtime tests, you should provide developers with some way to check for the change. One way to achieve this is to store the exact version number of the framework and make this number accessible to developers:
Document the change (in a release note, for instance) under a version number.
Set the current version number of your framework and provide some way to make it globally accessible. You might store the version number in your framework’s information property list (Info.plist
) and access it from there.
If the objects of your framework need to be written to nib file, they must be able to archive themselves. You also need to archive any documents that use the archiving mechanisms to store document data. For archiving, you can use the “old style” (initWithCoder:
and encodeWithCoder:
); but, for better compatibility with past, current, and future versions of your framework, you should use the keyed archiving mechanism.
Keyed archiving lets objects read and write archived values with keys. This approach gives you more flexibility in both backwards and forwards compatibility than the old archiving mechanism, which requires that code always maintain the same order for values read and written. Old-style archiving also does not have a good way to change what has been written out. For more information on keyed archiving, see Archives and Serializations Programming Guide.
Use keyed archiving for your new classes. If your previously released classes use the old style of archiving, you don’t need to do anything. Objects that implemented old archiving prior to Mac OS X version 10.2 need to be able to read and write their contents from and to old archives. However, if you add new attributes in Mac OS X v10.2 and later, you don’t have to store them in old archives, and in fact you shouldn’t (because this might render the old archives unreadable on older systems). You should switch to using keyed archiving for new attributes.
You should be aware of certain facts about keyed archiving:
If a key is missing in an archive, asking for its value will return nil
, NULL
, NO
, 0, or 0.0, depending on the type being asked for. Test for this return value to reduce the data that you write out. In addition, you can find out whether a key was written to the archive.
With old-style archiving, the burden of compatibility fell on the implementation of initWithCoder:
. With keyed archiving, both the encode and decode methods can do things to ensure compatibility. For instance, the encode method of a new version of a class might write new values using keys but can still write out older fields so that older versions of the class can still understand the object. In addition, decode methods might want to deal with missing values in some reasonable way to maintain some flexibility for future versions.
A recommended naming convention for archive keys for framework classes is to begin with the prefix used for other API elements of the framework and then use the name of the instance variable. Just make sure that names cannot conflict with the names of any superclass or subclass.
If you have a utility function that writes out a basic data type (in other words, a value that isn’t an object), be sure to use a unique key. For example, if you have an “archiveRect” routine that archives a rectangle should take a key argument, and either use that; or, if it writes out multiple values (for instance, four floats), it should append its own unique bits to the provided key.
Archiving bitfields as-is can be dangerous due to compiler and endianness dependencies. You should archive them only when, for performance reasons, a lot of bits need to be written out, many times. See “Bitfields” for a suggestion.
Each Objective-C object has a size that can be determined by the total size of its own instance variables plus the instance variables of all superclasses. You cannot change the size of a class without requiring the recompilation of subclasses that also have instance variables. To maintain binary compatibility, you usually cannot change object sizes by introducing new instance variables into your classes or getting rid of unneeded ones.
So, for new classes, it's a good idea to leave a few extra “reserved” fields for future expansion. If there are going to be few instances of a class this is clearly not an issue. But for classes instantiated by the thousands, you might want to keep the reserved variable small (say, four bytes for an arbitrary object).
For older classes whose objects have run out of room (and assuming the instance variables were not exported as public), you can move instance variables around, or pack them together in smaller fields. This rearranging may allow you to add new data without growing the total object size. Or you can treat one of the remaining reserved slots as a pointer to an additional block of memory, which the object allocates as it is initialized (and deallocates as it is released). Or you can put the extra data into an external hash table (such as a NSDictionary); this strategy works well for instance variables that are seldom created or used.
Most Cocoa framework methods do not force developers to catch and handle exceptions. That is because exceptions are not raised as a normal part of execution, and are not typically used to communicate expected runtime or user errors. Examples of these errors include:
File not found
No such user
Attempt to open a wrong type of document in an application
Error in converting a string to a specified encoding
However, Cocoa does raise exceptions to indicate programming or logic errors such as the following:
Array index out of bounds
Attempt to mutate immutable objects
Bad argument type
The expectation is that the developer will catch these kinds of errors during testing and address them before shipping the application; thus the application should not need to handle the exceptions at runtime. If an exception is raised and no part of the application catches it, the top-level default handler typically catches and reports the exception and execution then continues. Developers can choose to replace this default exception-catcher with one that gives more detail about what went wrong and offers the option to save data and quit the application.
Errors are another area where Cocoa frameworks differ from some other software libraries. Cocoa methods generally do not return error codes. In cases where there is one reasonable or likely reason for an error, the methods rely on a simple test of a boolean or object (nil
/non-nil
) returned value; the reasons for a NO
or nil
returned value are documented. You should not use error codes to indicate programming errors to be handled at runtime, but instead raise exceptions or in some cases simply log the error without raising an exception.
For instance, NSDictionary’s objectForKey:
method either returns the found object or nil
if it can’t find the object. NSArray’s objectAtIndex:
method can never return nil
(except for the overriding general language convention that any message to nil
results in a nil
return), because an NSArray object cannot store nil
values, and by definition any out-of-bounds access is a programming error that should result in an exception. Many init
methods return nil
when the object cannot be initialized with the parameters supplied.
In the small number of cases where a method has a valid need for multiple distinct error codes, it should specify them in a by-reference argument that returns either an error code, a localized error string, or some other information describing the error. For example, you might want to return the error as an NSError object; look at the NSError.h
header file in Foundation for details. This argument might be in addition to a simpler BOOL
or nil
that is directly returned. The method should also observe the convention that all by-reference arguments are optional and thus allow the sender to pass NULL
for the error-code argument if they do not wish to know about the error.
How you handle framework data has implications for performance, cross-platform compatibility, and other purposes. This section discusses techniques involving framework data.
For performance reasons, it is good to mark as constant as much framework data as possible because doing so reduces the size of the __DATA
segment of the Mach-O binary. Global and static data that is not const
ends up in the __DATA
section of the __DATA
segment. This kind of data takes up memory in every running instance of an application that uses the framework. Although an extra 500 bytes (for example) might not seem so bad, it might cause an increment in the number of pages required—an additional four kilobytes per application.
You should mark any data that is constant as const
. If there are no char *
pointers in the block, this will cause the data to land in the __TEXT
segment (which makes it truly constant); otherwise it will stay in the __DATA
segment but will not be written on (unless prebinding is not done or is violated by having to slide the binary at load time).
You should initialize static variables to ensure that they are merged into the __data
section of the __DATA
segment as opposed to the __bss
section. If there is no obvious value to use for initialization, use 0, NULL
, 0.0, or whatever is appropriate.
Using signed values for bitfields, especially one-bit bitfields, can result in undefined behavior if code assumes the value is a boolean. One-bit bitfields should always be unsigned. Because the only values that can be stored in such a bitfield are 0 and -1 (depending on the compiler implementation), comparing this bitfield to 1 is false. For example, if you come across something like this in your code:
BOOL isAttachment:1; |
int startTracking:1; |
You should change the type to unsigned int
.
Another issue with bitfields is archiving. In general, you shouldn’t write bitfields to disk or archives in the form they are in, as the format might be different when they are read again on another architecture, or on another compiler.
In framework code, the best course is to avoid allocating memory altogether, if you can help it. If you need a temporary buffer for some reason, it’s usually better to use the stack than to allocate a buffer. However, stack is limited in size (usually 512 kilobytes altogether), so the decision to use the stack depends on the function and the size of the buffer you need. Typically if the buffer size is 1000 bytes (or MAXPATHLEN
) or less, using the stack is acceptable.
One refinement is to start off using the stack, but switch to a malloc
’ed buffer if the size requirements go beyond the stack buffer size. Listing 2 presents a code snippet that does just that:
Listing 2 Allocation using both stack and malloc’ed buffer
#define STACKBUFSIZE (1000 / sizeof(YourElementType)) |
YourElementType stackBuffer[STACKBUFSIZE]; |
YourElementType *buf = stackBuffer; |
int capacity = STACKBUFSIZE; // In terms of YourElementType |
int numElements = 0; // In terms of YourElementType |
while (1) { |
if (numElements > capacity) { // Need more room |
int newCapacity = capacity * 2; // Or whatever your growth algorithm is |
if (buf == stackBuffer) { // Previously using stack; switch to allocated memory |
buf = malloc(newCapacity * sizeof(YourElementType)); |
memmove(buf, stackBuffer, capacity * sizeof(YourElementType)); |
} else { // Was already using malloc; simply realloc |
buf = realloc(buf, newCapacity * sizeof(YourElementType)); |
} |
capacity = newCapacity; |
} |
// ... use buf; increment numElements ... |
} |
// ... |
if (buf != stackBuffer) free(buf); |
The following items discuss issues related to Objective-C, including protocols, object comparison, and when to send autorelease
to objects.
In Objective-C, it is valid to send a message to a nil
object. The Objective-C runtime assumes that the return value of a message sent to a nil
object is nil
, as long as the message returns an object or any integer scalar of size less than or equal to sizeof(void*)
.
On Intel-based Macintosh computers, messages to a nil
object always return 0.0
for methods whose return type is float
, double
, long double
, or long long
. Methods whose return value is a struct
, as defined by the Mac OS X ABI Function Call Guide to be returned in registers, will return 0.0
for every field in the data structure. Other struct data types will not be filled with zeros. This is also true under Rosetta. On PowerPC Macintosh computers, the behavior is undefined.
You should be aware of an important difference between the generic object-comparison method isEqual:
and the comparison methods that are associated with an object type, such as isEqualToString:
. The isEqual:
method allows you to pass arbitrary objects as arguments and returns NO
if the objects aren’t of the same class. Methods such as isEqualToString:
and isEqualToArray:
usually assume the argument is of the specified type (which is that of the receiver). They therefore do not perform type-checking and consequently they are faster but not as safe. For values retrieved from external sources, such as an application’s information property list (Info.plist
) or preferences, the use of isEqual:
is preferred because it is safer; when the types are known, use isEqualToString:
instead.
A further point about isEqual:
is its connection to the hash
method. One basic invariant for objects that are put in a hash-based Cocoa collection such as an NSDictionary or NSSet is that if [A isEqual:B] == YES
, then [A hash] == [B hash]
. So if you override isEqual:
in your class, you should also override hash
to preserve this invariant. By default isEqual:
looks for pointer equality of each object’s address, and hash
returns a hash value based on each object’s address, so this invariant holds.
In your methods and functions that return object values, make sure that you return these values autoreleased
unless they are object-creation or object-copy methods (new
, alloc
, copy and their variants). “Autoreleased” in this context does not necessarily mean the object has to be explicitly autoreleased—that is, sending autorelease
to the object just before returning it. In a general sense, it simply means the return value is not freed by the caller.
For performance reasons, it’s advisable to avoid autoreleasing objects in method implementations whenever you can, especially with code that might be executed frequently within a short period; an example of such code would be a loop with unknown and potentially high loop count. For instance, instead of sending the following message:
[NSString stringWithCharacters:] |
Send the following message:
[[NSString alloc] initWithCharacters:] |
And explicitly release the string object when you are finished with it. Remember there are times, however, when you need to send autorelease
to objects, as when returning such objects from a function or method.
An important question is what is the right thing to do in accessor methods. For instance, if you return an instance variable directly in a get method, and the set method is called right away, freeing the previous value might be dangerous because it might free the value you returned earlier. The guideline for Cocoa frameworks has been to implement set methods to autorelease previous value, unless there are situations in which the set method in question can be called very often, such as in tight loops. In practice this is rarely the case except for some low-level objects. In addition, generic collections such as NSAttributedString, NSArray, and NSDictionary never autorelease objects, mainly to preserve object life times. Instead they simply retain and release their objects. They also should document this fact so that the client is aware of the behavior.
For framework code now being written, the recommendation is to autorelease objects in the get methods, as this is the safest route:
- (NSString *)title { |
return [[instanceVar retain] autorelease]; |
} |
- (void)setTitle:(NSString *)newTitle { |
if (instanceVar != newTitle) { |
[instanceVar release]; |
instanceVar = [newTitle copy]; |
// or retain, depending on object & usage |
} |
} |
One more consideration in set methods is whether to use copy
or retain
. Use copy
if you are interested in the value of the object and not the actual object itself. A general rule of thumb is to use copy
for objects which implement the NSCopying protocol. (You wouldn’t do this check at runtime. just look it up in the reference documentation.) Typically value objects such as strings, colors, and URLs, should be copied; views, windows, and so on should be retained. For some other objects (arrays, for instance), whether to use copy
or retain
depends on the situation.
Last updated: 2010-05-05