retain sent to deallocated instance with KVO (EXC_BREAKPOINT) - ios

I have this very strange error happening when I'm changing view controllers in my iOs app.
First some background info.
I am retrieving an image from the web in a background NSOperation. This operation is started from another (background) operation that is instantiated in an collection view cell. The way this works is that the cell creates an object , then sets itself as an observer for that object, then creates an NSOperation with that object as a parameter. This first level operation will start the second operation that will get the image from the web and another NSOperation that will try to get the data from a file (if possible) and report it via delegation to the first operation. That first level operation will change a property on the observed object thus triggering the KVO. The collection/tableView cell will update from the - observeValueChange method.
Here is the problem:
Sometime the cell disappears (reused or deallocated) and when the background thread tries to set the value on the observed object it triggers an EXC_BREAKPOINT exception ([collectionViewCell message retain sent to deallocated instance]).
In order to prevent this I tried implementing -prepareForReuse and -dealloc on the cells. But the error keeps happening.
The flow seem like this:
-User loads VC that has collectionViewWithCells
-cell creates object and NSOperation 1
NSoperation 1 creates NSOperation2 (this is of two types get from web or get from file)
NSOpeartion 2 get image from internet or from a local file
NSoperation 2 sends data to NSOperation1
User has left this screen
NSOperation 1 tries to set data on observed object
-- CRASH
Here is the code inside the cell:
#interface CustomCollectionViewCell ()
#property (strong, nonatomic) NSOperationQueue *imagesOperationQueue;
#property (strong, nonatomic) ImageObject *imgObj;
#end
#implementation CustomCollectionViewCell
- (id)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
// Initialization code
}
return self;
}
- (void)prepareForReuse{
[self clearDelegatesAndObservers];
[super prepareForReuse];
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.
- (void)drawRect:(CGRect)rect
{
// Drawing code
}
*/
- (void) getImage {
self.imgObj = [ImageObject newRefrenceWithId:obj_ref];
[self.imgObj addObserver:self forKeyPath:#"data" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:NULL];
TaskImageReqCache *imgReq = [[TaskImageReqCache alloc] initWithUrl:imgUrl andImageObject:self.imgObj];
[self.imagesOperationQueue addOperation:imgReq];
}
#pragma mark - KVO
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (object == self.imgObj) {
UIImage *img = [UIImage imageWithData:self.imgObj.data];
self.thumbnailImage.image = img;
}
}
- (void)dealloc
{
[self clearDelegatesAndObservers];
}
- (void)clearDelegatesAndObservers
{
[self.imagesOperationQueue cancelAllOperations];
self.thumbnailImage.image = nil;
[self.imgObj removeObserver:self forKeyPath:#"data"];
[self.pageListAdapter removeDelegateAtIndex:self.myIndexInCollection];
self.imgObj = nil;
}
In the first Level NSOperation this is where the exception breakpoint shows the crash happening:
- (void)didLoadDataFromFile:(NSData *)data
{
if (self.isCancelled) {
[self.opQueue cancelAllOperations];
[self completeOperation];
return;
}
if (!fileDownloadedFromWeb) {
self.observedObject.data = data; // CRASH
}
dataFromDisk = data;
fileReadDone = YES;
if (debugLog) {
NSLog(#"image loaded from local cache (%#)",self.sUrl);
}
}
Any suggestion on how to prevent this crash?
Thanks.
Edited to add:
what I am trying to achieve is: When a tableView cell is displayed a nsoperation is activated to get an image from the net. If the user scrolls quickly and the operation has not finished I need to end it and deallocate any data, and when the cell is reused start a new operation to get the appropriate image from the internet...

Based on comments below, we know that:
- (void)didLoadDataFromFile:(NSData *)data
is called on a different thread to dealloc, so there is a race condition. You need to access self.observedObject on the same thread as the thread it is deallocated on. I'm presuming "observedObject" is a weak reference?
dispatch_sync(dispatch_get_main_queue(), ^{
if (!fileDownloadedFromWeb) {
// Get a strong reference. This will retain observedObject - we must do this
// on the same thread as observedObject:dealloc is called, to prevent retaining
// an object during (or after) dealloc.
ObservedObject *strongRef = self.observedObject;
// This will do nothing if strongRef is nil.
strongRef.data = data;
}
});
A more structured approach would be to have the cell fetch all its images from a singleton cache (it looks as though at the moment there is no caching). The cell would obviously need to register itself as an observer for a particular URL in the cache, and the cache would notify the cell when the URL had downloaded. The cache should post that notification on the main thread.
The cache itself would manage all downloads, and there would be no background deallocation problem because it would be a singleton.
If you don't want to cache, that's fine. Use the same architecture, but call the cache an image fetcher instead. You can always add caching later if you want to.
EDIT - if your objects may be reused, rather than deallocated, as is the case for UITableViewCells, then the cell needs to be careful to ignore notifications about images that relate to a previous fetch. Either of these models will work
a) The cell retains a reference to the NSOperation until the NSOperation calls it back, or until prepareForReuse is called. Any callback from an unrecognised NSOperation must be a previous fetch (that we tried to cancel), and should be ignored. I don't really recommend this model, having the cell know about the operation AND vice versa seems silly.
b) The NSOperation sends a notification when it completes (on the main thread), and in the user info specifies the url/path that was requested. The UITableViewCell remembers what url/path it was trying to fetch, and ignores notifications that relate to other images. It unobserved that path in dealloc/prepareForReuse.

This was getting to long to be a comment so I'll make it an answer.
The reason why it's crashing has to do with the fact that UICollectionViewCells get recycled and deallocated. ARC is has put a [cvcell retain] in the wrong place. So, there are a few options:
One way to fix this is to just not create a NSOperation from a UICollectionViewCell.
Force the users to stay on the UICollectionViewController / UICollectionView so that it stays in memory.
Keep a property / pointer to the UICollectionViewController / UICollectionView so that it stays in memory even when the user has left it. (Make sure you retain it as strong or retain).
NOTE: All of these solutions do the same thing, force ARC to put the retain call somewhere else or to remove it entirely.

Cells get reused and reassigned frequently without your control so you should avoid assigning pending requests or operations to them.
Instead handle operations in your collection view data source (the view controller), and keep track of the operations not per cell but per indexPath's in a dictionary.
Even better keep this as a good experience and use something trusted and tested such as SDwebImage.

Related

Implement a Debounced/Coalesced Pattern in Cocoa Touch like `layoutSubviews`

A number of Cocoa Touch classes leverage a design pattern of coalescing events. UIViews, for example, have a method setNeedsLayout which causes layoutSubviews to be called in the very near future. This is especially useful in situations where a number of properties influence the layout. In the setter for each property you can call [self setNeedsLayout] which will ensure the layout will be updated, but will prevent many (potentially expensive) updates to the layout if multiple properties are changed at once or even if a single property were modified multiple times within one iteration of the run loop. Other expensive operations like the setNeedsDisplay and drawRect: pair of methods follow the same pattern.
What's the best way to implement pattern like this? Specifically I'd like to tie a number of dependent properties to an expensive method that needs to be called once per iteration of the run loop if a property has changed.
Possible Solutions:
Using a CADisplayLink or NSTimer you could get something working like this, but both seem more involved than necessary and I'm not sure what the performance implications of adding this to lots of objects (especially timers) would be. After all, performance is the only reason to do something like this.
I've used something like this in some cases:
- (void)debounceSelector:(SEL)sel withDelay:(CGFloat)delay {
[NSObject cancelPreviousPerformRequestsWithTarget:self selector:sel object:nil];
[self performSelector:sel withObject:nil afterDelay:delay];
}
This works great in situations where a user input should only trigger some event when a continuous action, or things like that. It seems clunky when we want to ensure there is no delay in triggering the event, instead we just want to coalesce calls within the same run loop.
NSNotificationQueue has just the thing you're looking for. See the documentation on Coalescing Notifications
Here a simple example in a UIViewController:
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(configureView:)
name:#"CoalescingNotificationName"
object:self];
[self setNeedsReload:#"viewDidLoad1"];
[self setNeedsReload:#"viewDidLoad2"];
}
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self setNeedsReload:#"viewWillAppear1"];
[self setNeedsReload:#"viewWillAppear2"];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
[self setNeedsReload:#"viewDidAppear1"];
[self setNeedsReload:#"viewDidAppear2"];
}
- (void)setNeedsReload:(NSString *)context
{
NSNotification *notification = [NSNotification notificationWithName:#"CoalescingNotificationName"
object:self
userInfo:#{#"context":context}];
[[NSNotificationQueue defaultQueue] enqueueNotification:notification
postingStyle:NSPostASAP
coalesceMask:NSNotificationCoalescingOnName|NSNotificationCoalescingOnSender
forModes:nil];
}
- (void)configureView:(NSNotification *)notification
{
NSString *text = [NSString stringWithFormat:#"configureView called: %#", notification.userInfo];
NSLog(#"%#", text);
self.detailDescriptionLabel.text = text;
}
You can checkout the docs and play with the postingStyle to get the behavior you desired. Using NSPostASAP, in this example, will give us output:
configureView called: {
context = viewDidLoad1;
}
configureView called: {
context = viewDidAppear1;
}
meaning that back-to-back calls to setNeedsReload have been coalesced.
I've implemented something like this using custom dispatch sources. Basically, you setup a dispatch source using DISPATCH_SOURCE_TYPE_DATA_OR as such:
dispatch_source_t source = dispatch_source_create( DISPATCH_SOURCE_TYPE_DATA_OR, 0, 0, dispatch_get_main_queue() );
dispatch_source_set_event_handler( source, ^{
// UI update logic goes here
});
dispatch_resume( source );
After that, every time you want to notify that it's time to update, you call:
dispatch_source_merge_data( __source, 1 );
The event handler block is non-reentrant, so updates that occur while the event handler is running will coalesce.
This is a pattern I use a fair bit in my framework, Conche (https://github.com/djs-code/Conche). If you're looking for other examples, poke around CNCHStateMachine.m and CNCHObjectFeed.m.
This borders on "primarily opinion based", but I'll throw out my usual method of handling this:
Set a flag and then queue processing with performSelector.
In your #interface put:
#property (nonatomic, readonly) BOOL needsUpdate;
And then in your #implementation put:
-(void)setNeedsUpdate {
if(!_needsUpdate) {
_needsUpdate = true;
[self performSelector:#selector(_performUpdate) withObject:nil afterDelay:0.0];
}
}
-(void)_performUpdate {
if(_needsUpdate) {
_needsUpdate = false;
[self performUpdate];
}
}
-(void)performUpdate {
}
The double check of _needsUpdate is a little redundant, but cheap. The truly paranoid would wrap all the relevant pieces in #synchronized, but that's really only necessary if setNeedsUpdate can be invoked from threads other than the main thread. If you're going to do that you also need to make changes to setNeedsUpdate to get to the main thread before calling performSelector.
It's my understanding that calling performSelector:withObject:afterDelay: using a delay value of 0 causes the method to be called on the next pass through the event loop.
If you want your actions to be queued up until the next pass through the event loop, that should work fine.
If you want to coalesce multiple different actions and only want one "do everything that accumulated since the last pass through the event loop" call, you could add single call to performSelector:withObject:afterDelay: in your app delegate (or some other single instance object) at launch, and invoke your method again at the end of each call. You could then add an NSMutableSet of things to do, and add an entry to the set each time you trigger an action that you want to coalesce. If you created a custom action object and overrode the isEqual (and hash) methods on your action object, you could set it up so there would only ever be a single action object of each type in your set of actions. Adding the same action type multiple times in a pass through the event loop would add one and only one action of that type).
Your method might look something like this:
- (void) doCoalescedActions;
{
for (CustomActionObject *aCustomAction in setOfActions)
{
//Do whatever it takes to handle coalesced actions
}
[setOfActions removeAllObjects];
[self performSelector: #selector(doCoalescedActions)
withObject: nil
afterDelay: 0];
}
It's hard to get into details on how to do this without specific details of what you want to do.

iOS ARC - Why objects not be released immediately?

Maybe this is NOT a duplicate question, as I have searched and tried many solutions about how to release objects under ARC.
The code is simple:
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
[self recreateView];
UITapGestureRecognizer *tap = [[UITapGestureRecognizer alloc] initWithTarget:self action:#selector(tapped:)];
[self.view addGestureRecognizer:tap];
}
- (void)tapped:(UITapGestureRecognizer *)g
{
[self recreateView];
}
- (void)recreateView
{
#autoreleasepool {
for (UIView *v in self.view.subviews) {
[v removeFromSuperview];
}
MyView *vv = [[MyView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:vv];
}
[self _performHeavyWork];
}
- (void)_performHeavyWork
{
int j = 0;
for (int i = 0 ; i < 100000000; ++i) {
j += random() % 7;
j = j % 18747;
}
}
#end
ViewController simply add a tap gesture recognizer whose action is to remove the old subview before adding a new one. MyView is a subclass of UIView which simply log a message when dealloced.
#implementation MyView
- (void)dealloc
{
NSLog(#"dealloc");
}
#end
The only magic is that the -_performHeavyWork is called every time a new view is created. When you keep on tapping on the screen quickly, the ViewController will be busy creating and discarding views. However, the odd thing is that all the discarded views are not dealloc immediately, but at the time you have stopped tapping for a while.
This is the profile of the process:
As you can see, the memory keep growing if you keep on tapping and so many of MyView instances exist at the same time. And if you comment out [self _performHeavyWork];, everything will be back to normal. So my question is:
Why do this happen?
And how can I solve it?
I think the main problem is that you're performing heavy work on the main thread. If you put the hard stuff on a different thread (or GCD) you'll probably see what you're expecting.
Here's some speculation on what's happening.
iOS responds to changes in the UI exclusively on the main thread. So if you're using the main thread for something else, the taps get queued for later processing.
You tap the screen, the main thread starts processing your heavy work.
You tap the screen some more. iOS can't deal with your request so it queues the event.
Eventually your heavy work completes and returns control to iOS.
iOS takes the queue of events and processes them all in a single run loop, which means the main loops auto release pool is never drained.
But what about the manual auto release pool? Well, all UI related stuff happens on the main loop and on the main thread, so the removeFrmSuperview: won't happen until control returns to the OS. Until that happens, the view hierarchy still holds a reference to your views, hence the memory growth.
You should never rely on dealloc being called when you think the last reference is gone. It is quite possible that references are still there where you don't expect them, but most importantly, dealloc can be called by ARC on a background thread.
First, iOS keeps a reference to a view when you add it to the visible window's view hierarchy. So, when you create a new instance your UIView subclass in the block, it remains in memory beyond the autorelease block. Second, the call to removeFromSuperview: does not actually result in the view being released by ARC until the main thread completes, which means there is still a reference to the view after the autorelease block ends. The work you're performing delays the main thread. This delays removing the final reference to the view.
Also, the autorelease block will not help in the case of removing the view because the view in question was not allocated in the same instance of the autorelease block. IOW, the view being created and added to the view hierarchy is not in the same scope when being removed later in the same block. So, there is no benefit to having the remove call in the autorelease block.

Message sent to a deallocated instance

Background:
All my OpenTok methods are in one ViewController that gets pushed into view, like a typical Master/detail VC relationship. The detailVC connects you to a different room depending on your selection. When I press the back button to pop the view away, I get a crash (maybe 1 out of 7 times):
[OTMessenger setRumorPingForeground] message sent to deallocated instance xxxxx
or
[OTSession setSessionConnectionStatus:]: message sent to deallocated instance 0x1e1ee440
I put my unpublish/disconnect methods in viewDidDisappear:
-(void)viewDidDisappear:(BOOL)animated{
//dispatch_async(self.opentokQueue, ^{
[self.session removeObserver:self forKeyPath:#"connectionCount"];
if(self.subscriber){
[self.subscriber close];
self.subscriber = nil;
}
if (self.publisher) {
[self doUnpublish];
}
if (self.session) {
[self.session disconnect];
self.session = nil;
}
//});
[self doCloseRoomId:self.room.roomId position:self.room.position];
}
Here is a trace:
Here is the DetailViewController on Github: link here
How to reproduce:
Make a selection from the MasterVC, that takes you into the DetailVC which immediately attempts to connect to a session and publish
Go back to previous, MasterVC quickly, usually before the session has had an a chance to publish a stream
Try this several times and eventually it will crash.
If I slow down and allow the publisher a chance to connect and publish, it is less likely to cause a crash.
Expected result:
It should just disconnect from the session/unpublish and start a new session as I go back and forth between the Master/DetailVC's.
Other:
What is your device and OS version?
iOS 6
What type of connectivity were you on?
wifi
Zombies Enabled?
Yes
ARC Enabled?
Yes
Delegates set to nil?
Yes, as far as I know
Any help solving this crash would be greatly appreciated. Perhaps I'm missing something basic that I just can't see.
What seems to happen is that the OTSession object in the OpenTok library continues to to send messages to objects in that library that have since been deallocated by switching views. The library has a [session disconnect] method that works fine if you give it enough time, but it takes close to 2-3 seconds, and that's a long time to pause an app between views.
This might be a stupid question, but:
Is there anyway to stop all processes initiated by a certain VC?
Closing the session from viewWillDisappear() works if you can determine for sure that the view is going to be popped, not pushed or hidden. Some answers suggest putting this code in dealloc(). Regarding those suggestions, Apple says,
You should try to avoid managing the lifetime of limited resources using dealloc.
So, here is how you can determine for sure that your view will get popped. viewWillDisappear() is called when the view is popped from the stack, or is otherwise pushed somewhere else. This is the easiest way to determine which, and then unpublish/disconnect if it is truly popped. You can test for this with isMovingFromParentViewController. Also, here is where you can remove specific observers.
- (void)viewWillDisappear:(BOOL)animated {
[super viewWillDisappear:animated]
// This is true if the view controller is popped
if ([self isMovingFromParentViewController])
{
NSLog(#"View controller was popped");
// Remove observer
[[NSNotificationCenter defaultCenter] removeObserver:self.session];
...
//dispatch_async(self.opentokQueue, ^{
if(self.subscriber){
[self.subscriber close];
self.subscriber = nil;
}
if (self.publisher) {
[self doUnpublish];
}
if (self.session) {
[self.session disconnect];
self.session = nil;
}
//});
[self doCloseRoomId:self.room.roomId position:self.room.position];
}
else
{
NSLog(#"New view controller was pushed");
}
}
Ref: Testing for Specific Kinds of View Transitions
Looks like OpenTok have a bug with usage NSNotificationCenter inside of OTSession and OTMessenger classes. You can see these classes in call-stack are separated with NSNotificationCenter calls:
You can manually unsubscribe your OTSession object when dealloc (hope OpenTok uses defaultCenter):
- (void)dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self.session];
}
You need to check if this code (dealloc) is really executed. If not - you need to fix problem of UIViewController deallocation. A lot of other answers contains tips how to help UIViewController to be deallocated.
-(void)viewDidDisappear:(BOOL)animated is called whenever the view is hidden, not only when it is popped from the view stack.
So if you push a view over it, viewWillDisappear will be called and your objects deleted.
This is specially problematic if you load these same objects from viewDidLoad: instead of viewDidAppear:.
Perhaps you should put your unpublish/disconnect code in -(void)dealloc.
This is what Apple suggests:
-(void) dealloc {
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
But this is only the last resort to remove observers, still often a good habit to always add it to make sure everything is cleand up on dealloc to prevent crashes.
It's still a good idea to remove the observer as soon as the object is no longer ready (or required) to receive notifications.
I most of the time put such a code in the viewWillDisappear, but I guess that doesn't really matter.
I believe the issue is that your session delegate is not set to nil. Just add the following in your viewDidDisappear:
self.session.delegate=nil;
You must call [super viewDidDisappear:animate]; at the beginning. May be it will fix your issue.
And better cleanup your session and subscriber in dealloc method:
- (void) dealloc {
[self.session removeObserver:self forKeyPath:#"connectionCount"];
if(self.subscriber){
[self.subscriber close];
self.subscriber = nil;
}
if (self.publisher) {
[self doUnpublish];
}
if (self.session) {
[self.session disconnect];
self.session = nil;
}
[self doCloseRoomId:self.room.roomId position:self.room.position];
//[super dealloc]; //for non-ARC
}
According to the stack trace you have posted, the notification center reaches out to an OTSession instance that is still alive. Afterwards, this instance provokes a crash calling methods on deallocated objects.
Adding to that the two different deallocated instance messages, we know there are asynchronous events occuring after the death of some objects that trigger the random crash you are having.
As ggfela suggested, you should make sure to nil out the delegates you have connected to the OpenTok framework. I strongly suggest you do that in the dealloc method as we want to make sure that after that point, no one has any dangling references to your object :
- (oneway void)dealloc
{
self.session.delegate = nil;
self.publisher.delegate = nil;
self.subscriber.delegate = nil;
}
Another odd thing in the code is that your handler for sessionDidConnect: creates a new dispatch_queue every time it is being called in order to call doPublish:. This means that you have concurrent threads sharing the SROpenTokVideoHandler instance which makes it prone to race conditions.

Get images from internet CollectionView

This is my first question so excuse me for being a newbie.
I am working with a CollectionView that shows images downloaded from the internet. The problem appears when I try to do it asynchronously.
#interface myViewController
{
NSArray *ArrayOfSections
}
#end
-(void)viewDidLoad{
[self performSelectorInBackground:#selector(refreshImages) withObject:nil];
}
-(void)refreshImages{
... //Get information from the net
NSArray internetInfo = ...;
[self performSelectorOnMainThread:#selector(refreshCollectionView:) withObject:internetInfo waitUntilDone:NO];
}
-(void)refreshCollectionView:(NSArray tempArray){
ArrayOfSections = tempArray;
}
This code is not working. It shows an empty CollectionView, although I have double checked that the information stored on ArrayOfSections is correct.
Moreover, if I do it synchronously (I change only viewDidLoad).
-(void)viewDidLoad{
[self refreshImage];
}
Everything works fine. I am going bananas. Please help
I think it's because you're not telling the collection view to reload. Your refresh method updates the model but not the view.
If you're fetching the data on a background thread, the main thread can continue it's lifecycle, which involves querying the collection view datasource and delegate methods then updating the view, but it will be doing this too soon in your case, as the model isn't ready. That's why you need to tell it to do that again, when the model is ready, at the end of your data fetch. Since you block the thread when doing it synchronously, it won't reach the collection view methods until the model is ready, which is why it works that way.

NSOperationQueue cancel specific operations

The problem is that I manage scrollView with lots of tiles in it.
Each visible tile display image loaded from URL or (after first URL load) cached file in background. Invisible tiles recycles (set new frame and redraw).
Image load depends on tile position.
With long range scroll there is multiple redraw called for each tile: each tile loads (and display) different image several times before display the correct one.
So problem is to cancel all previously added operations for tile before add new.
I subclass NSInvocationOperation just to contain context object to detect operation attached to and before add new operation I canceling all operation for same tile:
-(void)loadWithColler:(TileView *)coller {
if (queue == nil) {
queue = [NSOperationQueue new];
}
NSInvocationOperationWithContext *loadImageOp = [NSInvocationOperationWithContext alloc];
[loadImageOp initWithTarget:self selector:#selector(loadImage:) object:loadImageOp];
[loadImageOp setContext:coller];
[queue setSuspended:YES];
NSArray *opers = [queue operations];
for (NSInvocationOperationWithContext *nextOperation in opers) {
if ([nextOperation context] == coller) {
[nextOperation cancel];
}
}
[queue addOperation:loadImageOp];
[queue setSuspended:NO];
[loadImageOp release];
}
And in operation itself I check isCancelled:
-(void)loadImage:(NSInvocationOperationWithContext *)operation {
if (operation.isCancelled) return;
TileView *coller = [operation context];
/* TRY TO GET FILE FROM CACHE */
if (operation.isCancelled) return;
if (data) {
/* INIT WITH DATA IF LOADED */
} else {
/* LOAD FILE FROM URL AND CACHE IT */
}
if (operation.isCancelled) return;
NSInvocationOperation *setImageOp = [[NSInvocationOperation alloc] initWithTarget:coller selector:#selector(setImage:) object:cachedImage];
[[NSOperationQueue mainQueue] addOperation:setImageOp];
[setImageOp release];
}
But it is do nothing. Some times early returns works but tiles still load many images before the correct one.
So how could I success? And could this lots of unneeded operations cause delays on main thread when scrolling? (Because delays are exists and I do not know why...all load in background..)
Update:
With NSLog:
isCancelled while executing:
>
cancel loadImage method for:
>
So canceling work.
Now I save reference to last operation in TileView object and perform setImage operation only if invoked operation is equal to TileView operation.
Doesn't make any difference...
Looks like there IS number of operations to load different images to one tile invoked one after another.
Any another suggestions?
For clearance:
There is singleton DataLoader (all code from it). And all tiles has call to it in drowRect:
[[DataLoader sharedDataLoader] loadWithColler:self];
Update:
NSInvocationOperation subclass:
#interface NSInvocationOperationWithContext : NSInvocationOperation {
id context;
}
#property (nonatomic,retain,readwrite) id context;
#end
#implementation NSInvocationOperationWithContext
#synthesize context;
- (void)dealloc
{
[context release];
[super dealloc];
}
#end
Thanks a lot for any help!
SOLUTION:
From answer below: need to subclass from NSOperation
As I subclass NSOperation and put all loadImage: code into it "main" method (just move all code here and nothing else) and all work just perfect!
As about scroll delaying: it occurs cause loading images to UIImageView (it takes long time because of decompress and rasterize (as I understood).
So better way is to use CATiledLayer. It loads data in background and do it much faster.
The delays on main thread is due to a the mode of the runloop while you scroll. I suggest you to watch the WWDC2011 networking app sessions. I don't know if it is fine to subclass an NSInvocationOperation that is a concrete subclass of NSOperation. I will subclass NSOperation instead. For my experience if you like to avoid sluggish scrolling, you should create NSOperation subclasses that load their main on a specific thread for networking operation (you must create it). There is a wonderful sample code from apple https://developer.apple.com/library/ios/#samplecode/MVCNetworking/Introduction/Intro.html
The way that NSOperationQueue works with respect to "setSuspended" is that it won't start to run newly added NSOperations added to it after that point, and won't start to run any that are currently in it that haven't started running yet. Are you sure your operations you're trying to cancel haven't already started yet?
Also - does your NSOperation subclass correctly deal with Key Value Observing? Concurrent Queue subclassed NSOperations have to call willChangeValueForKey and didChangeValueForKey for some properties here - but doesn't look like that's the issue as your queue doesn't set isConcurrent. Just FYI if you go that route.

Resources