10 iPhone Memory Management Tips

Memory management in the iPhone is a hot topic. And since tonight I’m talking about it on tonight’s monthly meetup of the French-speaking Swiss iPhone Developers group, I might as well share some tips here from my own experience.

I won’t go dive through the basics; I think that Scott Stevenson did a great job in his “Learn Objective-C” tutorial at CocoaDevCentral, from where the image below comes. I’m just going to highlight some iPhone-specific issues here and there, and provide some hints on how to solve them.

learnobjectivec-referencecounting.png

To begin with, some important background information:

  • The iPhone 3G has 128 MB of RAM, but at least half of it might be used by the OS; this might leave as little as 40 MB to your application… but remember: you will get memory warnings even if you only use 3 MB;
  • The iPhone does not use garbage collection, even if it uses Objective-C 2.0 (which can use garbage collection on Leopard, nevertheless);
  • The basic memory management rule is: for every [ alloc | retain | copy ] you have to have a [ release ] somewhere;
  • The Objective-C runtime does not allow objects to be instantiated on the stack, but only on the heap; this means that you don’t have “automatic objects”, nor things like auto_ptr objects to help you manage memory;
  • You can use autorelease objects; but watch out! Since they are not released until their pool is released, they can become de facto memory leaks for you…;
  • The iPhone does not have a swap file, so forget about virtual memory. When there is no more memory, there is no more memory.

Having said this, here’s my list of tips:

  • Respond to Memory Warnings
  • Avoid Using Autoreleased Objects
  • Use Lazy Loading and Reuse
  • Avoid UIImage’s imageNamed:
  • Build Custom Table Cells and Reuse Them Properly
  • Override Setters Properly
  • Beware of Delegation
  • Use Instruments
  • Use a Static Analysis Tool
  • Use NSZombieEnabled

Respond to Memory Warnings

Whatever you do in your code, please do not forget to respond to memory warnings! I can’t stress this much. I have seen application crashes just because the handler methods were not present on the controllers, which means that, even if you do not have anything to clear in your controller, at least do this:

- (void)didReceiveMemoryWarning
{
    [super didReceiveMemoryWarning];
}

And you might as well respond to them on your application delegate, as follows:

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
    [[ImageCache sharedImageCache] removeAllImagesInMemory];
}

For the description of the ImageCache class, continue reading ;)

Or finally, as an NSNotification:

NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
[center addObserver:self
           selector:@selector(whatever:)
               name:UIApplicationDidReceiveMemoryWarningNotification
             object:nil];

Avoid Using Autoreleased Objects

Autoreleasing objects is easy and useful, but on the iPhone you should be careful with it. By default there is an NSAutoreleasePool instance created for you at the beginning of the main() function, but this pool is not cleared up until your application quits! This means that during runtime, your autoreleased objects are de facto memory leaks, since they are retained until the application quits. (please see the comments below; I have experienced better performance when avoiding autoreleased objects, but my understanding of pools is misleading :)

I started getting a better performance from my iPhone apps when I stopped using some methods creating autoreleased objects, for example:

// Instead of
NSString *string = [NSString stringWithFormat:@"value = %d", intVariable];

// use
NSString *string = [[NSString alloc] initWithFormat:@"value = %d", intVariable];
...
[string release];

In version 2.0 of the iPhone OS there was also the problem that some “convenience methods” did not work at all; I’m sure you’ve experienced your application crashing when using NSDictionary’s dictionaryWithObjects:forKeys: and then finding out that a replacing that with initWithObjects:forKeys: made your application run just fine. The NDA did not help at the time!

This does not mean that you can’t use autoreleased objects; you should use them, for example, when you have factory methods returning objects not owned neither by the factory nor by the client calling it. You can also use autorelease pools in loops, when you need to allocate lots of small objects, but remember to release the pool right afterwards:

NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
for (id item in array)
{
    id anotherItem = [item createSomeAutoreleasedObject];
    [anotherItem doSomethingWithIt];
}
[pool release];

Remember to always release the pool in the same context where it was created. CocoaDev has an interesting discussion about using NSAutoreleasePools in loops.

Oh, and please, never release an autoreleased object on the iPhone: your application will crash almost instantly.

Use Lazy Loading and Reuse

