简体   繁体   中英

Fastest way to handle UIImagePickerController compression

What's the fastest way of getting a picture into a SQLite data store for compression, so I can return control to the user?

  • I'm using UIImagePickerController to take pictures in my application. The problem is using the picture is quite slow, because of the speed of UIImageJPEGRepresentation .
  • I want to push JPEG compression into a background thread, but before trying this I need to satisfy myself that I can persist the picture in a way that will survive across runs. That means either a blob in SQLite or a file. Which as far as I can tell, takes me right back to doing slow picture encoding right away.

What I want to achieve is speed fast enough that it feels instant to the user.

How should I be handling this? Is there anything else I should know?

Based on comments and tests, here's what I'm currently doing:

When I get the image from the UIImageController , I retain it in a class ivar and dismiss the image picker. I show a view that blocks my main view and schedule a NSTimer event to do the compression in a second, then return to the caller.

This lets the animation run that dismisses the image controller. My blocker view is revealed under it.

(The blocker view fills the entire content area of the navigation controller, and is solid black with a UIActivityIndicatorView .)

- (void)imagePickerController: (UIImagePickerController *)picker
        didFinishPickingImage: (UIImage *)selectedImage
                  editingInfo: (NSDictionary *)editingInfo;
{
    busyView.userInteractionEnabled = YES;
    busyView.alpha = 0.7f;
    mainView.userInteractionEnabled = NO;
    [self dismissModalViewControllerAnimated: YES];
    [NSTimer scheduledTimerWithTimeInterval: 1.0f
                                     target: self
                                   selector: @selector(compress:)
                                   userInfo: selectedImage
                                    repeats: NO];
}

When the timer fires, I compress the image using JPEG (because it's faster than PNG, despite intuition) and fade away the blocker view.

- (void)compress: (NSTimer *)inTimer;
{
    [self gotJPEG: UIImageJPEGRepresentation( inTimer.userInfo, 0.5f )];
    [UIView beginAnimations: @"PostCompressFade" context: nil];
    [UIView setAnimationDuration: 0.5];
    busyView.userInteractionEnabled = NO;
    busyView.alpha = 0.0f;
    [UIView commitAnimations];
    mainView.userInteractionEnabled = YES;
}

Although this adds a second do the processing, it gets the image picker out of the way faster so it no longer feels like my application has frozen. The animation from the UIActivityIndicatorView does run while UIImageJPEGRepresentation is working.

A better answer than using the NSTimer with 1 second delay would be to get an event when the animation from dismissModalViewControllerAnimated: finishes, but I'm not sure how to do this.

(I don't consider this solved yet.)

You should not save a picture in the database, unless it's very small in size. The threshold which determines if the picture is small enough is, of course, highly subjective. In my humble opinion (and experience on the iPhone), it should not exceed one megabyte. Therefore, you should only save in the database small sized images, such as icons, thumbnails etc. For images beyond one megabytes you should simply store them as files in the filesystem, and put the filename (the image path) in the database. By the way, storing the image on the filesystem and its pathname in the database is extremely fast.

About compression: you can certainly compress the image using another thread, but consider whether or not it's really worth doing this. You can use a thread to save the image to a file, save the pathname in the database and return immediately the control to your user. You have (usually) plenty of space, but a very small computational power, even on the latest iPhone 3GS. Also, you should verify (I really do not know this) whether or not loading a compressed image through UIImageView requires more time wrt a non compressed one such as a PNG. If your application will incur an additional overhead when loading a compressed image, then it may definitely not worth compressing your images. It's basically a tradeoff between space and speed. Hope this helps to decide.

Using ios 5's parent, child managed object context:

I have my managed object contexts arranged in this order:

persistent store coordinator  --->  
Private Queue Managed Object Context ( for saving to disk in background) ----->  
Main Queue Managed Object Context (for UI)  ----->  
Misc. Private Managed Object Contexts (for temporary jobs like UIImagePNGRepresentation() for example)

The model looks like :

Image Entity -> title : string , image : relationship(ImageBlob) optional  
ImageBlob Entity -> image : Binary Data, imageEntity : relationship(Image)

relationship inverses are set.

once the user finishes picking an image:

- (void)imagePickerController:(UIImagePickerController *)picker didFinishPickingMediaWithInfo:(NSDictionary *)info
{  
// get the main queue managed object context
NSManagedObjectContext* mainQueueManagedObjectContext = self.managedObjectContext;

// get the image
UIImage* image = [info objectForKey:UIImagePickerControllerOriginalImage];

// create an object, using the managed object context for the main queue
NSManagedObject *newImage = [NSEntityDescription insertNewObjectForEntityForName:@"Image" inManagedObjectContext:mainQueueManagedObjectContext];

// edit not expensive properties
[newImage setValue:[NSString stringWithFormat:@"new title %i", [self tableView:self.tableView numberOfRowsInSection:0]] forKey:@"title"];

// lets save the main context to get a permanant objectID
[self saveContextForManagedObjectContext:mainQueueManagedObjectContext];

// get the permenant objectID, Thread Safe..
NSManagedObjectID* imageObjectID = newImage.objectID;

// create a private queue concurrent managed object context
NSManagedObjectContext* privateQueueManagedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSPrivateQueueConcurrencyType];

// set the main queue as the parent
[privateQueueManagedObjectContext setParentContext:mainQueueManagedObjectContext];

// we have to use blocks here, as this managed object context will work in a private queue
[privateQueueManagedObjectContext performBlock:
 ^{
     // get the png representation in background
     NSData* data = UIImagePNGRepresentation(image);

     // get the managed object using the thread safe objectID
     NSManagedObject* imageObjectInPrivateQueue = [privateQueueManagedObjectContext objectWithID:imageObjectID];

     // insert a new object for the ImageBlob entity
     NSManagedObject *imageBlobInPrivateQueue = [NSEntityDescription insertNewObjectForEntityForName:@"ImageBlob" inManagedObjectContext:privateQueueManagedObjectContext];

     // set our image data
     [imageBlobInPrivateQueue setValue:data forKey:@"image"];

     // set the relationship to the original record
     [imageObjectInPrivateQueue setValue:imageBlobInPrivateQueue forKey:@"image"];

     // save changes to private queue context to main queue context
     [self saveContextForManagedObjectContext:privateQueueManagedObjectContext];

     // since we are not in the main queue, we have to ask the main managed object context using performBlock
     [mainQueueManagedObjectContext performBlock:
      ^{
          // what time is it before launching save in main queue
          NSDate* startDate = [NSDate date];

          // launch save on main queue
          [self saveContextForManagedObjectContext:mainQueueManagedObjectContext];

          // what time is it after finishing save in main queue
          NSDate* finishDate = [NSDate date];

          // see how long UI blocked
          NSLog(@"blocked UI for %f seconds", [finishDate timeIntervalSinceDate:startDate]);
      }];

}];

if ([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad)
{
    [self.popOverController dismissPopoverAnimated:YES];
}
else
{
    [self dismissViewControllerAnimated:YES completion:nil];
}
}

and this is how the saving is done:

-(void)saveContextForManagedObjectContext:(NSManagedObjectContext*)managedObjectContext
{
// Save the context.
NSError *error = nil;
if (![managedObjectContext save:&error]) {
    // Replace this implementation with code to handle the error appropriately.
    // abort() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development.
    NSLog(@"Unresolved error %@, %@", error, [error userInfo]);
    abort();
}
}

This greatly reduces blockage on UI, on an iphone 4, choosing a 5 megapixel image will block the UI for only 0.015 seconds.

on the other hand, loading the image will block the UI for a noticeable time also, so you might've as well load it in background too.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM