I'm using the CocoaLibSpotify library to load album art for Spotify search results.
Instruments reports no leaks, and static analysis isn't helping out either, and I've manually reviewed all of my code that deals with keeping track of loading the album art, yet, after loading a few hundred results, the app consumes over 100mb of memory and crashes.
I believe that CocoaLibSpotify is keeping a cache of the images in memory, but there is no way that I have found of disabling the cache. There is a "flushCaches" method, which I've been calling each time I get a memory warning, but, it is ineffective.
Here's what I'm using to load the album art, I keep a reference to all of the SPImage objects in an array, so that I can use them when serving up table view rows.
[self sendRequestToURL: #"http://ws.spotify.com/search/1/track.json" withParams: #{#"q": spotifySearchBar.text} usingMethod: #"GET" completionHandler: ^(id result, NSError *error) {
//after the search completes, re-enable the search button, replace the searchResults, and
// request the result table to reload the data
spotifySearchBar.userInteractionEnabled = YES;
[searchBar endEditing: YES];
[searchResults release];
int resultLength = [[result objectForKey: #"tracks"] count] < 100 ? [[result objectForKey: #"tracks"] count] : 100;
searchResults = [[[result objectForKey: #"tracks"] subarrayWithRange: NSMakeRange(0, resultLength)] retain];
for(int i = 0; i < 100; i++) {
[albumArtCache replaceObjectAtIndex: i withObject: [NSNull null]];
}
for(NSDictionary *trackDict in searchResults) {
NSString *trackURI = [trackDict objectForKey: #"href"];
[SPTrack trackForTrackURL: [NSURL URLWithString: trackURI] inSession: session callback: ^(SPTrack *track) {
[SPAsyncLoading waitUntilLoaded: track timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
if(track == nil) return;
[SPAsyncLoading waitUntilLoaded: track.album timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
if(track.album == nil) return;
[SPAsyncLoading waitUntilLoaded: track.album.largeCover timeout: kSPAsyncLoadingDefaultTimeout then:^(NSArray *loadedItems, NSArray *notLoadedItems) {
if(track.album.largeCover == nil) return;
if(![searchResults containsObject: trackDict]) {
NSLog(#"new search was performed, discarding loaded result");
return;
} else{
[albumArtCache replaceObjectAtIndex: [searchResults indexOfObject: trackDict] withObject: track.album.largeCover];
[resultTableView reloadRowsAtIndexPaths: #[[NSIndexPath indexPathForRow: [searchResults indexOfObject: trackDict] inSection: 0]] withRowAnimation: UITableViewRowAnimationAutomatic];
}
}];
}];
}];
}];
}
[resultTableView reloadData];
}];
And here is the code that deals with loading table view cells.
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath {
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier: #"artistCell"];
if(cell == nil) {
cell = [[[UITableViewCell alloc] initWithStyle: UITableViewCellStyleSubtitle reuseIdentifier: #"artistCell"] autorelease];
}
cell.textLabel.text = [[searchResults objectAtIndex: indexPath.row] objectForKey: #"name"];
cell.detailTextLabel.text = [[[[searchResults objectAtIndex: indexPath.row] objectForKey: #"artists"] objectAtIndex: 0] objectForKey: #"name"];
if([albumArtCache objectAtIndex: indexPath.row] != [NSNull null]) {
cell.imageView.image = ((SPImage *)[albumArtCache objectAtIndex: indexPath.row]).image;
} else{
cell.imageView.image = nil;
}
return cell;
}
I really have no idea what's going wrong. Any assistance would be greatly appreciated.
First off, you should use SPSearch rather than the web API for searching.
The reason that Instruments isn't showing a memory leak is because there isn't one - CocoaLibSpotify caches albums and images internally for performance reasons. As a result, loaded album covers will also stick around.
Now, loading hundreds of 1024x1024 images into memory is obviously going to end badly. An easy way to mitigate the problem would be to not load the largest size image - it's not normally required for a table view at 1024x1024 pixels.
Otherwise, you can modify CocoaLibSpotify to be able to unload images. The easiest way to do this is to probably add a method to SPImage that basically does the opposite of -startLoading - namely, setting the image property to nil, the hasStartedLoading and loaded properties to NO and calling sp_image_release on the spImage property before setting that to NULL.
Related
I'm using Apple's KMLViewer to load a KML file and display it in a MapView. There are over 50,000 lines of coordinates in the KML file which, of course, causes it to load slowly. In an attempt to speed things up, I'm trying to perform the parsing in another thread using GCD.
I have it working reasonably well as far as it is displaying properly and the speed is acceptable. However, I'm getting intermittent runtime errors when loading the map. I suspect it is because the way I have things laid out, the UI is being updated within the GCD block. Everything I'm reading says the UI should be updated in the main thread or else runtime errors can occur which are intermittent and hard to track down. Well, that's what I'm seeing.
The problem is, I can't figure out how to update the UI in the main thread. I'm still new to iOS programming so I'm just throwing things against the wall to see what works. Here is my code, which is basically Apple's KMLViewerViewController.m with some modifications:
#import "KMLViewerViewController.h"
#implementation KMLViewerViewController
- (void)viewDidLoad
{
[super viewDidLoad];
activityIndicator.hidden = TRUE;
dispatch_queue_t myQueue = dispatch_queue_create("My Queue",NULL);
dispatch_async(myQueue, ^{
// Locate the path to the route.kml file in the application's bundle
// and parse it with the KMLParser.
NSString *path = [[NSBundle mainBundle] pathForResource:#"BigMap" ofType:#"kml"];
NSURL *url = [NSURL fileURLWithPath:path];
kmlParser = [[KMLParser alloc] initWithURL:url];
[kmlParser parseKML];
dispatch_async(dispatch_get_main_queue(), ^{
// Update the UI
// Add all of the MKOverlay objects parsed from the KML file to the map.
NSArray *overlays = [kmlParser overlays];
[map addOverlays:overlays];
// Add all of the MKAnnotation objects parsed from the KML file to the map.
NSArray *annotations = [kmlParser points];
[map addAnnotations:annotations];
// Walk the list of overlays and annotations and create a MKMapRect that
// bounds all of them and store it into flyTo.
MKMapRect flyTo = MKMapRectNull;
for (id <MKOverlay> overlay in overlays) {
if (MKMapRectIsNull(flyTo)) {
flyTo = [overlay boundingMapRect];
} else {
flyTo = MKMapRectUnion(flyTo, [overlay boundingMapRect]);
}
}
for (id <MKAnnotation> annotation in annotations) {
MKMapPoint annotationPoint = MKMapPointForCoordinate(annotation.coordinate);
MKMapRect pointRect = MKMapRectMake(annotationPoint.x, annotationPoint.y, 0, 0);
if (MKMapRectIsNull(flyTo)) {
flyTo = pointRect;
} else {
flyTo = MKMapRectUnion(flyTo, pointRect);
}
}
// Position the map so that all overlays and annotations are visible on screen.
map.visibleMapRect = flyTo;
});
});
}
-(void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
activityIndicator.hidden = FALSE;
[activityIndicator startAnimating];
}
#pragma mark MKMapViewDelegate
- (MKOverlayView *)mapView:(MKMapView *)mapView viewForOverlay:(id <MKOverlay>)overlay
{
return [kmlParser viewForOverlay:overlay];
}
- (MKAnnotationView *)mapView:(MKMapView *)mapView viewForAnnotation:(id <MKAnnotation>)annotation
{
return [kmlParser viewForAnnotation:annotation];
}
- (void)mapViewDidFinishLoadingMap:(MKMapView *)mapView
{
[activityIndicator stopAnimating];
activityIndicator.hidden = TRUE;
}
#end
Suggestions?
I would have thought NSFileManagers method of removeItemAtURL:error: would remove the Core Data log files created when using UIManagedDocuments with iCloud.
What is the best way to make sure all of these log files are removed?
I have used...
- (void)deleteRemnantsOfOldDatabaseDocumentAndItsTransactionLogsWithCompletionHandler:(completion_success_t)completionBlock
{
__weak CloudController *weakSelf = self;
NSURL *databaseStoreFolder = self.iCloudDatabaseStoreFolderURL;
NSURL *transactionLogFolder = self.transactionLogFilesFolderURL;
[self deleteFileAtURL:databaseStoreFolder withCompletionBlock:^(BOOL docSuccess) {
[weakSelf deleteFileAtURL:transactionLogFolder withCompletionBlock:^(BOOL logSuccess) {
completionBlock(docSuccess && logSuccess);
}];
}];
}
In conjunction with...
- (void)deleteFileAtURL:(NSURL *)fileURL withCompletionBlock:(completion_success_t)completionBlock
{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
NSError *coordinatorError = nil;
__block BOOL success = NO;
[fileCoordinator coordinateWritingItemAtURL:fileURL
options:NSFileCoordinatorWritingForDeleting
error:&coordinatorError
byAccessor:^(NSURL *writingURL) {
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSError *removalError = nil;
if ([fileManager fileExistsAtPath:[writingURL path]]) {
if (![fileManager removeItemAtURL:writingURL error:&removalError]) {
NSLog(#"deleteFileAtURL: removal error: %#", removalError);
} else {
success = YES;
}
}
}];
if (coordinatorError) {
NSLog(#"deleteFileAtURL: coordinator error: %#", coordinatorError);
}
completionBlock(success);
});
}
Note: this was used for a single document toolbox style app, and was intended more for clearing out the iCloud container before creating a brand new document, in an 'apparently' empty iCloud store for the first time. But I'm sure it can be adapted without too much work.
Oops, the above won't make sense/work without:
typedef void (^completion_success_t)(BOOL success);
You can debug the contents of your iCloud container and verify things have been removed by using a method like (which to be honest I've probably lifted from somewhere else and modified):
- (void)logDirectoryHierarchyContentsForURL:(NSURL *)url
{
NSFileManager *fileManager = [NSFileManager defaultManager];
NSDirectoryEnumerator *directoryEnumerator = [fileManager enumeratorAtURL:url
includingPropertiesForKeys:#[NSURLNameKey, NSURLContentModificationDateKey]
options:NSDirectoryEnumerationSkipsHiddenFiles
errorHandler:nil];
NSMutableArray *results = [NSMutableArray array];
for (NSURL *itemURL in directoryEnumerator) {
NSString *fileName;
[itemURL getResourceValue:&fileName forKey:NSURLNameKey error:NULL];
NSDate *modificationDate;
[itemURL getResourceValue:&modificationDate forKey:NSURLContentModificationDateKey error:NULL];
[results addObject:[NSString stringWithFormat:#"%# (%#)", itemURL, modificationDate]];
}
NSLog(#"Directory contents: %#", results);
}
And it's also worth logging onto developer.icloud.com and examining what is actually in the iCloud store. There is sometimes a difference between what is retained in the device ubiquity container, and what is actually in the iCloud server folder structure. Between all of these you can get quite a good idea of what's going on.
i want to ask a question about core location and core data. i looked some questions but couldnt do that..
i have a application which stores some textfields, photos, date and time datas in UITableView With core data, i stored everything (photos, texts, date etc.) Now trying to store Location data i couldnt do.
this is some of my code here.
#pragma mark - View lifecycle
- (void)viewDidLoad
{
[super viewDidLoad];
locationManager = [[CLLocationManager alloc] init];
locationManager.delegate = self;
locationManager.desiredAccuracy = kCLLocationAccuracyBest;
[locationManager startUpdatingLocation];
NSDateFormatter *myFormatter = [[NSDateFormatter alloc] init];
[myFormatter setDateFormat:#"MM-dd-yyyy HH:mm"];
[myFormatter setTimeZone:[NSTimeZone systemTimeZone]];
todaysDate = [myFormatter stringFromDate:[NSDate date]];
myDateLabel.text = todaysDate;
UIView *patternBg = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 320, 480)];
patternBg.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:#"background01.png"]];
self.tableView.backgroundView = patternBg;
// If we are editing an existing picture, then put the details from Core Data into the text fields for displaying
if (currentPicture)
{
[companyNameField setText:[currentPicture companyName]];
[myDateLabel setText:[currentPicture currentDate]];
if ([currentPicture photo])
[imageField setImage:[UIImage imageWithData:[currentPicture photo]]];
}
}
in the saveButton
- (IBAction)editSaveButtonPressed:(id)sender
{
// For both new and existing pictures, fill in the details from the form
[self.currentPicture setCompanyName:[companyNameField text]];
[self.currentPicture setCurrentDate:[myDateLabel text]];
[self.currentPicture setCurrentTime:[myTimeLabel text]];
[self.currentPicture setLatitudeData:[_latitudeLabel text]];
[self.currentPicture setLongtidueData:[_longtitudeLabel text]];
}
and last one, my locationManager's method..
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
NSLog(#"didUpdateToLocation: %#", newLocation);
CLLocation *currentLocation = newLocation;
if (currentLocation != nil) {
_longtitudeLabel.text = [NSString stringWithFormat:#"%.8f", currentLocation.coordinate.longitude];
_latitudeLabel.text = [NSString stringWithFormat:#"%.8f", currentLocation.coordinate.latitude];
[self->locationManager stopUpdatingLocation];
}
}
i tried "[locationmanager stopUpdatingLocation];" many times, but when i entered the app, code starts to calculating latitude and longtitude data, i just want to take that data 1 time, and store..
Thanks!
If calling stopUpdatingLocation doesn't stop location updates, then most likely self->locationManager is nil. That would mean you're not really making the call.
It's hard to be sure exactly why this would happen, except that your code seem to make a point of not using any semantics implied by a #property declaration. Just assigning to location in viewDidLoad avoids any declaration, and looking up the manager using self->locationManager does as well. Assuming that location is a property, you should assign it to self.locationManager, and use that when looking it up as well.
A couple things:
- (void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation
{
NSTimeInterval locationAge = -[newLocation.timestamp timeIntervalSinceNow];
if (locationAge > 5) return; // ignore cached location, we want current loc
if (newLocation.horizontalAccuracy <= 0) return; // ignore invalid
// wait for GPS accuracy (will be < 400)
if (newLocation.horizontalAccuracy < 400) {
_longtitudeLabel.text = [NSString stringWithFormat:#"%.8f", newLocation.coordinate.longitude];
_latitudeLabel.text = [NSString stringWithFormat:#"%.8f", newLocation.coordinate.latitude];
[manager stopUpdatingLocation];
}
}
In your didUpdateToLocation do this code
(void)locationManager:(CLLocationManager *)manager didUpdateToLocation:(CLLocation *)newLocation fromLocation:(CLLocation *)oldLocation {
NSTimeInterval locationAge = -[newLocation.timestamp timeIntervalSinceNow];
if (locationAge > 5) return;
// ignore cached location, we want current loc
if (newLocation.horizontalAccuracy <= 0) return; // ignore invalid
// wait for GPS accuracy (will be < 400)
if (newLocation.horizontalAccuracy < 400) {
_longtitudeLabel.text = [NSString stringWithFormat:#"%.8f", newLocation.coordinate.longitude];
_latitudeLabel.text = [NSString stringWithFormat:#"%.8f", newLocation.coordinate.latitude];
[manager stopUpdatingLocation];
//assign nil to locationManager object and delegate
locationManager.delegate = nil;
locationManager = nil;
}
}
Thanks.
I am saving the latest internet request of my tableviewdata in an (core data) entity, but have problems with error exceptions about "faults".
I have two methods 'loadData' which gets the latest 'ordersitems' that will be loaded in my tableview AND 'loadThumbnails' which will try to cache the thumbnail into the core data entity.
The problem occurs when the managedobject gets deleted and the thumbnail method still tries to access it. Though i made a variable stopThumbnails to stop the loadThumbnails method, the problem keeps occurring.
What is the proper iOS 6 way to lazyload the images and save them to coredata but check if the object has not been deleted? i found this Core Data multi thread application which was useful but my newbie understanding of core data is still limited and i have problems writing code. I read the apple docs about http://developer.apple.com/library/ios/#documentation/cocoa/conceptual/coredata/Articles/cdConcurrency.html but it was hard to understand completely.
I want at least my http request to load asychronous (but preferably as much as possible) i came up with the following:
-(void)viewdidload
{
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:#"OrderItems"];
fetchReq.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.data = [self.managedObjectContext executeFetchRequest:fetchReq error:nil];
MYFILTER = #"filter=companyX";
[self loadData];
}
-(void)loadData
{
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
//json request from url
NSDictionary *reqData = myOrderJSONRequest(MYFILTER);
dispatch_async( dispatch_get_main_queue(), ^{
if(reqData!=NULL && reqData!=nil)
{
//request successful so delete all items from entity before inserting new ones
stopThumbnails = YES;
for(int i=self.data.count-1;i>=0;i--)
{
[self.managedObjectContext deleteObject:[self.data objectAtIndex:i]];
}
[self.managedObjectContext save:nil];
if(reqData.count>0)
{
//insert latest updates
for (NSDictionary *row in reqData){
OrderItem *item = [NSEntityDescription insertNewObjectForEntityForName:#"OrderItem" inManagedObjectContext:self.managedObjectContext];
item.order_id = [NSNumber numberWithInt:[[row objectForKey:#"order_id"] intValue]];
item.description = [row objectForKey:#"description"];
item.thumbnail_url = [row objectForKey:#"thumbnail_url"];
}
[self.managedObjectContext save:nil];
}
NSFetchRequest *fetchReq = [NSFetchRequest fetchRequestWithEntityName:#"OrderItems"];
fetchReq.sortDescriptors = [NSArray arrayWithObject:[NSSortDescriptor sortDescriptorWithKey:#"name" ascending:YES]];
self.data = [self.managedObjectContext executeFetchRequest:fetchReq error:nil];
[TableView reloadData];
//LOAD THUMBNAILS ASYNCHRONOUS
stopThumbnails = NO;
[self loadThumbnails];
}
else{
//NO INTERNET
}
});
});
}
-(void)loadThumbnails
{
if(!loadingThumbnails)
{
loadingThumbnails = YES;
dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
for (int i=0;i<self.data.count; i++) {
if(!stopThumbnails)
{
OrderItem *item = [self.data objectAtIndex:i];
if(item.thumbnail==NULL)
{
//ASYNCHRONOUS IMAGE REQUEST
NSURL *image_url = [NSURL URLWithString:item.thumbnail_url];
NSData *image_data = [NSData dataWithContentsOfURL:image_url];
dispatch_async( dispatch_get_main_queue(), ^{
if(image_data!=nil && image_data!=NULL && !stopThumbnails)
{
//IMAGE REQUEST SUCCESSFUL
item.thumbnail = image_data;
[self.managedObjectContext save:nil];
//RELOAD AFFECTED TABLEVIEWCELL
NSIndexPath* rowToReload = [NSIndexPath indexPathForRow:i inSection:0];
NSArray* rowsToReload = [NSArray arrayWithObjects:rowToReload, nil];
[TableView reloadRowsAtIndexPaths:rowsToReload withRowAnimation:UITableViewRowAnimationFade];
}
else
{
loadingThumbnails = NO;
return;
}
});
}
if(stopThumbnails)
{
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
}
}
else{
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
}
}
dispatch_async( dispatch_get_main_queue(), ^{
loadingThumbnails = NO;
return;
});
});
}
}
Any help is of course greatly appreciated :)
Well i dont know if this is the right approach but it works, so i'll mark this as an answer.
To do everything on the background i used a second nsmanagedobjectcontext (MOC) and then merge the changes to the main MOC. the dispatch queue works great although i had to use the NSManagedObjectContextDidSaveNotification in order to merge the changes of the two contexts.
since IOS 5 its possible to use blocks instead that do the merging for you. So i decided to use this instead of the dispatch way (this way i didnt have to use notofications).
Also using blocks i got the same problem (faults) when an object got selected on a background queue while is was deleted on a different queue. So i decided instead of deleting it right away, insert a NSDate 'deleted' property for the OrderItem. then have a timer with a delete check to see if there are objects that have been deleted longer than 10 minutes ago. This way i am sure no thumbnail was still downloading. It works. Though i would still like to know id this is the right approach :)
I am not sure if sandbox is taking too long to update or if my code is funky.
I am simply grabbing the local players last entered score and adding another score to it and trying to post the result.
Here is my code:
- (void) reportScore: (int64_t) score forCategory: (NSString*) category
{
GKScore *scoreReporter = [[[GKScore alloc]initWithCategory:category] autorelease];
scoreReporter.value = score;
[scoreReporter reportScoreWithCompletionHandler:^(NSError *error) {
if (error != nil)
{
// handle the reporting error
NSLog(#"Error reporting score");
}
}];
}
-(void)postScore:(int64_t)score forCategory:(NSString *)category {
GKLeaderboard *query = [[GKLeaderboard alloc]init];
query.category = category;
if (query != nil)
{
[query loadScoresWithCompletionHandler: ^(NSArray *scores, NSError *error) {
if (error != nil){
// Handle the error.
NSLog(#"Error loading scores");
}
if (scores != nil){
// Process the score.
int64_t newScore = query.localPlayerScore.value + score;
[self reportScore:newScore forCategory:category];
}
}];
}
[query release];
}
Thanks for any help.
EDIT: Sandbox leaderboard has the first score, but will not update the subsequent scores.
Having the same issue at my end. It will provide the score correctly for the first time in a session. After that, it keep sending back the same score even if we update the score in that session.
You need to check property of GKleaderBoard class.For Your Info. see below code.
GKLeaderboardViewController *leaderController = [[GKLeaderboardViewController alloc] init];
if (leaderboardController != NULL)
{
leaderController.category = self.currentLeaderBoard;
leaderController.timeScope = GKLeaderboardTimeScopeWeek;
leaderController.leaderboardDelegate = self;
[self presentModalViewController: leaderController animated: YES];
}
AND
you can also check apple docs for both GKLeaderBoard and GKAchievementViewController class below.
for GKLeaderBoard
http://developer.apple.com/library/ios/#documentation/GameKit/Reference/GKLeaderboard_Ref/Reference/Reference.html
for GKAchievementViewController
http://developer.apple.com/library/ios/#documentation/GameKit/Reference/GKAchievementViewController_Ref/Reference/Reference.html