If your application consists of several different controllers embedded into each other, defer their instantiation until the last possible moment; this means in practical terms that your init method is minimalistic, and that you do more stuff when you need it; the example below is a typical list + detail layout, using a UITableViewController subclass inside a UINavigationController:

@interface UITableViewControllerSubclass
{
@private
    NSMutableArray *items;
    DetailController *detailController;
    UINavigationController *navigationController;
}
@end
@implementation UITableViewControllerSubclass

#pragma mark -
#pragma mark Constructors and destructors

- (id)init
{
    if (self = [self initWithStyle:UITableViewStylePlain])
    {
        // only basic stuff
        items = [[NSMutableArray] alloc] initWithCapacity:20];
        navigationController = [[UINavigationController alloc]
                                initWithRootViewController:self];
    }
    return self;
}

- (void)dealloc
{
    [items release];
    [detailController release];
    [navigationController release];
    [super dealloc];
}

// ...

#pragma mark -
#pragma mark UITableViewDelegate methods

- (void)tableView:(UITableView *)tableView
        didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    Item *item = [items objectAtIndex:indexPath.row];
    if (detailController == nil)
    {
        detailController = [[DetailController alloc] init];
    }
    detailController.item = item;
    [self.navigationController
        pushViewController:detailController
                  animated:YES];
}

// ...

@end

As you can see in the example above, not only we are creating a DetailController instance only when needed (that is, when the user taps on an item in the UITableView), but we’re reusing it every time the user taps on another cell of the table; this has another benefit: a reduction of object allocation and instantiation, which also helps increasing performance a little bit.

You could also use UIViewController’s viewWillAppear: and viewDidDisappear: methods to perform some kind of lazy loading initialization and release, and if you really need to go further, you could use the UITabBarControllerDelegate’s tabBarController:didSelectViewController: method to load and unload parts of your application from memory, as you need it.

Avoid UIImage’s imageNamed:

Alex Curylo has written an absolutely great article about the problems with UIImage’s imageNamed: static method. It seems (and in my tests this appears to be true) that the iPhone OS (versions 2.0 and 2.1 at least) uses an internal cache for images loaded from disk using imageNamed:, and that in cases of low memory this cache is not cleared up completely (this seems to be corrected with version 2.2, though, but I cannot confirm).

Since I have projects that must run on version 2.0 of the iPhone OS, I have created a UIImage category with the following method:

@implementation UIImage (AKLoadingExtension)

+ (UIImage *)newImageFromResource:(NSString *)filename
{
    NSString *imageFile = [[NSString alloc] initWithFormat:@"%@/%@",
                           [[NSBundle mainBundle] resourcePath], filename];
    UIImage *image = nil;
    image = [[UIImage alloc] initWithContentsOfFile:imageFile];
    [imageFile release];
    return image;
}

@end

The name of the method includes the word “new”, to comply with Objective-C’s naming guidelines, since the object we’re returning to the caller is not autoreleased and has a retain count of 1. The caller is then owner of the UIImage and responsible to release it.

Once I have this UIImage instance, I place it in an image cache with this interface:

#import <Foundation/Foundation.h>

@interface ImageCache : NSObject
{
@private
    NSMutableArray *keyArray;
    NSMutableDictionary *memoryCache;
    NSFileManager *fileManager;
}

+ (ImageCache *)sharedImageCache;

- (UIImage *)imageForKey:(NSString *)key;
- (BOOL)hasImageWithKey:(NSString *)key;
- (void)storeImage:(UIImage *)image withKey:(NSString *)key;
- (BOOL)imageExistsInMemory:(NSString *)key;
- (BOOL)imageExistsInDisk:(NSString *)key;
- (NSUInteger)countImagesInMemory;
- (NSUInteger)countImagesInDisk;
- (void)removeImageWithKey:(NSString *)key;
- (void)removeAllImages;
- (void)removeAllImagesInMemory;
- (void)removeOldImages;

@end

Basically, ImageCache can be configured to have a fixed size in memory, and the images that were added first are removed first. It loads the images from the disk as required, keeping a copy in memory, and as suggested by Alex, you can remove them from memory in case of a warning:

- (void)applicationDidReceiveMemoryWarning:(UIApplication *)application
{
    [[ImageCache sharedImageCache] removeAllImagesInMemory];
}

