Archive for the 'Geekery' Category

A long road to a simple solution

Tuesday, January 5th, 2010

The first iPhone app I wrote was BigNames - a large-text contact list. It’s also one of the apps that I use the most, so I was surprised to hear that a particular user found that it worked the first time, but then crashed on subsequent loads.

The only thing I could think of was that the app was running out of memory and was being killed by the OS. BigNames builds an array of names when it first loads, so that it can load quickly in the future. A huge number of contacts might consume too much memory. This user had around 6700 (!!!) contacts โ€“ far more than I had ever tested โ€“ but still I would have expected to load hundreds of thousands of strings into an NSArray before hitting the 20-30 MB memory limit.

To replicate the problem, I wrote a method to create thousands of bogus contacts. Sure enough, when there were more than 4000 names being loaded from cache, it would crash on load. One odd point, was that that app would only crash when I wasn’t running it in debug mode. This was annoying, since normally I would add NSLog messages or breakpoints to identify and understand a problem, but the app would only crash when these tools were unavailable.

I remembered that Crash Logs are stored on the device and can be viewed with Xcode’s Organizer.

organizer-crash-logs-450x.png

I was able to see that the crashes weren’t the normal EXC_BAD_ACCESS variety but was something called 0×8badf00d (ha ha, get it?) and was actually the launchd complaining that the app was “failed to launch in time”. So, we aren’t actually taking about a crash, just a slow to load app.

And that explains why this wasn’t happening during a debug session. Debugging is slower than simply running an app, so I assume that launchd skips the timeout check when in debug mode.

The “failed to launch in time” error led me to think that I was doing too much work in applicationDidFinishLaunching:. I moved some of the loading of cached names to a separate thread so the app was able to load without getting killed by launchd โ€“ but then was unresponsive for the first 10-15 seconds. Not a huge improvement.

I realized that the problem was some code was taking far too long to load. It appeared to be UITableView reloadData. But since that is an API call - I couldn’t step through Apple’s code to see what it was doing. But I did know what the app was doing when it was killed by launchd. The stack trace from the Crash Log was:

Thread 0:
0   Foundation                   0x0005d9e0 -[_NSIndexPathUniqueTree uniqueIndexPath:withIndexes:count:] + 132
1   Foundation                   0x0005db8e -[NSIndexPath initWithIndexes:length:] + 86
2   Foundation                   0x0005d6ce +[NSIndexPath indexPathWithIndexes:length:] + 126
3   UIKit                        0x000a2ed8 +[NSIndexPath(UITableView) indexPathForRow:inSection:] + 40
4   UIKit                        0x0009fe84 -[UISectionRowData refreshWithSection:tableView:tableViewRowData:] + 2052
5   UIKit                        0x0009f5a0 -[UITableViewRowData rectForFooterInSection:] + 96
6   UIKit                        0x000c1380 -[UITableViewRowData rectForTableFooterView] + 168
7   UIKit                        0x001596ac -[UITableView setTableFooterView:] + 240
8   BigNames                     0x000043ea -[ContactTableViewController loadView] (ContactTableViewController.m:103)
9   UIKit                        0x00069750 -[UIViewController view] + 44
10  UIKit                        0x00088fd8 -[UIViewController contentScrollView] + 24
11  UIKit                        0x00088d90 -[UINavigationController _computeAndApplyScrollContentInsetDeltaForViewController:] + 36
12  UIKit                        0x00088c3c -[UINavigationController _layoutViewController:] + 28
13  UIKit                        0x0008863c -[UINavigationController _startTransition:fromViewController:toViewController:] + 504
14  UIKit                        0x000883a8 -[UINavigationController _startDeferredTransitionIfNeeded] + 256
15  UIKit                        0x00088298 -[UINavigationController viewWillLayoutSubviews] + 12
16  UIKit                        0x0006c86c -[UILayoutContainerView layoutSubviews] + 76
17  UIKit                        0x000482d0 -[UIView(CALayerDelegate) _layoutSublayersOfLayer:] + 32
18  QuartzCore                   0x0000c1b8 -[CALayer layoutSublayers] + 80
19  QuartzCore                   0x0000bed4 CALayerLayoutIfNeeded + 192
20  QuartzCore                   0x0000b83c CA::Context::commit_transaction(CA::Transaction*) + 256
21  QuartzCore                   0x0000b46c CA::Transaction::commit() + 276
22  QuartzCore                   0x0000b318 +[CATransaction flush] + 32
23  UIKit                        0x00052e94 -[UIApplication _reportAppLaunchFinished] + 28
24  UIKit                        0x00004a80 -[UIApplication _runWithURL:sourceBundleID:] + 608
25  UIKit                        0x00055df8 -[UIApplication handleEvent:withNewEvent:] + 1516
26  UIKit                        0x00055634 -[UIApplication sendEvent:] + 60
27  UIKit                        0x0005508c _UIApplicationHandleEvent + 4528
28  GraphicsServices             0x00005988 PurpleEventCallback + 1044
29  CoreFoundation               0x00057524 CFRunLoopRunSpecific + 2296
30  CoreFoundation               0x00056c18 CFRunLoopRunInMode + 44
31  UIKit                        0x00003c00 -[UIApplication _run] + 512
32  UIKit                        0x00002228 UIApplicationMain + 960
33  BigNames                     0x0000293e main (main.m:14)
34  BigNames                     0x000028d4 start + 44

