iPhone Memory Management Reading List
Memory Management in Objective-C for the iPhone gets a lot of beginners confused. It’s a topic that can be explained several different ways, so keep reading and experimenting till it clicks for you. Here’s a list of places to learn about it:
My video lessons: Basics of iPhone Memory Management.
Books
Online
- Practical Memory Management from Apple
- Memory Management Guide from Apple
- Dr Dobbs
- Open Kosmaczewski
- Mac Developer Tips
- Memo.tv
- mauvilasoftware.com
- Stepwise.com
- Tristan O’Tierney
- Mac Developer Network Video
- O’Reilly and here
- Cocoa Dev Central
- Cocoa Dev
- HyperJeff’s list of resources
- WikiBooks
- Devplace
- Woojijuice
- Peter Dikant
Fixing Blurry Subviews
The app I’m currently working on uses a small panel with extra info in a small font drawn over the top of an image. It’s implemented of course as a UIView nested as the subview in another UIView. My code loads the info panel view from a separate xib file and positions it in the main view with some code like this:
NSArray *nibContent = [[NSBundle mainBundle] loadNibNamed:@"Box" owner:self options:NULL]; UIView* overlayBox1 = [nibContent objectAtIndex:0]; [worksheet addSubview:overlayBox1]; overlayBox1.center = worksheet.center; overlayBox1.frame = CGRectOffset(overlayBox1.frame, 0, -140);
It didn’t quite work though, because the detail view came out really blurry. It didn’t look blurry in Interface Builder, but both on the simulator and the iPhone it looked terrible, see below. I spent hours changing font properties and colors, thinking it was some weird text anti-aliasing gone wrong, but it always came out blurry. Can you guess the problem? It’s one of those things that will probably stump you until you’ve run across it yourself…
The problem is Core Graphics awesome power and screen co-ordinates that are floats, not integers. Core Graphics lets you compose your views from graphics not to 100% scale, it will scale everything for you, and it can even position views with sub-pixel accuracy, rendering ‘between’ each pixel with an anti-alias like effect. This is possible because view coordinates are specified with CGRect, which uses CGPoint, where x and y are floats, not ints.
The problem occurs when you have views that are supposed to be drawn normally, ie at 100% scale and at exact pixels. In the code snippet above the subview overlayBox1 is being positioned using its center. Center is a convenience method implemented as a property, it is really setting the frame origin. As the box is sized with an even number of pixels, 174 x 82, setting the origin relative to the center results in an origin at half pixels: 73.5, 69.5. Core Graphics goes ahead and renders at the half pixel position, but as the view is drawn at 100% it means every pixel in the view lies at half pixels on the screen, so each pixel is smeared between 4 neighboring pixels resulting in a big blur.
This is easily fixed, by positioning the view to whole pixels:
overlayBox2.center = worksheet.center; CGRect overlay2Frame = overlayBox2.frame; overlay2Frame.origin.x = round(overlay2Frame.origin.x); overlay2Frame.origin.y = round(overlay2Frame.origin.y); overlayBox2.frame = overlay2Frame;
If you want to play around with this you can download the full example: blurryview.zip
#import "BlurryViewViewController.h" @implementation BlurryViewViewController // The designated initializer. Override to perform setup that is required before the view is loaded. - (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil { if (self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil]) { // Custom initialization } return self; } // Implement viewDidLoad to do additional setup after loading the view, typically from a nib. - (void)viewDidLoad { [super viewDidLoad]; NSArray *nibContent = [[NSBundle mainBundle] loadNibNamed:@"Box" owner:self options:NULL]; UIView* overlayBox1 = [nibContent objectAtIndex:0]; [worksheet addSubview:overlayBox1]; overlayBox1.center = worksheet.center; //WARNING! blur ahead. overlayBox1.frame = CGRectOffset(overlayBox1.frame, 0, -140); nibContent = [[NSBundle mainBundle] loadNibNamed:@"Box" owner:self options:NULL]; UIView* overlayBox2 = [nibContent objectAtIndex:0]; [worksheet addSubview:overlayBox2]; overlayBox2.center = worksheet.center; CGRect overlay2Frame = overlayBox2.frame; overlay2Frame.origin.x = round(overlay2Frame.origin.x); overlay2Frame.origin.y = round(overlay2Frame.origin.y); overlayBox2.frame = overlay2Frame; NSLog(@"Worksheet1 origin: %f,%f",overlayBox1.frame.origin.x,overlayBox1.frame.origin.y); } - (void)didReceiveMemoryWarning { // Releases the view if it doesn't have a superview. [super didReceiveMemoryWarning]; // Release any cached data, images, etc that aren't in use. } - (void)viewDidUnload { // Release any retained subviews of the main view. // e.g. self.myOutlet = nil; } - (void)dealloc { [super dealloc]; } @end
Ad Hoc Distribution to Windows and Mac – zipping the build
There’s a handy distribution tool for iPhone developers doing beta testing called ‘ad hoc distribution’. With ad hoc distribution a developer can make a special build of an iPhone app that can be installed on testers iPhones and iPods before the app hits the app store. It’s tricky to get working and your testers have to do a bit of work too, but it’s worth the effort. Developers: just keep trying to follow Apple’s instructions until you get it to work on your own device.
When you send the app out to testers, they need to drag and drop it into iTunes to get it on their phone, and this can easily go wrong for Windows users. In OS X when you compress your ad hoc build, you just do this right?
On OS X that creates a zip file that includes a resource fork, which is extra stuff that’s not needed in this case. When the zip arrives on your tester’s windows laptop that Mac resource fork just looks like a normal folder to Windows, and it can prevent iTunes on Windows from recognizing the folder as an app… leaving your windows tester baffled as to why they can’t just drag and drop the file into iTunes as you instructed them to.
If they unzip with a windows right click->uncompress file they get this:
And you tell your users to drag and drop CallingCard1.app into iTunes, but look inside that folder:
__MACOSX is the resource fork thing, and it’s going to confuse poor iTunes on Windows so iTunes ignores the drag and drop, and your testers won’t be able to install. Your testers need the app folder inside the app folder. Confusing! If they unzip with WinZip they are in more luck, because it unzips the content to the current folder and gives them the right app folder right away.
You can save all this trouble though, but making a zip archive in the first place that doesn’t have the Mac resource fork. BetterZip is a handy Mac app that will do this for you. That preferences setting ‘Remove Mac specific stuff from archives’ keeps __MACOSX out of the zip, and the resulting zip will install on Mac and Windows. Another way to do this is to unzip in windows yourself, find the correct app folder, and re-zip.
You can do the re-zip method in windows running in Parallels on your Mac, but it can go horribly wrong. A typical Parallels setup shares the OS X desktop folder as the Windows desktop folder so that your Windows and Mac desktops are one and the same. If you unzip and re-zip in Windows on a shared Mac desktop, then the zip archive will still be messed up. So if you are doing this, do everything in a normal (in the parallels virtual drive) Windows folder.
Finally, good luck, ad-hoc is super tricky.
What Color is My Pixel? Image based color picker on iPhone
My Postcards app has a design screen where you can change colors of things, so I needed a color picker. A very easy way to make a color picker on just about any platform is to make some kind of color pallet image file, display it for the user to click on, and do a call like: getPixelColorAtScreenLocation(x,y)
This was available back in 1983 on my ZX-Spectrum, but alas Cocoa Touch doesn’t have it. (If I missed something I hope a reader will set me straight.) What are some alternatives then?
- Ditch the color pallet and use sliders to mix colors. Not very user friendly for the everyday user.
- Manually build a lookup table for position to color. Hard work and what if I want to use a different image?
- Parse the image file myself and find the color at x,y using some third party library. Maybe…
- Bend core graphics to my will and have it ‘parse’ the image file for me and make the uncompressed pixels available.
Googling around turned up a way to make the last technique work, which is good because we’re using existing APIs, and it will work for any image:
- Make an off screen bitmap image context that is uncompressed and has a known byte to pixel mapping.
- Re-draw the color picker image into that context.
- Turn x,y touch co-ordinates into a position in the context’s bitmap buffer.
- Turn the pixel bytes at the offset position into a UIColor.
I’ve wrapped this up in ColorPickerImageView, a subclass of UIImageView. When you tap it, it uses this approach to get the pixel color you tapped. You can also register a simple delegate with it to get a callback with the picked color. You put your own image in it, so you can make it a little color pallet in a corner or make it fill the screen. As it’s derived from UIImageView, you use it just like UIImageView in code or in Interface Builder. The pixel grabbing source code is below, and the whole class is included in: ColorPicker code to download and copy.
- (UIColor*) getPixelColorAtLocation:(CGPoint)point { UIColor* color = nil; CGImageRef inImage = self.image.CGImage; // Create off screen bitmap context to draw the image into. Format ARGB is 4 bytes for each pixel: Alpa, Red, Green, Blue CGContextRef cgctx = [self createARGBBitmapContextFromImage:inImage]; if (cgctx == NULL) { return nil; /* error */ } size_t w = CGImageGetWidth(inImage); size_t h = CGImageGetHeight(inImage); CGRect rect = {{0,0},{w,h}}; // Draw the image to the bitmap context. Once we draw, the memory // allocated for the context for rendering will then contain the // raw image data in the specified color space. CGContextDrawImage(cgctx, rect, inImage); // Now we can get a pointer to the image data associated with the bitmap // context. unsigned char* data = CGBitmapContextGetData (cgctx); if (data != NULL) { //offset locates the pixel in the data from x,y. //4 for 4 bytes of data per pixel, w is width of one row of data. int offset = 4*((w*round(point.y))+round(point.x)); int alpha = data[offset]; int red = data[offset+1]; int green = data[offset+2]; int blue = data[offset+3]; NSLog(@"offset: %i colors: RGB A %i %i %i %i",offset,red,green,blue,alpha); color = [UIColor colorWithRed:(red/255.0f) green:(green/255.0f) blue:(blue/255.0f) alpha:(alpha/255.0f)]; } // When finished, release the context CGContextRelease(cgctx); // Free image data memory for the context if (data) { free(data); } return color; } - (CGContextRef) createARGBBitmapContextFromImage:(CGImageRef) inImage { CGContextRef context = NULL; CGColorSpaceRef colorSpace; void * bitmapData; int bitmapByteCount; int bitmapBytesPerRow; // Get image width, height. We'll use the entire image. size_t pixelsWide = CGImageGetWidth(inImage); size_t pixelsHigh = CGImageGetHeight(inImage); // Declare the number of bytes per row. Each pixel in the bitmap in this // example is represented by 4 bytes; 8 bits each of red, green, blue, and // alpha. bitmapBytesPerRow = (pixelsWide * 4); bitmapByteCount = (bitmapBytesPerRow * pixelsHigh); // Use the generic RGB color space. colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericRGB); if (colorSpace == NULL) { fprintf(stderr, "Error allocating color space\n"); return NULL; } // Allocate memory for image data. This is the destination in memory // where any drawing to the bitmap context will be rendered. bitmapData = malloc( bitmapByteCount ); if (bitmapData == NULL) { fprintf (stderr, "Memory not allocated!"); CGColorSpaceRelease( colorSpace ); return NULL; } // Create the bitmap context. We want pre-multiplied ARGB, 8-bits // per component. Regardless of what the source image format is // (CMYK, Grayscale, and so on) it will be converted over to the format // specified here by CGBitmapContextCreate. context = CGBitmapContextCreate (bitmapData, pixelsWide, pixelsHigh, 8, // bits per component bitmapBytesPerRow, colorSpace, kCGImageAlphaPremultipliedFirst); if (context == NULL) { free (bitmapData); fprintf (stderr, "Context not created!"); } // Make sure and release colorspace before returning CGColorSpaceRelease( colorSpace ); return context; }
UITables with Downloaded Images – Easy Asynchronous Code
Readers… do look through the comments if you plan to use this code, other people have posted improvements. Thanks for all the great feedback everyone. MJ
The app ‘Postcards’ from my iPhone developer training class is a utility app for quickly sending a customized postcard, and one thing that makes it super easy is that you can grab pictures from Flickr to include in the postcard design. Postcards makes simple HTTP calls Flickr’s REST API to download public domain images and displays them in a UITableView for the user to pick from. Cocoa Touch makes this all simple and easy to code, and my first development version used synchronous calls to get the images by using NSData dataWithContentsOfURL:
.
.
NSData* data = [NSData dataWithContentsOfURL:[NSURL URLWithString:urls]];
Making synchronous calls to remote web servers from the thread that’s running the apps GUI is of course a bad idea that results in a laggy UI and unsatisfied users. Using synchronous calls in UITableView cellForRowAtIndexPath to load all the images results in a problem six times worse (for 6 rows on the screen) and makes scrolling basically broken as the table won’t scroll until it has the next cell, which it can’t get while the app is waiting for an image to download. Then imagine that on the Edge network! Obviously we need something multi-threaded that can load the images in parallel in the background and update the UI as they finish downloading.
Multi-threaded programming is hard and should be avoided whenever possible, and in this case Cocoa’s beautiful design came to my rescue:
UIView heirachy + URL loading system + delegate design = multi-threaded image loading with no multi-threaded coding!
How can you have your cake and eat it too? Every iPhone app is a multi-threaded program, or at least its running in conjuction with the multi-threaded iPhone operating system. Use the right delegate methods in the right ways, and you can take advantage of extra threads of execution that the iPhone gives you for free without writting any multi-threaded code of your own, hence sidesteping the problem of threading bugs in your code. An iPhone app is one big event loop – your classes have methods that the event loop calls in response to stuff happening on the device and in your app. When you use the URL loading system’s asynchronous APIs, the iPhone uses a different thread than the one running your app’s event loop to load the contents of the URL, and it makes callbacks via your apps event loop when data has been downloaded.
connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; - (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData
Note carefully, when data has arrived from the remote webserver, that other iPhone thread doing the downloading doesn’t make calls into your objects at the same time as your methods are running, it puts messages into your apps event loop. If it called your app directly then chances are your app would be running some UI code or something and you’d have to write thread safe code. Instead, the call that data is ready arrives as an event on the event loop. Events on the event loop run single threaded, one at a time. Using this we can get asynchrous image download from Flickr without writting thread safe code ourselves. Even better, Cocoa’s URL loading system will download those URLs in parallel! For free!
That’s all well and good, but how do you get a table view to update the UITableViewCell with the image after its already been returned? A UIImage is imutable (right?) so you can’t change its image later when the image data has downloaded. Turns out Apple made this super easy too. Instead of putting a UIImage in the UITableViewCell, you put your own UIView object, that is sized correctly for the image you want to display, into the content view of the UITableCell (as a subview). At first your view object it can be empty, or it can have a dummy image in it, or you can pop in one of those spinny ’something is happening’ views. Then when the image data is downloaded, create a UIImageView with the image and pop it in your view in the cell. Hey presto… it appears. While all this is happening the user can be scrolling and going back and forth with a fully functioning UI.
I put this all together in a class AsyncImageView, listed below. It’s use is simple
- alloc and initWithRect:
- add it to a view, eg in a table cell’s content view;
- send it the loadImageFromURL: message.
LoadImageFromURL will return right away, the image will load in the background, and will automatically appear in the view when its finished downloading. The code posted below is something I whipped up pretty quickly (and I didn’t leak check yet!), but hey – parallel, asynchronous image download and display in about 40 lines of code with no thread-safe worries? Works in smooth scrolling tables, even on the Edge network? I rate it a big win, and wanted to share the technique.
I’ve developed an iPhone programming training class that I will be teaching soon in the SFBay Area (though my partners and I could probably bring it to you). It’s very hands on, and specially designed to help professional programmers new to Cocoa and Objective-C over the difficult initial learning curve. In the class we build the Postcards app from start to finish, including AsyncImageView. Email me for more info: markj at markj.net
@interface AsyncImageView : UIView { NSURLConnection* connection; NSMutableData* data; } @end @implementation AsyncImageView - (void)loadImageFromURL:(NSURL*)url { if (connection!=nil) { [connection release]; } if (data!=nil) { [data release]; } NSURLRequest* request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:60.0]; connection = [[NSURLConnection alloc] initWithRequest:request delegate:self]; //TODO error handling, what if connection is nil? } - (void)connection:(NSURLConnection *)theConnection didReceiveData:(NSData *)incrementalData { if (data==nil) { data = [[NSMutableData alloc] initWithCapacity:2048]; } [data appendData:incrementalData]; } - (void)connectionDidFinishLoading:(NSURLConnection*)theConnection { [connection release]; connection=nil; if ([[self subviews] count]>0) { [[[self subviews] objectAtIndex:0] removeFromSuperview]; } UIImageView* imageView = [[[UIImageView alloc] initWithImage:[UIImage imageWithData:data]] autorelease]; imageView.contentMode = UIViewContentModeScaleAspectFit; imageView.autoresizingMask = ( UIViewAutoresizingFlexibleWidth || UIViewAutoresizingFlexibleHeight ); [self addSubview:imageView]; imageView.frame = self.bounds; [imageView setNeedsLayout]; [self setNeedsLayout]; [data release]; data=nil; } - (UIImage*) image { UIImageView* iv = [[self subviews] objectAtIndex:0]; return [iv image]; } - (void)dealloc { [connection cancel]; [connection release]; [data release]; [super dealloc]; } @end
And here is the usage in UITableViewCell. The AsyncImageView gets tagged with 999, and when it gets recycled, that 999 tagged view gets fished out and removed. So only the cell is being recycled, not the AsyncImageView object. When its removed from the cells content view it also gets released, causing dealloc, which in turn cancels the url download (if its outstanding).
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { static NSString *CellIdentifier = @"ImageCell"; UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:CellIdentifier]; if (cell == nil) { cell = [[[UITableViewCell alloc] initWithFrame:CGRectZero reuseIdentifier:CellIdentifier] autorelease]; } else { AsyncImageView* oldImage = (AsyncImageView*) [cell.contentView viewWithTag:999]; [oldImage removeFromSuperview]; } CGRect frame; frame.size.width=75; frame.size.height=75; frame.origin.x=0; frame.origin.y=0; AsyncImageView* asyncImage = [[[AsyncImageView alloc] initWithFrame:frame] autorelease]; asyncImage.tag = 999; NSURL* url = [imageDownload thumbnailURLAtIndex:indexPath.row]; [asyncImage loadImageFromURL:url]; [cell.contentView addSubview:asyncImage]; return cell; }