The complete source code of this ImageCache class, together with some unit tests (thanks to the Google Toolkit for Mac), is available on the Projects section of this blog for you to download and play with.

Build Custom Table Cells and Reuse Them Properly

Remember to always use static NSString identifiers for your cells, which helps the UITableView class to reuse them and reduce memory consumption:

- (UITableViewCell *)tableView:(UITableView *)tableView
         cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    Item *item = [items objectAtIndex:indexPath.row];
    static NSString *identifier = @"ItemCell";

    ItemCell *cell = (ItemCell *)[tableView
          dequeueReusableCellWithIdentifier:identifier];

    if (cell == nil)
    {
        cell = [[[ItemCell alloc] initWithIdentifier:identifier]
                                                        autorelease];
    }
    cell.item = item;
    return cell;
}

I also avoid using NIBs when working with table cells, for performance reasons. I prefer to draw the cells through my own subclasses of UITableViewCell, themselves using overridden setters for their properties, which takes me to the next point.

Override Setters Properly

As I said above, I tend to create my own subclasses of UITableViewCell, providing a simple property through which I change the model class holding the data that the cell is supposed to show. This has the effect of changing all the values of the fields and labels to the values corresponding to those of the model instance.

To do that, I override the setters as follows; for the following class definition…

@interface SomeClass
{
@private
    NSArray *items;
    NSString *name;
    id<someProtocol> delegate;
}

@property (nonatomic, retain) NSArray *items;
@property (nonatomic, copy) NSString *name;
@property (nonatomic, assign) id<someProtocol> delegate;
@end

… I use the following implementation:

@implementation SomeClass

@synthesize items;
@synthesize name;
@synthesize delegate;

- (void)dealloc
{
    [items release];
    [name release];
    delegate = nil;
}

#pragma mark -
#pragma mark Overridden setters

- (void)setItems:(NSArray *)obj
{
    if (obj == items)
    {
        return;     // thanks Marco! (see comment #3 below)
    }
    [items release];
    items = nil;
    items = [obj retain];

    if (items != nil)
    {
        // create the internal structure of the cell
        // if not present, and change the widget values
    }
}

- (void)setName:(NSString *)obj
{
    [name release];
    name = nil;
    name = [obj copy];   // I always copy NSStrings!

    if (name != nil)
    {
        // create the internal structure of the cell
        // if not present, and change the widget values
    }
}

- (void)setDelegate:(id<someProtocol>)obj
{
    // do not retain! This is an "assign" property
    delegate = obj;

    if (delegate != nil)
    {
        // create the internal structure of the cell
        // if not present, and change the widget values
    }
}

@end

Overriding setters properly is important because you might be introducing memory leaks if done wrong. I usually copy all my NSString properties too, as a rule of thumb.

Beware of Delegation

If your code is delegate of some other object which you are about to release, remember to set its delegate property to nil before releasing it; otherwise, the object might “think” that its delegate is still there, and will send a message to an invalid pointer. To see what I’m talking about, consider this code:

@interface SomeClass <WidgetDelegate>
{
@private
    Widget *widget;
}
@end
@implementation SomeClass

- (id)init
{
    if (id = [super init])
    {
        widget = [[Widget alloc] init];
        widget.delegate = self;
    }
    return self;
}

- (void)dealloc
{
    // widget might be retained by someone else!
    widget.delegate = nil;
    [widget release];
    [super dealloc];
}

#pragma mark -
#pragma mark WidgetDelegate methods

- (void)widget:(Widget *)obj callsItsDelegate:(BOOL)value
{
    // and here something happens...
}

@end

SomeClass is delegate of Widget. Widget instances might be retained by someone else, which means that even after the release message in the dealloc method, widget might still be alive and call its delegate; if this variable is not nil, widget will send a message to a non-existent object, which will surely crash your application.

Use Instruments

The “Leaks” instrument is your friend, and you should use it after you write the first line of code. Typically, I launch it every time before doing a checkin of some new code. In Xcode, select “Run / Start with Performance Tool / Leaks” and you’re done. You can use it in the simulator or on your device.

Use a Static Analysis Tool

Use the LLVM/Clang Static Analyzer tool. This amazing tool will catch naming errors (regarding the Objective-C naming conventions) and some hidden memory leaks, which are particularly nasty when using CoreFoundation libraries (Address Book, sound, CoreGraphics, etc). You can add it to your daily build script, it’s very easy to use.

