views:

263

answers:

3

Hi,

I'm using an array of ASIHTTPRequest wrappers (AsyncImageLoader) to download images for cells in a UITableView.

I'm having problems handling ASIHTTPRequests lifetime. If I release them, I end up having a EXC_BAD_ACCESS if I keep scrolling up and down while they try to load images.

Here's what my wrapper looks like. self.request has retain property, targetCell is the cell I want to put the image in:

@implementation AsyncImageLoader
- (void)loadImageFromURL:(NSString*)fileName target:(ResultTableViewCell*)targetCell {
    // check for cached images, etc

    NSURL* url = [NSURL URLWithString:fileName];
    [self.request cancel];
    self.request = [ASIHTTPRequest requestWithURL:url];
    self.request.delegate = self;
}

- (void)startLoading {
    [self.request startAsynchronous];
}

- (void)cancel {
    [self.request cancel];
    self.request = nil;
}

- (void)requestFinished:(ASIHTTPRequest*)requestDone {
    // cache image, set cell image...

    self.request = nil;
}

- (void)requestFailed:(ASIHTTPRequest*)requestDone {
    // handle answer as well

    self.request = nil;
}
@end

loadImageFromURL is called in cellForRowAtIndexPath for the indexPath.row % 6th AsyncImageLoader, so if I keep scrolling up and down it is called again and again with the same object, cancelling requests as they aren't over yet.

But I always end up having a EXC_BAD_ACCESS. According to the call stack it happens in ASIHTTPRequest's markAsFinished, called by failWithError, called by [self.request cancel] in loadImageFromURL:. Most of the time the ASIHTTPRequest was already released (I added a NSLog in it's dealloc), but I don't see how this is possible as the ASIHTTPRequest seems to throw retains so it is not freed while cancelling.

I don't have any EXC_BAD_ACCESS if I remove self.request = nil in the delegate methods, but as ASIHTTPRequests keep being created without deallocation, they end up not working at all.

Could someone tell me what am I doing wrong?

+1  A: 

When you run into an issue where you get EXC_BAD_ACCESS it means you've over-released something. In most cases, you simply need to run the debugger (Command-Y) and see the last line of your code in the stack trace when it crashes to find out which object is being over-released. If that doesn't work, then you need to turn on zombies and see what object is being accessed again after it has been released. This will almost always point you in the right direction.

As a side note, there are some other libraries out there that do what you are trying to do. I haven't used this one much, but it looks like it has potential at least. Check out: SDWebImage. Maybe that will give you some ideas.

Best regards.

Matt Long
Thanks for your answer. I've activated Zombie and it pointed out the problem is caused by a retain called for a deallocated instance of ASIHTTPRequest. I don't see why this happens, as only one retain and release is done in AsyncImageLoader, and ASIHTTPRequest should retain itself enough to work properly. The reason could be that when I call [self.request cancel] in loadImageFromURL, self.request is already released, but it should be set to nil as requestFinished/Failed methods have been called in the main thread.
Jukurrpa
It's hard to say for sure, but I think you probably need to do some additional re-factoring. Here's what I mean. What happens if you scroll the table view while the current request is still pending? You've passed in a table view cell I’m assuming you're going to populate with an image once it's has finished downloading, but if the user has now scrolled, the image you want to show will now be the wrong one. It would be better to pass in a delegate that you can notify when the image is available. That delegate can then determine if it should still populate the current cell with that image.
Matt Long
In other words, my point is you need to re-consider your design a bit which may very well solve this issue intentionally or not.
Matt Long
Well, I've thought of different conceptions, but I'll ultimately always have to cancel an image download in order not to launch dozens of requests which will be fully executed for 'nothing' and will slow down other downloads, or even the app itself. I'll try to change the design a bit though, but I fear the problem will remain.
Jukurrpa
+1  A: 

This sounds very like a crash I am seeing.

I am not yet sure what the cause it, but I firmly believe it's not related to the client code (ie. it's either in ASIHTTPRequest or the OS).

I've have fixed some problems in asi http request, but this crash remains.

I've been trying to get assistance on the apple forums here:

https://devforums.apple.com/thread/59843?tstart=0

Here's what the crash looks like for me:

