0

I'm querying all persons from the IOS address book and store their image in a local cache. Everything works fine for small address books - however a lot of entries (>1000) crash the app due to memory pressure.

After investigating the issue it seems that the ABPersonCopyImageData allocates memory for that image, and returns a CFDataRef photoData with a refcount of 2. After releasing the data CFRelease(photoData) the refcount stays at 1, which suggests that the ABAddressBookRef addressBook keeps a reference, probably for caching reasons. The memory consumption linearly increases through the whole loop.

After the loop CFRelease(addressBook) finally cleans up all references and frees up the memory. So one hack-ish solution is to periodically release the address book and create a new one (every 100 items or so), but it has some downsides.

Is there another way to tell the address book to release the reference to the image data?

- (void)testContacts {
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, nil);
    CFArrayRef allContacts = ABAddressBookCopyArrayOfAllPeople(addressBook);
    CFIndex nPeople = ABAddressBookGetPersonCount(addressBook);

    for (CFIndex idx = 0; idx < nPeople; idx++ ) {
        ABRecordRef person = CFArrayGetValueAtIndex( allContacts, idx );

        if (ABPersonHasImageData(person)) {
            CFDataRef photoData = ABPersonCopyImageDataWithFormat(person, kABPersonImageFormatThumbnail);
            if (photoData) {
                // do something (eg. store data) - does not affect problem

                CFRelease(photoData);
            }
        }
    }

    CFRelease(addressBook);
}
Simon Heinzle
  • 1,095
  • 1
  • 9
  • 17
  • 1
    does a manual handled autoreleasepool maybe help? Have you checked with instruments of allocs happens without free`s ? – Volker Apr 27 '15 at 15:22
  • Autoreleaspool will not help (I am already releasing all relevant data manually), and free happens as soon as the addressBook is released – Simon Heinzle Apr 27 '15 at 15:38

2 Answers2

0

I think that an @autoreleasepool will indeed help your case like Volker suggested in the comments. Calling CFRelease doesn't actually free the memory, it just decreases the retain count of the object, like release would do before ARC. So the memory usage is accumulated until the next drain of the autorelease pool which happens at the end of the current cycle of the current run-loop.

Also, please note that you can use toll-free bridging to transfer the ownership to ARC and make this even more efficient:

- (void)testContacts {
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, nil);
    NSArray *allContacts = (__bridge_transfer NSArray*)ABAddressBookCopyArrayOfAllPeople(addressBook);
    CFIndex nPeople = ABAddressBookGetPersonCount(addressBook);

    for (CFIndex idx = 0; idx < nPeople; idx++ ) {
        @autoreleasepool {
            ABRecordRef person = (__bridge ABRecordRef)allContacts[idx];

            if (ABPersonHasImageData(person)) {
                NSData *photoData = (__bridge_transfer NSData*)ABPersonCopyImageDataWithFormat(person, kABPersonImageFormatThumbnail);
                if (photoData) {
                    // do something (eg. store data) - does not affect problem
                }
            }
        }
    }

    CFRelease(addressBook);
}
Artal
  • 8,933
  • 2
  • 27
  • 30
  • Thank you for your response. Unfortunately memory behaviour is the same as in the original version. Memory increases linearly throughout the loop (from 6MB to 44MB peak for 1000 contacts), and drops down to 6MB after `CFRelease(addressBook)`. I really think that after requesting the image the `addressBook` retains a reference to the image for caching - and is therefore not released in the `@autoreleasepool` – Simon Heinzle Apr 28 '15 at 08:52
0

Ok, so far periodically releasing the addressBook seems to be the only solution to problem, see code below.

Releasing every 100 entries adds around 8% overhead on execution time, but as expected reduces the memory almost by a factor of 10 for 1000 phonebook entries (i.e 35MB and 15sec vs 4MB vs 16.2sec on an iPhone 4)

I hope anybody can come up with a better solution, until then I will use this.

void abImagesV3() {
    ABAddressBookRef addressBook = ABAddressBookCreateWithOptions(NULL, nil);
    CFArrayRef allContacts = ABAddressBookCopyArrayOfAllPeople(addressBook);
    CFIndex nPeople = ABAddressBookGetPersonCount(addressBook);

    for (CFIndex idx = 0; idx < nPeople; idx++ ) {
        ABRecordRef person = CFArrayGetValueAtIndex( allContacts, idx );

        if (ABPersonHasImageData(person)) {
            CFDataRef photoData = ABPersonCopyImageDataWithFormat(person, kABPersonImageFormatThumbnail);
            if (photoData) {
                // do something (eg. store data) - does not affect problem
                CFRelease(photoData);
            }
        }

        if (idx%100 == 99) {
            CFRelease(addressBook);
            CFRelease(allContacts);
            addressBook = ABAddressBookCreateWithOptions(NULL, nil);
            allContacts = ABAddressBookCopyArrayOfAllPeople(addressBook);
        }
    }

    CFRelease(addressBook);
}
Simon Heinzle
  • 1,095
  • 1
  • 9
  • 17