Performance issues using an MPMusicPlayerController - can it be accessed in the background? - objective-c

EDIT: Updating my own question with the answer I figured out months later. Short answer is no, MPMusicPlayerController forwards all calls to the main thread. But it uses a CPDistributedMessagingCenter to actually handle all operations, so one can very easily write a replacement controller that makes asynchronous calls (but it won't work for a sandboxed App Store app, as far as I know - and if it did, Apple would promptly reject it).
I'm making a simple app to control iPod playback, so I've been using an MPMusicPlayerController, which Apple states can only be used in the main thread. However, I've been experiencing some frustrating performance issues in the UI. Switching to the next or previous song is triggered by a swipe, which moves the entire display (of the song info) with it, and then updates the display for the next song when the switch is triggered. The trouble is that once the song has been changed, the UI hangs for up to a second while the song info is retrieved from the MPMusicPlayerController. I've tried most everything I can think of to optimize the code, but it seems to me that the only way to fix it is to move the MPMusicPlayerController code on to a background thread, despite Apple's instructions not to.
The code to update the display, for reference:
// Called when MPMusicPlayerControllerNowPlayingItemDidChangeNotification is received
- (void) nowPlayingDidChange {
if ([iPodMusicPlayer nowPlayingItem]) {
// Temp variables (necessary when updating the subview values in the background)
title = [[iPodMusicPlayer nowPlayingItem] valueForProperty:MPMediaItemPropertyTitle];
artist = [[iPodMusicPlayer nowPlayingItem] valueForProperty:MPMediaItemPropertyArtist];
album = [[iPodMusicPlayer nowPlayingItem] valueForProperty:MPMediaItemPropertyAlbumTitle];
artwork = [[[iPodMusicPlayer nowPlayingItem] valueForProperty:MPMediaItemPropertyArtwork] imageWithSize:CGSizeMake(VIEW_HEIGHT - (2*MARGINS), VIEW_HEIGHT - (2*MARGINS))];
length = [[[iPodMusicPlayer nowPlayingItem] valueForProperty:MPMediaItemPropertyPlaybackDuration] doubleValue];
if (updateViewInBackground)
[self performSelectorInBackground:#selector(updateSongInfo) withObject:nil];
else
[self updateSongInfo];
}
else
[self setSongInfoAsDefault];
}
- (void) updateSongInfo {
// Subviews of the UIScrollView that has performance issues
songTitle.text = title;
songArtist.text = artist;
songAlbum.text = album;
songLength.text = [self formatSongLength:length];
if (!artwork) {
if ([[UIScreen mainScreen] respondsToSelector:#selector(scale)] && [[UIScreen mainScreen] scale] == 2.00)
songArtwork.image = [UIImage imageWithContentsOfFile:
#"/System/Library/Frameworks/MediaPlayer.framework/noartplaceholder#2x.png"];
else
songArtwork.image = [UIImage imageWithContentsOfFile:
#"/System/Library/Frameworks/MediaPlayer.framework/noartplaceholder.png"];
}
else
songArtwork.image = artwork;
title = nil;
artist = nil;
album = nil;
artwork = nil;
length = 0.0;
}
Is there anything I'm missing here (ie. performance optimization when updating the UIScrollView subviews)? And if not, would it be such a bad idea to just use the MPMusicPlayerController in a background thread? I know that can lead to issues if something else is accessing the iPodMusicPlayer (shared instance of the iPod in MPMusicPlayerController), but are there any ways I could potentially work around that?
Also, this is a jailbreak tweak (a Notification Center widget), so I can make use of Apple's private frameworks if they would work better than, say, the MPMusicPlayerController class (which is fairly limited anyways) for my purposes. That also means, though, that my app will be running as a part of the SpringBoard process, so I want to be sure that my code is as safe and stable as possible (I experience 2 minute hangs whenever my code does something wrong, which I don't want happening when I release this). So if you have any suggestions, I'd really appreciate it! I can provide more code / info if necessary. Thanks!

Related

App crashes in main after adding delegate protocol; no error code

In my app, a graph in one view can be dragged to a second view so that the new graph replaces the second view (like a copy/paste effect with a drag and drop feature). The app works if the delegate protocol is taken out so that the second view handles the change in function itself. When the protocol is added, the app crashes in the main file at
return UIApplicationMain(argc, argv, nil, NSStringFromClass([Load_CreatorAppDelegate class]));.
There isn't any error output other than the standard (lldb). Even when I take out the call to the delegate (keeping in the code), the app crashes. I know that it has to be related to the protocol code, though, because it worked fine before that.
Here is part of the code for the second view (BeamView):
[self drawSupportsAtLeftPoint:self.beamBottomLeft rightPoint:self.beamBottomRight inContext:context :leftPin :rightPin];
BOOL pt = NO;
if (self.tempLoad) {
//self.loadGraph = [self.dataSource changeToTempLoad:self]; NOTE #1
//if (self.tempPtLoad.x != 0 || self.tempPtLoad.y != 0) pt = YES;
pt = [self changeLoad];
[self drawLoadWithFunction:self.loadGraph inContext:context fromPoint:self.beamTopLeft toPoint:self.beamTopRight withAlpha:0.3 isPointLoad:pt inBlack:YES];
}
else {
self.loadGraph = ^(int x) {return x/15;};
[self drawLoadWithFunction:self.loadGraph inContext:context fromPoint:self.beamTopLeft toPoint:self.beamTopRight withAlpha:1 isPointLoad:pt inBlack:NO];
}
self.tempLoad = NO;
NOTE #1: These lines that are commented out are the ones that call on the delegate. Those two methods and their implementation are the only changes I made.
I'm completely confused, any help would be greatly appreciated. What are possible reasons the app will crash in the main file?
Ok! I feel kind of stupid, but it turned out that the crash didn't have anything to do with delegation (well, kind of). I had deleted outlets in the ViewController.m file without disconnecting them in IB, which was causing the crash.
I'd forgotten that I'd done that, and so it took me a while to think of it--it wasn't until I went back to an older saved version that I saw the differences.

Cache thousands of images

I've been developing a music player recently, I'm writing my own pickers.
I'm trying to test my code to it's limits, so I have around 1600 albums in my iPhone.
I'm using AQGridView for albums view, and since MPMediaItemArtwork is a subclass of NSObject, you need to fire up a method on it to get an image from it, and that method scales images.
Scaling for each cell uses too much CPU as you can guess, so my grid album view is laggy, despite all my effort manually driving each cell's includes.
So I thought of start scaling with GCD on app launch, then save them to file, and read that file for each cell.
But, my code
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^ {
MPMediaQuery *allAlbumsQuery = [MPMediaQuery albumsQuery];
NSArray *albumsArray = allAlbumsQuery.collections;
for (MPMediaItemCollection *collection in albumsArray) {
#autoreleasepool {
MPMediaItem *currentItem = [collection representativeItem];
MPMediaItemArtwork *artwork = [currentItem valueForProperty:MPMediaItemPropertyArtwork];
UIImage *artworkImage = [artwork imageWithSize:CGSizeMake(90, 90)];
if (artworkImage) [toBeCached addObject:artworkImage];
else [toBeCached addObject:blackImage];
NSLog(#"%#", [currentItem valueForProperty:MPMediaItemPropertyAlbumTitle]);
artworkImage = nil;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
[[NSUserDefaults standardUserDefaults] setObject:[NSKeyedArchiver archivedDataWithRootObject:albumsArray] forKey:#"covers"];
});
NSLog(#"finished saving, sir");
});
in AppDelegate's application:didFinishLaunchingWithOptions: method makes my app crash, without any console log etc.
This seems to be a memory problem, so many images are kept in NSArray which is stored on RAM until saving that iOS force closes my app.
Do you have any suggestions on what to do?
Cheers
Take a look at the recently-released SYCache, which combines NSCache and on-disk caching. It's probably a bad idea to get to a memory-warning state as soon as you launch the app, but that's better than force closing.
As far as the commenter above suggested, mapped data is a technique (using mmap or its equivalent) to load data from disk as if it's all in memory at once, which could help with UIImage loading later on down the road. The inverse (with NSMutableData) is also true, that a file is able to be written to as if it's directly in RAM. As a technique, it could be useful.

Perform action after label is updated in ios

I am using a pin screen for login to my app. The pin screen consists of four labels and a hidden text field. When the user enters text via the keypad, I update the labels with a symbol. This works fine, except that the last label does not get actually get updated before login begins, and remains empty while the login process is completed.
These are the relevant bits of code:
//an observer has been added elsewhere
- (void)textDidChange:(NSNotification *)notification
{
UITextField *field = [notification object];
if (field == inputField)
{
NSString *newText = field.text;
if ([newText length] <= pinLength) [self updatePINDisplay];
}
}
-(void)updatePINDisplay
{
if ([pinText length] > pinLength) return;
for (NSInteger ii = 0; ii < [pinText length]; ii++)
{
UILabel *label = [pinFields objectAtIndex:ii];
[label setText:#"x"];
}
for (NSInteger ii = [pinText length]; ii < pinLength; ii++)
{
UILabel *label = [pinFields objectAtIndex:ii];
[label setText:[NSString string]];
}
if ([pinText length] == pinLength) [self login];
}
The problem arises because [self login] launches other processes which happen before the last pin label is updated, so the login occurs while the last box is still empty.
I have worked around the problem by replacing
[self login]
with
[self performSelector:#selector(login) withObject:nil afterDelay:0.1]
but I don't like the arbitrary time delay. I was hoping that maybe there was a delegate method that I could use to launch my login code after the label has been drawn. Something like:
-(void)labelDidGetDrawn
Any other (non-hack) solution is also welcome:-)
Thanks!
Based on your description, it sounds like the problem is that the 4th item doesn't get drawn until after the [self login] finishes, which is indicative that the login procedure takes some time. In iOS, drawing doesn't happen immediately, which is why you're only getting the draw if you defer the login until after the OS has an opportunity to update the display.
You have used one reasonable solution here. Another (arguably less of a hack) is to have your -[self login] spawn the login on a separate thread, or at least using an asynchronous mechanism (such as the asynchronous modes of NSURLConnection, assuming you're making a network request). Then your main thread will quickly return control to iOS and your box will draw.
With Grand Central Dispatch, you could do most of this by having the -[self login] place the network code on a background thread, and have the background thread call back to your main thread when complete. However, this can cause some problems if you want to respond to user events during the login process.
If you can, using NSURLConnection asynchronously, and setting up the delegate to report back to you when the operation is complete is probably the best choice, as it gives you the operation to cancel the NSURLConnection during the login process if the user requests it.
How about:
[label setNeedsDisplay:YES];
if ([pinText length] == pinLength) [self login];
Yes, that notification exists, in a way. The label will be drawn during the next iteration of the run loop. So do your login at the end of the next run loop iteration, for instance using a performSelector:afterDelay:0 or maybe using
dispatch_async (dispatch_get_main_queue (), ^{ [self login]; });
But a) this depends on the order of execution of rendering versus timers and dispatch_queues. If rendering happens before timer execution, you're all set.
And b) don't block the main thread. Try to perform the login in a background thread/concurrent queue, or do it asynchronously on the main thread if you're using, e.g., NSURLConnection.

Game Center: authenticates ok, shows leaderboard ok, posts (my) score ok, only 2 players?

My game (called "Clear the Square") has gotten approved. It's getting hundreds of downloads. It's currently #44 in free word games in the US. There is only one problem: when I go to the Game Center leaderboards, it shows two users, one of them being me. And one of them being (very appropriately named) someone who isn't me. Here is a screenshot of what this looks like: http://clearthesquareapp.com/photo-9.PNG
I know that there are more than two people playing this (iAd tells me so.) I also know that the game is not unwinnable; somewhat challenging, but not unwinnable. And I know that Game Center didn't just completely go out of style overnight. In all other games that have ever been anywhere near a top-100 list, there are at least a few hundred all-time scores on the leaderboard.
Thinking that the problem might be that my development build is somehow special and goes to an alternate, "sandbox" set of leaderboards, I deleted the binary from my phone, rebooted my phone, and downloaded the game from the App Store. Same thing. So that wasn't the problem.
Here's all my GameCenter code; it comes straight from example projects. I don't know what could be missing, since it does successfully authenticate, it does show leaderboards, and it does post my score -- each time I set a high score, it is instantly reflected in the leaderboard.
I would really, really appreciate it if someone could download the free version of my game, and send me a screenshot of what the leaderboards look like on your end. You would definitely get promo codes for every future app I make.
- (void) authenticateLocalPlayer
{
GKLocalPlayer *localPlayer = [GKLocalPlayer localPlayer];
[localPlayer authenticateWithCompletionHandler:^(NSError *error) {
if (localPlayer.isAuthenticated)
{
NSLog(#"authenticated in game center!");
// Perform additional tasks for the authenticated player.
}
}];
}
- (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
}
}];
}
- (void) showLeaderboard
{
GKLeaderboardViewController *leaderboardController = [[GKLeaderboardViewController alloc] init];
if (leaderboardController != nil)
{
leaderboardController.leaderboardDelegate = self;
[self presentModalViewController: leaderboardController animated: YES];
}
}
- (void) leaderboardViewControllerDidFinish:(GKLeaderboardViewController *)viewController {
[self dismissModalViewControllerAnimated:YES];
}
There should be more than two players on this leaderboard. What am I missing?
Edit: also, worth noting that it has already been out for about 48 hours, and probably has about 1000 downloads so far. So I am quite sure that it's not just a matter of nobody having had time to win a game yet.
You're probably still signed in to your "sandbox" Game Center account. Log out of that, and then log into your real account, and you should see the real leaderboards.
I ran into this problem myself, and it sometimes didn't "take" to log out and back in, so try a couple times. I think I may have had to kill (via the double-tap home button) the Game Center app after logging out of the sandbox, and before logging into the real account, for it to work.

iPhone SDK: How to know when background task has completed?

We are trying to get a background task working for the purpose of including an activity indicator in a workhouse screen. From our understanding, this requires one to create a background thread to run it on. I also understand that no GUI updates can be performed on the background thread.
Given that, here is the general pattern of what needs to happen.
a.) Pre-validate fields. Make sure user did not enter any invalid data
b.) Setup background task.
c.) Process results from background task
This is what it looks like in code so far:
-(IBAction)launchtask:(id)sender
{
//validate fields
[self validateFields];
/* Operation Queue init (autorelease) */
NSOperationQueue *queue = [NSOperationQueue new];
/* Create our NSInvocationOperation to call loadDataWithOperation, passing in nil */
NSInvocationOperation *operation = [[NSInvocationOperation alloc] initWithTarget:self
selector:#selector(backgroundTask)
object:nil];
/* Add the operation to the queue */
[queue addOperation:operation];
[operation release];
//TO DO: Add any post processing code here, BUT how do we know when it is done???
ConfirmationViewController *otherVC;
//show confirm
//if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
//{
// otherVC = [[ConfirmationViewController alloc] initWithNibName:#"ConfirmationViewPad" bundle:nil];
//}
//else
{
otherVC = [[ConfirmationViewController alloc] initWithNibName:#"ConfirmationView" bundle:nil];
}
//TO DO: Let's put this in a struct
otherVC.strConfirmation = strResponse;
otherVC.strCardType = strCardType;
otherVC.strCardNumber = txtCardNumber.text;
otherVC.strExpires = txtExpires.text;
otherVC.strCustomerEmail = txtEmail.text;
[self.navigationController pushViewController:otherVC animated:YES];
[otherVC release];
otherVC = nil;
}
So far, that works pretty well except that we don't yet have a way to know when the background task is complete. Only when it is complete, can we process the results of the background task. Right now, it doesn't work because there is not synchronization to the two. How to solve?
One other thing, noticed that a spinner is now displayed in the status bar. That is a good thing but it doesn't seem to be going away after the background task has completed? What to do?
Thanks in advance.
Your options are, briefly:
key value observe the 'operationCount' property on NSOperationQueue and wait for it to reach 0 (or, equivalently, the 'operations' property and check the count)
have your operations fire off a little notification that they're done (probably on the main thread with performSelectorOnMainThread:...) and wait until the correct number of notifications have been received.
[EDIT: I see you've asked specifically about the old SDK 3.0. In that case, observe operations and check count because the operationCount property postdates SDK 3.0]
There's no automatic system for starting and stopping a spinner in the general case. You'll have to talk to it yourself. However, a neat thing about a spinner is that it continues spinning even if the main thread is blocked, so if you're thread hopping just for that purpose then you don't actually need to.
A spinner appears in the status bar to show data fetches, I believe. If it continues spinning then you still have URL requests ongoing, whether or not you're actually waiting for the results.