Only the first few lines are relevant. The last bit of code that I control is loadView, and then it seems to get stuck in some sort of loop creating NSIndexPaths.

I thought perhaps the custom table footer was causing problems (lines 5-7), but removing the footer had no effect.

I was out of ideas, and the problem seemed to be in Apple’s UITableView code, so I decided I needed to send an email for the Apple developer support. If it was a bug that a UITableView couldn’t handle more than 4000 rows maybe they would have a workaround.

I created a new project and made a UITableView with 10,000 rows. It loaded almost instantly. Weird. I then added some of the UITableViewDelegate and UITableViewDataSource methods and it became incredibly slow. The method that was causing the problems: tableView:heightForRowAtIndexPath:

I looked closer at the UITableViewDelegateProtocol documentation and found this blurb under the tableView:heightForRowAtIndexPath:

There are performance implications to using tableView:heightForRowAtIndexPath: instead of rowHeight. Every time a table view is displayed, it calls tableView:heightForRowAtIndexPath: on the delegate for each of its rows, which can result in a significant performance problem with table views having a large number of rows (approximately 1000 or more).

Oh. (Why didn’t I see that before?) So, I removed this method from my UITableViewController subclass:

- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath {
	return ROW_HEIGHT;
}

And added the following to the loadView method:

self.tableView.rowHeight = ROW_HEIGHT;

Done! And so ridiculously simple and obvious in hindsight. Is there a moral to this story? Well, a specific lesson is only use cells of a consistent height if your table might be long, and never use the inefficient heightForRowAtIndexPath: if all cells are the same height. A more general take-away for me is that I should spend more time reading and re-reading the documentation. Not only is it easy to miss tiny details on an initial read, later versions of the documentation might specifically address the problem at hand. (I swear the issue on performance problems with heightForRowAtIndexPath: wasn’t there when I first read the docs!)

BikeFixTO: iPhone app source code for sale

Thursday, July 9th, 2009

bikefixto screenshots

I built BikeFixTO - an iPhone app that shows your closest bike repair location. I’m thrilled with how it turned out: The UI is nice and clean. The animated sorting of the nearest locations looks great, and I personally use the app all the time. So, on all these technical fronts, it was a hit.

Where I’ve been less successful is on the marketing side. I never expected to sell thousands, but I expected to sell hundreds. And I had vague plans of rolling it out to other bike friendly cities. But, like so many developers I underestimated how much work it was involved in the sales process. It’s hard work to get the word out. Developers often have this idea that sales should be perfectly rational where the best software wins. The pitch is: “My software solves a problem you have. Therefore you should buy it.” And if there is no immediate response, they give up since clearly the person didn’t have that problem. The whole idea of “touching” people multiple times, reminding people, tweaking the message, running contests and having sales feels spammy and inefficient.

And that is why developer’s need to consciously stop thinking like developers when it comes to marketing. And if that isn’t possible, they should partner with someone who can better wear the marketer’s hat.

I would like BikeFixTO to take on a new life. I would love it to spread to other cities where it can promote cycling, local bike shops and bicycle culture. Personally, I’m not the guy to do that, but maybe you are.

I’ve decided to make BikeFixTO free*, but am selling the source code. For $199 you get the full source to an iPhone application that you can build, tweak, customize and sell. There are no limitations with what you can do, except that I require that you don’t distribute the source code (which would prevent me from selling other copies).

The code is also an excellent code base for anyone wanting to build location-aware applications. Some of the details about what you’ll find in the source code:

  • query data from sqlite database
  • custom sqlite function to calculate distance between coordinates
  • parse and display opening hours
  • animation to visually sort items
  • dial phone numbers
  • php code for displaying maps on a webserver
  • access GPS data

If you’re interested, download BikeFixTO and give it a test drive. Drop me a line if you have any questions or would like to purchase the source code.

*Update: I’ve changed the price back to 99ยข, but if you’re interested in kicking the tires, I’m happy to send you a promo code.

*Update 2: I’ve updated the source to use MapKit.framework for fully dynamic map of all locations instead of a static map of a single location.

A simple, secure password-protected file area

Thursday, November 13th, 2008

A secure area on a website is a great way to deliver content to clients. You don’t have to send attachments around, and you can watch the logs to see when the files were accessed.

Recently, I worked on a site that required a password-protected media area. They didn’t want HTTP Authentication (which you have to admit is clunky) so I reworked the client area script that runs on my consulting site. It’s not rocket science, but there are a few tricky bits so I thought I’d share it and maybe save you some time.

Before releasing it, I asked for some advice on distributing PHP applications on Stack Overflow. I incorporated most of the suggestions, so I hope you’ll find the install painless. You’ll need PHP. This was tested on Linux and OS X, but I don’t see why it wouldn’t work on Windows.

Download securearea-1.0.tar.gz

Installation

1. Download and untar: securearea-1.0.tar.gz

2. open SecureAreaConfig.php.sample and edit it for your setup

3. save as: SecureAreaConfig.php

Now, you can copy these files to your webserver and access the file: client.php

You’ll see a some vanilla HTML which you can easily customize to match your site.

Once you have SecureArea working, you should move the “secure” directory to somewhere outside your webserver root, so that the files can only be accessed through the SecureArea script.

Download securearea-1.0.tar.gz

Demo: Try out SecureArea

Send feedback to the SecureArea comment box