5
\$\begingroup\$

I've got an app that's got an activity/timeline like view. Since I don't want to retrieve the entire timeline every time, I'm caching all the events.

Currently, this is how I go about it:

- (void)saveEventArray:(NSMutableArray *)eventArray {
    // save hier in bg
    dispatch_async(kAsyncQueue, ^{
        __block int loopCount = 0;
        NSArray *copyEventArray = [eventArray copy];
        NSMutableArray *archiveArray = [NSMutableArray arrayWithCapacity:copyEventArray.count];

        if([copyEventArray count] == 0){ // If it's an empty array to save, it won't loop
            NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
            [userData setObject:[[NSMutableArray alloc]init] forKey:@"events"];
            NSLog(@"save is done, no events left in eventarray");
        }

        for (NSMutableDictionary *event in copyEventArray) {
            // PFFile isn't easy to encode, but UIImage is, so whenever we encounter a PFFile, we convert it to UIImage
            id imageFile = [event objectForKey:@"img"];
            if([imageFile isKindOfClass:[PFFile class]]){
                [imageFile getDataInBackgroundWithBlock:^(NSData *imageData, NSError *error) {
                    if (!error) {
                        UIImage *image = [UIImage imageWithData:imageData];
                        [event setObject:image forKey:@"img"]; // the PFFile is now replaced for an UIImage
                        NSData *eventEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:event];
                        [archiveArray addObject:eventEncodedObject];
                        loopCount++;

                        if(loopCount == [copyEventArray count]){ // when done looping, save it all
                            NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
                            [userData setObject:archiveArray forKey:@"events"];
                            NSLog(@"save is done");

                        }
                    }
                }];
            } else {
                loopCount++;
                NSData *eventEncodedObject = [NSKeyedArchiver archivedDataWithRootObject:event];
                [archiveArray addObject:eventEncodedObject];

                if(loopCount == [copyEventArray count]){ // when done looping
                    NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
                    [userData setObject:archiveArray forKey:@"events"];
                    NSLog(@"save is done");
                }
            }

        }
    });

}

However, I noticed that this takes insane amounts of storage and the app grows very quickly in size when doing this. Is there a better/more efficient way for this?

\$\endgroup\$
  • \$\begingroup\$ What amount of images and at what size are you trying to save? It's possible that the answer is as simple as limiting the number of images you cache. We're talking about mobile devices--they have limited disk space. \$\endgroup\$ – nhgrif Feb 21 '15 at 22:53
  • \$\begingroup\$ It depends on the number of events logged by the user. Images are about 500kb max each. \$\endgroup\$ – bdv Feb 21 '15 at 22:55
  • \$\begingroup\$ What would be a typical number? What is the number for which inspired you to want to store to disk rather than downloading each time? What is the number for which inspired you to have this reviewed? And the images are 500kb in what format? As a UIImage object? As a .png? As a .jpg? \$\endgroup\$ – nhgrif Feb 21 '15 at 22:56
  • \$\begingroup\$ Because of speed. Retrieving 5 events rather than one speeds loading up to 5 times faster. Typical amount would be 20, but the longer the app is used, the more events are logged, and they will grow among time. UIImages are jpg. I don't think this is the smartest way to cache, which is why I'd like to have it reviewed \$\endgroup\$ – bdv Feb 21 '15 at 23:01
  • \$\begingroup\$ UIImages are not jpg... and I'm attempting to give you some pointers, but I need a lot more understanding of the scenario. I didn't ask why. I'm asking for details on the quantity of images that would be typical for you to archive, and size of the typical image (and perhaps just explain to me how you measured the size). And perhaps adding a link to whatever PFFile is and its documentation? \$\endgroup\$ – nhgrif Feb 21 '15 at 23:04
3
\$\begingroup\$

This doesn't help with your primary concern of disk space, but you've got a high amount of unnecessary duplication in your code, as well as what appears to be some unnecessary checking and duplication.

We effectively see this block of code three times:

if(loopCount == [copyEventArray count]){ // when done looping, save it all
    NSUserDefaults *userData = [NSUserDefaults standardUserDefaults];
    [userData setObject:archiveArray forKey:@"events"];
    NSLog(@"save is done");
}

Moreover, as a result of your approach, there's actually a bug that could cause the entire method to be a wasted effort. If the last object in copyEventArray is a PFFile object, but it has an error being fetched, your array won't save to user defaults at all.

And ultimately, this is code is probably a little bit hard to follow (even for you perhaps?) In a background thread, we're iterating through an array and dispatching other background threads out (presuming getDataInBackgroundWithBlock actually does what it suggests it does).

The problem is, we're trying to asynchronously load a ton of images into the same array but then wait until we're completely done loading them all into the same array before we save that single array into NSUserDefaults.

So, with all of that said, I've got a few recommendations.

  1. Have you tried storing your images purely as NSData objects? Have you compared the size of NSData versus UIImage?

  2. Consider as an option compressing your downloaded images. Perhaps, realistically, it might be better if the version stored on the server is more compressed, but certainly a mobile device could compress the image. Once you have a UIImage object, you can get an NSData object for the .png representation of that image, or you could get an NSData object for the .jpg representation of that image. A .png will compress the image as much as possible without any loss. A .jpg, you can specify the compression rate.

  3. I highly recommend looking into Core Data. This would greatly simplify the downloading and archiving process. You could greatly improve the rate at which you pull the images back off the disk to display. And it's probably more efficient than using NSUserDefaults.

\$\endgroup\$
  • \$\begingroup\$ Thanks! That's some really useful feedback :) Do you maybe have a more specific corner of Core Data you'd like to point me to? I believe it's quite big. Also, the images are now compressed on the server, but compression means loss of quality, so even though it saves disk space, compressing images makes the image also less beatiful, right? \$\endgroup\$ – bdv Feb 21 '15 at 23:50
  • \$\begingroup\$ It depends. Some images, if saved as PNG, suddenly take up significantly less disk space, and PNG is lossless compression, so there is no difference in quality. \$\endgroup\$ – nhgrif Feb 22 '15 at 0:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.