But you must use it. Enough said.

Use NSZombieEnabled

Lou Franco has posted an excellent article about how to use NSZombieEnabled in your development cycle. The idea is to be able to find which messages are being sent to invalid pointers, referencing objects which have been released somewhere in your code. Always remember who’s the owner of your objects, and check for existence elsewhere!

And You?

How about you? What are your tips or best practices you usually use for your iPhone apps? Feel free to share them in the form below.

Update, 2009-01-29: I am overwhelmed with the response and traffic that this post has gotten so far! Yesterday evening I had the pleasure of discussing this subject with the guys of the iPhone Developers Facebook group, and I got interesting remarks from Marco Scheurer from Sen:te (including a comment below), which I’ve added to this post today.

Oh, and by the way, I’ve uploaded the slides here! They have a Creative Commons license, so feel free to use them if you find them useful.

Comments

33 Comments so far. Leave a comment below.
  1. Nice post, cant add anything since you already know about the LLVM/Clang ;-)

    Regarding autorelease pools, they’re not released when the program quits, but in the main loop, same as a MacOSX application.

  2. heh, thank you Stephan for letting me know about the static analysis tool via Twitter ;) I kinda became addicted to it. Thanks also for the remark about the autorelease pools…!

  3. Hi there!

    As discussed today:

    The overriden setters can crash as written: retain first, or use autorelease, or test for object equality before releasing.

    Also:
    [attribute release];
    attribute = nil; // this is a waste of time and space, especially in dealloc.

    Otherwise, nice post!

  4. Mustafa,

    A superb article on memory management for iPhone application. Enough said. ; ) I liked the sound of that.

    I wish i had found it earlier since i had to learn most of these things the hard way : (. If you can find sometime, and update the article with a little detail on using Instruments, Clang and elaborating other points (for newbies) – that would be great (just a suggestion).

  5. Awesome post. I learned some new tricks here.

  6. Can you comment on best practices using your ImageCache code? I have an app with tons of small images locally used all the time, and I also get many random images on demand (but may be used frequently in a session) from the internets… I am trying to figure out the best image caching strategy.

  7. Hi Matt, I’m using the ImageCache class in a project where I have to download profile images from a remote server, but I still use imageNamed: when building the user interface elements (particularly when building UITableViewCell instances manually, with small PNG images here and there).

    I use a mixed approach and so far it works fine so far, and the best is that I can free a bunch of memory at once if needed.

    The ImageCache class also provides a way also to clear images that are older than a certain amount of time (say some days) in order to keep a clean cache.

    Feel free to pass your suggestions too! I’m glad that you found this code useful!

  8. What I have ended up doing is removing the singleton nature of your cache and created two separate ImageCache instances in my app. The first one caches data pics that are downloaded, with a small size of 30 items, and the second is for the gui elements and has 100 spots. All my gui elements fit in this memory so there is no rotation. I only did this because I wanted to be able to free this mem, whereas the internal imageNamed cache does not allow freeing at all.

  9. raj,

    excellent post. easily one of the best i have come across so far. great work, keep it up!. i have bookmarked this at http://www.iphonekicks.com/objectivec/10_iPhone_Memory_Management_Tips

  10. You’ve got a lot of good info in here, however the NSAutoreleasePool information is incorrect.

    Every UIKit-based app has an NSAutoreleasePool that is released every time through the main event loop, not just at the end of the app. It’s not visible in your source code, but UIApplicationMain sets this up as part of the event loop. So there’s no problem using autoreleased objects in code in general. That event loop is on the main thread; if you create other threads, then you must create a pool for each.

    In a tight loop that creates many autoreleased objects, its helpful to create your own pool, as you indicated. However, its often better to put the pool creation and release inside the loop, so that objects are released each time through the loop. If you have some code that autoreleases 100 objects each pass, and loops 100 times, you want to release the pool every pass.

    For more info see “Tuning for Performance and Responsiveness” in the iPhone Application Programming Guide and also the Memory Management Programming Guide for Cocoa (although some parts, such as garbage collection, don’t apply to iPhone OS).

  11. Thanks for the comments Paul! I have modified this post accordingly; however, I must say that I have experienced much better results avoiding autoreleased objects, but maybe it was just my application having some sort of particular problem.

  12. Rich,

    Hi Adrian! Nice article! Well worth the read and I was interested in your version of ImageCache. I downloaded the version of ImageCache from github and when I try to run the unit tests if gives the following error:
    line 23: 31779 Abort trap “$TARGET_BUILD_DIR/$EXECUTABLE_PATH” -RegisterForSystemEvents
    Any ideas on what I else I have to configure to run these tests? I have not run any other unit tests on iPhone software so I’m a totally new to unit tests on this platform.
    Thanks again for the great article! Rich

  13. (sent by David Phillip Oster via e-mail)

    One more tip: if you override UIViewController, and have your subclasses stacked on a UINavBarController, when a memory-low notification occurs, the default behavior of Cocoa Touch is to discard the UIViews of all covered UIViewControllers.
    It is common practice to have IBOutlets that refer to sub-UIViews inside the view. Since the view has been discarded, you are now holding retained pointers to UIViews that will never be visible, and you are doing it right while memory is low. Further, then the top UIViewController is popped, so your UIViewController becomes visible again the nib file will load again, and new UIViews will be attached to your IBOutlets.

    You can fix this by overriding your UIViewController’s setView: and if it is being set to nil, release and set to nil all the IBOutlet sub-views.

  14. Joe,

    Nice work with the ImageCache! I was trying out the caching strategy from the Threaded Flickr Table View example from Stanford’s infamous CS193P course:

    http://www.stanford.edu/class/cs193p/cgi-bin/index.php (see 10-ThreadedFlickrTableView.zip in lecture 10)

    I thought it might be nice to merge the ImageLoadingOperation class and NSOperationQueue usage in this example with the ImageCache in yours. Then we’d have asynchronous URL loading support with a nicer cache – perfect for use with Table Views.

    Now, I know that Apple has an example of this (URLCache), but it looks to be a bit unwieldy and not as self-contained and tidy as what you’re using. Translation: I can understand your code a bit more than theirs. :)

    Before I go reinvent the wheel, has anyone accomplished something similar already (that can be shared)? I’m diving in now, but I figure it doesn’t hurt to ask!

  15. DeathscytheSephiroth,

    I have some question about the delegate.
    I think delegate object should not knowing about the parent object ex. our UITextField delegate object does not know the UITextField

    But in your tips you say that delegate should know the parent object so we can set delegate of that object to nil when delegate object get released

    So I confuse what should I do?

  16. @DeathscytheSephiroth
    Although decoupling is always good, I don’t know why a delegate should not know about the other object… actually that’s why the first parameter of delegate methods is a pointer to the object calling its delegate.
    By the way, otherwise you might run into problems when the object calls a non-nil delegate pointer that has been released. That is a crash, for sure. Having a reference helps you solve that problem.
    Hope this helps!

  17. DeathscytheSephiroth,

    @Adrian
    yeah delegate maybe know about the other object, but should delegate knowing about other delegate by having it as instance variable? or just be the parameter of delegate method?

    I know that having a reference help us to solve the crash problem but It may loose the decoupling?

    It is the trade-off?

  18. @DeathscytheSephiroth If you find other ways to solve the problem without coupling, I’m all ears :)

  19. DeathscytheSephiroth,

    I’ve no other ways now, and try to finding it too. :)

    So if you find the way please tell me. :P

    anyway Thank you very much for your opinion :D

  20. @DeathscytheSephiroth glad to help! Thanks for reading my blog.

  21. Michael Kessler,

    Great article, Adrian!

    About the delegate:
    There are many different situations – in some you could have a reference to the other object inside the delegate and in others you shouldn’t.
    I think that in most cases the delegate shouldn’t have a reference to the other object – actually, this is why usually the caller object sends its reference to the delegate. Sometimes I have a single delegate for few objects. Sometimes I change the delegate of the object – in this case I don’t want to set nil to the delegate of the object once some old delegate object dies…

    About overriding the setters:
    If you override the setter then why do you need the property? The only benefit left is the getter (its code is much shorter than the “@property …” and the “@synthesize …”).

    About copying the strings (instead of retaining):
    Why is it better? Doesn’t it increase the objects allocations amount?

  22. Hi Michael,

    Thanks for your comments! Here some more for you:

    1) Retaining delegates: I try doing it to avoid cross-references and, thus, retain cycles; AFAIK there is only one exception to this rule, which is NSURLConnection’s delegate. Check out this Cocoa with Love article which explains the problem in great detail. But as you say, there could be situations where retaining delegates is not only useful, but required.

    2) Overriding setters: I prefer to do it to make the interface of the code more explicit about what properties are exposed. Not needed, just self-documenting, even if overridden. I prefer explicit code.

    3) Retain vs. Copy NSStrings: this is an old discussion, going back to the times of NeXTSTEP. I will let the experts talk, like Helge Hess in this 1999 thread message and this Stack Overflow thread response, both explaining why you should copy objects implementing the NSCopying protocol, instead of retaining

    Hope this helps! Thanks again for your comments!

  23. @Michael by the way, it seems that copying NSStrings does not really “copy” the object; that only happens with NSMutableString objects. Which makes it safe and fast most of the time.

  24. Thanks for this really informative summary on most (if not all) memory management tips for iPhone development, really helps the beginning programmer on tackling the different quirks that have to be taken into consideration with the iPhone.

  25. sam,

    Why do you write about stuff that you admit you don’t really understand? And then once you realize that you’ve written something wrong, why don’t you correct it in the main article instead of simply crossing it out and refering to the comment as in the folllowing –

    ” but this pool is not cleared up until your application quits! This means that during runtime, your autoreleased objects are de facto memory leaks, since they are retained until the application quits. (please see the comments below; I have experienced better performance when avoiding autoreleased objects, but my understanding of pools is misleading :)

    Your understanding it not misleading — your understanding is WRONG!
    Any book on iPhone development tells you when the autoreleased pools are cleared.

    Once you’ve made one mistake how can I trust your other tips?

  26. @sam you’re right, I should stop writing blog posts altogether, and instead leave infuriating comments on other people’s blogs.
    Let’s begin by the basic concept: this blog is part of a bigger learning mechanism. That’s why I edited the post to reference the comments below (without removing my own mistakes), because constructive comments help me (and my polite readers) to learn and understand.
    This blog is not an encyclopaedia about Cocoa, and I never claimed it to be such a thing. If you don’t trust what I say, well, what can I say. Since you’re so enlightened about autorelease pools, you could maybe explain your own experience; otherwise, your comment reflects a self-awareness similar to that of Jean-Claude Van Damme, if you see what I mean.

  27. Sean,

    Sam,
    Great work…and pay no attention to people who live their lives breaking other peoples work apart rather than helping to build them up. I am relatively new to xCode so I was hoping that you could enhance your section on lazy-loading or point me to where I could find an example that did not use a subview. I am working with large tables 10k records – at present I am loading them into the view from a sqlite table because I have to search the tableview so the user can narrow and make a selection… You get the suggest looking to get better at this.

  28. Sergio,

    For better performance don’t use UITextField in XIBs. And create them by yourself.

  29. Could you elaborate why you think it would be any use to implement didReceiveMemoryWarning the way you state here? Calling super and doing nothing else is exactly the same as not implementing it. If it’s not implemented *at all* your call to super will fail (and will cause a compile-time warning).

    So, for all I know, this is pointless and wrong. Do you have any referenceson the web somewhere that indicate that the OS actually looks only at the subclass’s method list and makes you crash if it’s not implemented there, even though it’s implemented farther up in the hierarchy?

    • Thanks for your comment Uli.

      This article was written back in the times of iPhone OS 2.x, and back then (I don’t know if it’s still the case now) I could avoid application crashes just by adding this stub code on my UIViewController instances, as if the runtime was asking them some kind of “respondsToSelector” question at some point. And apparently I wasn’t the only who saw that (http://akos.ma/5nc). This might have changed since the release of iPhone OS 3.x. I don’t know. I still add the code (sometimes, of course, with more extended behaviour).

      By the way, “for all I know”, your argument that “If it’s not implemented *at all* your call to super will fail (and will cause a compile-time warning)”, looks to me “pointless and wrong”, too. I’ve never seen such a thing, and I know something about it thanks to systematically turning warnings into errors http://akos.ma/g2vlx

      Cheers!

  30. Carol,

    Sure would be nice if this article was updated for v3.1.3 instead of v2.0

Add Your Comments

Required
Required
Tips

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <ol> <ul> <li> <strong>

Your email is never published nor shared.

Ready?