My goal is to initialize a large amount of data on a different (non-main) thread, and then refresh a UIProgressView on the main thread corresponding to the large data load's progress. To do this, I use performSelectorInBackground to load the data, and update the progress throughout the way using performSelectorOnMainThread.
I am currently getting a EXE_BAD_ACCESS error from one of the dictionaries that I am initializing, specifically the line where I am setting self.someDictionary = #{...}. self.someDictionary takes on strong and nonatomic properties, and is initialized on the non-main thread.
As a total newbie to multithreading, I am beginning to see that I shouldn't be setting a strong and nonatomic property (however, changing it to atomic still caused the crash). What else am I doing incorrectly to cause the EXE_BAD_ACCESS error, and how do I set a large amount of data to an NSDictionary on a non-main thread and still be able to update the progress on the main thread?
Thanks!
Edit 1:
Code:
//In viewWillAppear, from the main thread
[self performSelectorInBackground:#selector(populateDictionaries) withObject:nil];
//In populateDictionaries method
Dictionary *someDictionary = [[Dictionary alloc] init];
//the methods inside the Dictionary class
- (id) init{
self = [super init];
if (self){
[self makeDictionaries];
}
return self;
}
- (void)makeDictionaries{
self.insiderDictionary = #{ ...} //this line is causing the crash
}
If you want to perform the process in background try Dispatch queue
ref: https://developer.apple.com/library/ios/documentation/General/Conceptual/ConcurrencyProgrammingGuide/OperationQueues/OperationQueues.html
Thank you.
Related
I'm trying to build an array of dictionaries in a background thread while keeping access to the current array until the background operation is done. Here's a simplified version of my code:
#property (nonatomic, strong) NSMutableArray *data;
#property (nonatomic, strong) NSMutableArray *dataInProgress;
- (void)loadData {
self.dataInProgress = [NSMutableArray array];
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
[self loadDataWorker];
});
}
- (void)loadDataWorker {
for (int i=0; i<10000; i++) {
[self addDataItem];
}
dispatch_async(dispatch_get_main_queue(), ^{
[self loadDataFinish]; // the crash occurs before we get to this point
});
}
- (void)addDataItem {
// first check some previously added data
int currentCount = (int)[self.dataInProgress count];
if (currentCount > 0) {
NSDictionary *lastItem = [self.dataInProgress objectAtIndex:(currentCount - 1)];
NSDictionary *checkValue = [lastItem objectForKey:#"key3"]; // this line crashes with EXC_BAD_ACCESS
}
// then add another item
NSDictionary *dictionaryValue = [NSDictionary dictionaryWithObjectsAndKeys:#"bar", #"foo", nil];
NSDictionary *item = [NSDictionary dictionaryWithObjectsAndKeys:#"value1", #"key1", #"value2", #"key2", dictionaryValue, #"key3", nil];
// as described in UPDATE, I think this is the problem
dispatch_async(dispatch_get_main_queue(), ^{
[dictionaryValue setObject:[self makeCustomView] forKey:#"customView"];
});
[self.dataInProgress addObject:item];
}
- (UIView *)makeCustomView {
return [[UIView alloc] initWithFrame:CGRectMake(0, 0, 0, 0)];
}
- (void)loadDataFinish {
self.data = [NSMutableArray arrayWithArray:self.dataInProgress];
}
This works fine in most cases, but when the dataset is large, I start to get crashes on the line indicated above. The likelihood of a crash is greater with more data or a device with less memory. On an iPhone 6 with 10,000 items, it happens about one in five times. So it looks like when memory gets tight, the dictionaries inside the data array are destroyed before I access them.
If I do everything in the main thread there are no crashes. I originally had this problem with non-ARC code, then I converted my project to ARC and the same problem remains.
Is there a way to ensure the objects added earlier in the build process are retained until I'm done? Or is there a better way to do what I'm doing?
Here's a stack trace:
thread #17: tid = 0x9c586, 0x00000001802d1b90 libobjc.A.dylib`objc_msgSend + 16, queue = 'com.apple.root.background-qos', stop reason = EXC_BAD_ACCESS (code=1, address=0x10)
frame #0: 0x00000001802d1b90 libobjc.A.dylib`objc_msgSend + 16
frame #1: 0x0000000180b42384 CoreFoundation`-[__NSDictionaryM objectForKey:] + 148
frame #2: 0x00000001002edd58 MyApp`-[Table addDataItem](self=0x000000014fd44600, _cmd="addDataItem", id=0x00000001527650d0, section=3, cellData=0x0000000152765050) + 1232 at Table.m:392
frame #4: 0x00000001002eca28 MyApp`__25-[Table loadData]_block_invoke(.block_descriptor=0x000000015229efd0) + 52 at Table.m:265
frame #5: 0x0000000100705a7c libdispatch.dylib`_dispatch_call_block_and_release + 24
frame #6: 0x0000000100705a3c libdispatch.dylib`_dispatch_client_callout + 16
frame #7: 0x0000000100714c9c libdispatch.dylib`_dispatch_root_queue_drain + 2344
frame #8: 0x0000000100714364 libdispatch.dylib`_dispatch_worker_thread3 + 132
frame #9: 0x00000001808bd470 libsystem_pthread.dylib`_pthread_wqthread + 1092
frame #10: 0x00000001808bd020 libsystem_pthread.dylib`start_wqthread + 4
UPDATE
I traced through my full code with the answers below in mind, particularly those about locking while multithreading, and realized that part of the data I'm adding to my data array is a UIView that I'm creating during the build process. Since it's bad to build views in a background thread, and I did see problems when doing that, I'm jumping back to the main thread for makeCustomView. See the lines of code I added above with "UPDATE" in the comment. This must be the problem now; when I skip adding the custom view, I have no more crashes.
I could rework the build workflow so that all the data except the custom views are added on the background thread, then I could make a second pass and add the custom views on the main thread. But is there a way to manage the threads in this workflow? I tried locking with NSLock before and after calling makeCustomView, but that made no difference. I also found an SO answer saying NSLock is basically outdated, so I didn't go further with that.
If I understood you correctly, concurrent accesses to the dataInProgress array causes the problem, because the array is filled in a background thread and used in the main thread. But NSMutableArray is not thread safe. This fits to my intention that the array itself is corrupted.
You could solve that with NSLock to serialize the accesses to the array, but this is akin of outdated and does not fit to the rest of your code, which uses the more modern (and better) GCD.
A. Situation
What you have:
a builder control flow, which has to run in background
a creation of views control flow, which has to run in the main queue (thread). (I'm not completely sure, whether the pure creation of a view has to be done in main thread, but i would do.)
both control flows accesses the same resource (dataInProgress)
B. GCD
With classical thread/lock approach you start async control flows and serialize them with locks, when they access concurrently a shared resource.
With GCD you start control flows concurrently to each other, but serialized for a given shared resource. (Basically, there are more features, more complexity, but this is, what we need here.)
C. Serializing
It is correct to start the builder in a background queue ("thread") to run it without blocking the main thread. Done.
It is correct to switch back to main thread, if you want to do something with UI elements, esp. creating a view.
Since both control flows accesses the same resource, you have to serialize the accesses. You do this by creating a (serial) queue for that resource:
…
#property dispatch_queue_t dataInProgressAccessQ;
…
// In init or whatever
self. dataInProgressAccessQ = dispatch_queue_create("com.yourcompany.dataInProgressQ", NULL);
After doing that, you put every access to the dataInProgress array in that queue. There is a simple example for that:
// [self.dataInProgress addObject:item];
dispatch_async( self.dataInProgressAccessQ,
^{
[self.dataInProgress addObject:item];
});
In this case it is very easy, because you have to switch the queue at the and of the code. If it is in the middle, you have two options:
a) Use the queue similar to a lock. Let's have an example:
// NSInteger currentCount = [self.dataInProgress count]; // Why int?
NSInteger currentCount;
dispatch_sync( self.dataInProgressAccessQ,
^{
currentCount = [self.dataInProgress count];
});
// More code using currentCount
Using dispatch_sync() will let the code execution wait, until accesses from other control flows are finished. (It is like a lock.)
Edit: As with locks, the access is guaranteed to be serialized. But there might be the problem, that another thread removes objects from the array. Let's have a look to such a situation:
// NSInteger currentCount = [self.dataInProgress count]; // Why int?
NSInteger currentCount;
dispatch_sync( self.dataInProgressAccessQ,
^{
currentCount = [self.dataInProgress count];
});
// More code using currentCount
// Imagine that the execution is stopped here
// Imagine that -makeCustomView removes the last item in meanwhile
// Imagine that the execution continues here
// -> currentCount is not valid anymore.
id lastItem = [self.dataInProgress objectAtIndex:currentCount]; // crash: index out of bounds
To prevent this, you really have to isolate your concurrent code. This highly depends on your code. However, in my example:
id lastItem;
dispatch_sync( self.dataInProgressAccessQ,
^{
NSInteger currentCount;
currentCount = [self.dataInProgress count];
lastItem = [self.dataInProgress objectAtIndex:currentCount]; // don't crash: bounds are not changed
});
// Continue with lastItem
As you can imagine, when getting the last item, if can be removed from the array in the very next moment after you read it. Maybe this causes problems of inconsistency in your code. It really depends on your code.
End of edit
b) Maybe you get performance problems, because is works like a lock (synch). If so, you have to analyze your code and extract parts, that can run concurrently again. The pattern looks like this:
// NSInteger currentCount = [self.dataInProgress count]; // Why int?
dispatch_async( self.dataInProgressAccessQ, // <-- becomes asynch
^{
NSInteger currentCount = [self.dataInProgress count];
// go back to the background queue to leave the access queue fastly
dispatch_async( dispatch_get_global_queue(),
^{
// use current count here.
});
});
dispatch_async( self.dataInProgressAccessQ,
^{
// Another task, that can run concurrently to the above
});
What you can do there, is a matter of your concrete code. Maybe it is a help for you, to have your own private builder queue instead of using the global queue.
But this is the basic approach: Move a task into a queue and do not wait, until it is finished, but add code at the end, that completes the task in another control flow.
Instead of
Code
--lock--
var = Access code
--unlock--
More Code using var
it is
Code
asynch {
var Access Code
asynch {
More code using var
}
}
Of course, you have to do the same inside -makeCustomView.
I agree with Phillip Mills. This looks like a thread safety issue around your self.dataInProgress object.
From Apple docs https://developer.apple.com/library/mac/documentation/Cocoa/Conceptual/Multithreading/ThreadSafetySummary/ThreadSafetySummary.html :
Mutable objects are generally not thread-safe. To use mutable objects in a threaded application, the application must synchronize access to them using locks. (For more information, see Atomic Operations). In general, the collection classes (for example, NSMutableArray, NSMutableDictionary) are not thread-safe when mutations are concerned. That is, if one or more threads are changing the same array, problems can occur. You must lock around spots where reads and writes occur to assure thread safety.
If addDataItem is being called from various background threads, you need to lock around reading and writing to self.dataInProgress.
I don't think you need deep copies. If the dictionaries aren't mutable, all you need is for them to not be released...and a copy of the array they're in will do that for you.
What I believe you need is synchronization around any access to self.data. I suggest creating a NSLock object for your class and wrapping each of the following two lines with lock/unlock method calls:
self.data = [NSMutableArray arrayWithArray:self.dataInProgress];
//...
NSDictionary *item = [self.data objectAtIndex:index];
Also, why does self.data need to be mutable? If it doesn't, self.data = [self.dataInProgress copy]; is simpler...and quite possibly more efficient for memory and performance.
The one thing that's worrying me is, what about the caller of getData. It may not know that the self.data array has changed. If the array becomes shorter, you're headed for an "index out of bounds" crash.
It would be good to only call getData when you know the array is going to be stable. (In other words, synchronize the data gets at a higher level.)
I would attempt at passing in a weak reference to self. I bet if you might have a strong retain cycle happening there somewhere. If I remember correctly, __weak doesn't up the retain count, and __block allows you to change the variable
- (void)loadData {
self.dataInProgress = [NSMutableArray array];
__weak __block SelfClassName *weakSelf = self;
dispatch_async(dispatch_get_global_queue(QOS_CLASS_UTILITY, 0), ^{
[weakSelf loadDataWorker];
});
}
- (void)loadDataWorker {
for (int i=0; i<10000; i++) {
[self addDataItem];
}
__weak __block SelfClassName *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
[weakSelf loadDataFinish];
});
}
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.
I am a newbie. I am using Grand Central Dispatch to populate an array (student_temp) on another thread. That part is working fine. The problem is I cannot pass the array to a class property (student_Array) where it is used throughout the class. I can't get the array back on the main thread.
it works fine until I get back tot he main thread and I can't pass student_temp into student_Array (the property) either inside or outside of GCD.
What am I doing wrong, or is there a better to populate the array property using GCD?
Thank you for your help. And please try to explain in non-technical language if possible I am new at this.
- (void)viewDidLoad
{
[super viewDidLoad];
R2LFetcher *studentFetch = [[R2LFetcher alloc] init];
__block NSMutableArray *student_temp = [[NSMutableArray alloc] init];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0ul);
dispatch_async(queue, ^{
//long-running code goes here…
student_temp = [studentFetch fetchToStudentArray];
dispatch_async(dispatch_get_main_queue(), ^{
// code the updates the main thread (UI) here...
student_Array = student_temp;
});
});
student_Array = student_temp;
A couple of reactions:
In the last line of your code, you're setting student_Array to student_temp. Clearly that line makes no sense because you're populating student_temp asynchronously. And you're opening yourself up to synchronization issues if you're trying to simultaneously access the save variable in two queues. Don't bother to assign student_Array to student_temp at the end of viewDidLoad, but rather just do it inside the nested dispatch_async calls.
Inside the block, you're populating and setting student_temp. It probably makes more sense to make that variable scoped within that block, avoiding temptation to access it from outside that block as well as simplifying your code because the __block qualifier is no longer needed.
This block is running asynchronously, so when you update student_Array in the main queue, you might want to update your UI at the same time (e.g. reload the tableview or whatever). Perhaps you're doing that already and just removed it for the sake of brevity, but I just wanted to make sure.
Thus:
- (void)viewDidLoad
{
[super viewDidLoad];
dispatch_queue_t queue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0ul);
dispatch_async(queue, ^{
R2LFetcher *studentFetch = [[R2LFetcher alloc] init];
// long-running code goes here, for example ...
NSMutableArray *student_temp = [studentFetch fetchToStudentArray];
dispatch_async(dispatch_get_main_queue(), ^{
student_Array = student_temp;
// code the updates the main thread (UI) here, for example...
[self.tableView reloadData];
});
});
}
You should be able to add objects to student_Array directly from your block. Unlike stack variables, properties and ivars don't get copied when used inside a block. Instead, self gets retained in the block, and the property is referenced through it.
Of course, you need to be aware of concurrency issues, e.g. if you need to access the data from the main thread as well. For that, you probably still want to have this at the end of your async GCD block:
// done populating the data
dispatch_async(dispatch_get_main_queue(), ^{
// update the UI
}
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.
I need some help in using objects from Core Data with GCD; I seem to get NSManagedObjects that are aren't faulted into the main thread, even when I access their properties. Would appreciate some help.
This is what I'm doing: on launch, I need to load a list of Persons from the Core Data DB, do some custom processing in the background, then reload the table to show the names. I am following the guidelines for Core Data multi-threading by only passing in the objectIDs into the GCD queues. But when I reload the tableview on the main thread, I never see the name (or other properties) displayed for the contacts, and on closer inspection, the NSManagedObjects turn out to be faults on the main thread, even though I access various properties in cellForRowAtIndexPath. The name property is visible in the background thread when I NSLog it; and it's also showing correctly on the main thread in NSLogs in cellForRowAtIndexPath. But they don't show in the tableView no matter what I do. I tried accessing the name property using the dot notation, as well as valueForKey, but neither worked.
Here's my code …. it's called from the FRC initializer:
- (NSFetchedResultsController *)fetchedResultsController
{
if (__fetchedResultsController != nil)
{
return __fetchedResultsController;
}
__fetchedResultsController = [self newFetchedResultsControllerWithSearch:nil]; // creates a new FRC
[self filterAllContactsIntoDictionary: __fetchedResultsController];
return [[__fetchedResultsController retain] autorelease];
}
- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{
NSArray *fetchedIDs = [[frc fetchedObjects] valueForKey:#"objectID"];
NSArray *fetched = [frc fetchedObjects];
if (filterMainQueue == nil) {
filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
}
dispatch_async(self.filterMainQueue, ^{
NSManagedObjectContext *backgroundContext = [[[NSManagedObjectContext alloc] init] autorelease];
[backgroundContext setPersistentStoreCoordinator:[[self.fetchedResultsController managedObjectContext] persistentStoreCoordinator]];
NSMutableArray *backgroundObjects = [[NSMutableArray alloc] initWithCapacity: fetchedIDs.count];
// load the NSManagedObjects in this background context
for (NSManagedObjectID *personID in fetchedIDs)
{
Person *personInContext = (Person *) [backgroundContext objectWithID: personID];
[backgroundObjects addObject:personInContext];
}
[self internal_filterFetchedContacts: backgroundObjects]; // loads contacts into custom buckets
// done loading contacts into character buckets ... reload tableview on main thread before moving on
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint savedOffset = [self.tableView contentOffset];
[self.tableView reloadData];
[self.tableView setContentOffset:savedOffset];
});
});
}
What am I doing wrong here? Is there any other way to explicitly make the Person objects fire their faults on the main thread? Or am I doing something wrong with GCD queues and Core Data that I'm not aware of?
Thanks.
Why not take the easy route, since you are not saving anything new ?
Instead of creating an extra context for the background thread and working with IDs, use the main managedObjectContext in the background thread after locking it.
for example:
- (void) filterAllContactsIntoDictionary: (NSFetchedResultsController *) frc
{
if (filterMainQueue == nil) {
filterMainQueue = dispatch_queue_create("com.queue.FilterMainQueue", NULL);
}
dispatch_async(self.filterMainQueue, ^{
NSManagedObjectContext *context = ... // get the main context.
[context lock]; // lock the context.
// do something with the context as if it were on the main thread.
[context unlock]; // unlock the context.
dispatch_async(dispatch_get_main_queue(), ^{
CGPoint savedOffset = [self.tableView contentOffset];
[self.tableView reloadData];
[self.tableView setContentOffset:savedOffset];
});
});
}
This works for me when I call a method with performSelectorInBackground, so I guess it should work for GCD dispatch too.
Well, mergeChangesFromContextDidSaveNotification: is your friend. You'll need to tell the MOC on the main thread that there have been changes elsewhere. This will do the trick.
Here's Apple's documentation. To quote from there:
This method refreshes any objects which have been updated in the other context, faults in any newly-inserted objects, and invokes deleteObject:: on those which have been deleted.
EDIT: original answer removed, OP is not fetching in the background
I looked closer at your code and it doesn't look like you are doing anything that will change data and/or affect the context on the main thread.
You have a fetchedResultsController on the main thread. Presumably, this is working and your table is populating with data. Is this true?
When filterAllContentsIntoDictionary is invoked, you pass an array of the fetchedResultsController's current objectIDs to a background thread and do some processing on them (presumably filtering them based on some criteria) but you are not changing data and saving backgroundContext.
internalFilterFetchedContents is a black box. Without knowing what you intend for it to do, hard to say why it's not working.
When this is done, you reload the table on the main thread.
You haven't made any changes to the store, the context, or the fetchedResultsController so of course, the table shows the same data it did before. The missing details to help further are:
Is your tableView showing correct data from the fetchedResultsController to begin with? If not, most likely your only problem is in handling the tableView delegate and dataSource methods and the rest of this isn't really relevant.
What do you intend to happen in filterAllContentsIntoDictionary and internalFilterFetchedContents?
If your intent is to filter the data as displayed by the fetchedResultsController not sure you need to do anything in the background. If you modify the fetchRequest and do performFetch again your table will reload based on the new results.
I you need more help, please answer my questions, add more relevant code to your post and let me know if I'm missing anything wrt the problem and what you're trying to accomplish.
good luck!