#2  0x32c02e14 in CFRetain ()
#3  0x32c709b6 in __CFTypeCollectionRetain ()
#4  0x32c06c60 in _CFArrayReplaceValues ()
#5  0x32b0994c in -[NSCFArray insertObject:atIndex:] ()
#6  0x32b098f0 in -[NSCFArray addObject:] ()
#7  0x32b709f0 in __chooseAll ()
#8  0x32b71bde in __finishedOp ()
#9  0x32b26636 in +[NSOperation observeValueForKeyPath:ofObject:change:context:] ()
#10 0x32b265aa in NSKVONotify ()
#11 0x32b13306 in -[NSObject(NSKeyValueObserverNotification) didChangeValueForKey:] ()
#12 0x0007d6ec in -[ASIHTTPRequest markAsFinished] (self=0x5a7cc40, _cmd=0xa1710) at ASIHTTPRequest.m:2817
#13 0x00077e02 in -[ASIHTTPRequest failWithError:] (self=0x5a7cc40, _cmd=0x330cdfc4, theError=0x248130) at ASIHTTPRequest.m:1708
#14 0x000712dc in -[ASIHTTPRequest cancel] (self=0x5a7cc40, _cmd=0x322b4298) at ASIHTTPRequest.m:515
#15 0x0008884c in -[UIHTTPImageView setImageWithURL:placeholderImage:] (self=0x5a7c9d0, _cmd=0xa3f0c, url=0x5aa7760, placeholder=0x0) at UIHTTPImageView.m:21
#16 0x0006183c in -[MeetingView tiledScrollView:tileForRow:column:resolution:] (self=0x5a32560, _cmd=0x9bfac, tiledScrollView=0x5a74570, row=1, column=0, resolution=0) at MeetingView.m:1053
#17 0x0000475e in -[TiledScrollView layoutSubviews] (self=0x5a74570, _cmd=0x32299680) at TiledScrollView.m:181
#18 0x31515f32 in -[UIView(CALayerDelegate) _layoutSublayersOfLayer:] ()
#19 0x32c29ffa in -[NSObject performSelector:withObject:] ()
#20 0x30964798 in -[CALayer layoutSublayers] ()
#21 0x30964568 in CALayerLayoutIfNeeded ()
#22 0x30959b62 in CA::Context::commit_transaction ()
#23 0x3095997a in CA::Transaction::commit ()
#24 0x3097f164 in CA::Transaction::observer_callback ()
#25 0x32c702a0 in __CFRunLoopDoObservers ()
#26 0x32c23bb0 in CFRunLoopRunSpecific ()
#27 0x32c234e0 in CFRunLoopRunInMode ()
#28 0x30d620da in GSEventRunModal ()
#29 0x30d62186 in GSEventRun ()
#30 0x314d54c8 in -[UIApplication _run] ()
#31 0x314d39f2 in UIApplicationMain ()
#32 0x0000245c in main (argc=1, argv=0x2ffff5b4) at main.m:14

From what I can tell, the actions look something like this:

  1. Request is created and started, and hence added to queue
  2. Request is canceled before it starts running
  3. Request is successfully cancelled, removed from the queue and deallocated
  4. Another request completes, and the queue than calls retain on the deallocated request

As an experiment, I tried retain requests one extra time - this stops the crash, but the queue stops running.

It's possible that ASIHTTPRequest is still mishandling the cancel request somehow, but I'm finding it difficult to see how.

Update

This should fix it:

http://github.com/jogu/asi-http-request/commit/887fcad0f77e9717f003273612804a9b9012a140

As best I can tell, the NSOperationQueue was getting into a bad state after being told that a request that hadn't started had finished.

JosephH
Hi, I'm also pretty sure this bug comes from ASIHTTPRequest (or maybe iOS). The actions chain you listed is exactly the same in my situation, so I believe request cancelling may be unstable. As Matt Long advised, I rather searched for another design (see my answer).
Jukurrpa
I think it is ASIHTTPRequest, I've just updated my answer with a link to a fix, if you fancy trying it out.
JosephH
A: 

Answering to myself as a synthesis of my conclusion and other answers.

It seems ASIHTTPRequest (or maybe iOS) is a bit unstable when cancelling requests, in particular when there are several requests and many get cancelled and released pretty fast.

As a solution I discarded the design of creating many requests and cancelling them if they aren't needed anymore. I made a wrapper storing ASIHTTPRequests in a NSOperationQueue, which also filters urls so the same request is not started twice (in the case a cell appears, then disappears before image is fully loaded, and is displayed again). I store all request results (i.e images) in a cache.

This results in a possibly slightly higher network activity, but everything else is clearer as every new request is completed (even though the image may not be displayed) and cached, so there's no juggling with creation/cancellation/etc... And consequently no more crash.

Jukurrpa