iOS, AddressBook framework and GCD
For my latest iOS app I was working on an autocompletion field that hooks into the user’s phone address book and calendar. This is a great way for making the user feel at home in your app and makes your app feel part of the phone.
At first I tapped into the AddressBook
and EventKit
frameworks on the main thread while the user is typing, but that lead to a rather unresponsive user interface since both the AddressBook
and EventKit
frameworks can take a short while to access the contents. So, that work should be done on a background thread, while keeping the main thread available to respond to the user’s typing.
My approach was to put the fetching methods in GCD blocks that run on a background thread, which worked most of the time, but occassionally lead to some cryptic sqlite crashes:
CPSqliteStatementSendResults: database disk image is malformed Got SQLITE_CORRUPT for db 0x190000. Will try to delete database. runIntegrityCheckAndAbort: database disk image is malformed for SELECT ROWID, First, Last, Middle, Organization, Kind, Nickname, Prefix, Suffix, FirstSort, LastSort, CreationDate, ModificationDate, CompositeNameFallback, StoreID, FirstSortSection, LastSortSection, FirstSortLanguageIndex, LastSortLanguageIndex, PersonLink, IsPreferredName FROM ABPersonSearchKey abs JOIN ABPerson abp ON abs.person_id = abp.ROWID WHERE ( ( (1 == has_sort_key_prefix(abs.SearchKey, ?, 0)) ) ) ORDER BY FirstSortLanguageIndex+0, FirstSortSection, FirstSort;
It only happened about every forth time I tried and it wasn’t clear which line in my code caused it. After it happened a few times, the address book on my phone got corrupted, spring board forced a restart, couldn’t recover, and eventually deleted my address book. Luckily, there’s iCloud so it could restore it quickly. However, this is not something that you want to ship to users!
So, what causes this cryptic error? I wasn’t writing to the address book - just reading from it. It took me way too long to figure out that this was caused by me instantiating my local ABAddressBookRef
reference on the main thread and then using it on a background thread. The documentation notes that this should not be done:
Important You must ensure that an instance of ABAddressBookRef is used by only one thread.
So, if you encounter the above error message, you probably did the same.
Here’s my final corruption-free method:
Note that this is slightly inefficient when calling multiple times as I do in the autocompletion field. This is because I create and release address book references repeatedly. I assume here that this isn’t much overhead since the documentation doesn’t say so (as it, say, does for the EventStore
in the EventKit
framework).
It is possible to create the address book reference just once and re-use it, but then you need to make sure, that it will be re-used only from within the same thread. There are a couple of related discussions on Stack Overflow and the gist there is: ABAddressBookRef and related references need to be used within the same thread or otherwise you can lead to corruption (and eventual deletion of the user’s addressbook). A GCD dispatch queue - even a serial one - does not guarantuee that every block is executed on the same thread. So you’ll need to do a whole lot of additional work to manage your thread. I cannot recommend risking it and just trying a serial dispatch queue. It might be fine in most cases, but you might end up corrupting a user’s address book and that’s not worth it.
Update
After having had a chat with Apple engineers, the recommended way is the following: Create address book references on the fly whenever you need them. Do not bother keeping them around since they are lightweight and fast to create. The performance issue that I experienced was due to doing the initial search and the cache for the address book not yet being warmed up. You could just create a fake query at first to warm it up, but it is not recommended.