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 % 6
th 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?
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.
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:
- Request is created and started, and hence added to queue
- Request is canceled before it starts running
- Request is successfully cancelled, removed from the queue and deallocated
- 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.
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 ASIHTTPRequest
s 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.
try adding something like this in the dealloc of your view:
[self.thumbnailRequest cancel];
self.thumbnailRequest.delegate = nil;
[self.thumbnailRequest setDidFinishSelector:NULL];
[self.thumbnailRequest setDidFailSelector:NULL];
self.thumbnailRequest = nil;