Recursive blocks == out of memory silent crash - objective-c

I'm using Blocks in iOS5 with ARC to play a never ending cycle of audio recordings with AVAudioPlayer. I alloc a new AVAudioPlayer after receiving the AVAudioPlayerDidFinishRecording message, which starts a cycle of recursion. My app consistently crashes after 120 or so audio clips are played back. I can see that it uses up more and more memory as each audio file is loaded, but never releases the memory.
I'm using a single AVAudioPlayer property, so I was expecting ARC to clean up the old instances every time I alloc a new player.
Could the blocks be somehow causing the old AVAudioPlayers to stick around?
Would it be better to use an Observer to monitor the player and launch/alloc new players?
Thanks so much for your help!
-(void)playNextAudioItem{
//get ready to load audio doc from iCloud
NSURL *ubiquitousContainer = [[NSFileManager defaultManager] URLForUbiquityContainerIdentifier:nil];
if(ubiquitousContainer){
//query iCloud
query = [[NSMetadataQuery alloc]init];
[query setSearchScopes:[NSArray arrayWithObject:NSMetadataQueryUbiquitousDocumentsScope]];
NSPredicate *pred = [NSPredicate predicateWithFormat:#"(%K == %#)", NSMetadataItemFSNameKey, [NSString stringWithFormat:#"%#.caf", audioItemGUID]];
[query setPredicate:pred];
[[NSNotificationCenter defaultCenter] addObserverForName:NSMetadataQueryDidFinishGatheringNotification object:query queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *notif){
//iCloud query finished gathering data
[query disableUpdates];
[[NSNotificationCenter defaultCenter] removeObserver:self name:NSMetadataQueryDidFinishGatheringNotification object:query];
NSMetadataItem *item1 = [query resultAtIndex:0];
audioCurrentVerse = [[audioDoc alloc] initWithFileURL:[item1 valueForAttribute:NSMetadataItemURLKey]];
//open the audio file
[audioCurrentVerse openWithCompletionHandler:^(BOOL success) {
if(success){
//configure the session and play the audio file
AVAudioSession *audioSession = [AVAudioSession sharedInstance];
[audioSession setActive:YES error:nil];
NSError *error = nil;
[audioSession setCategory:AVAudioSessionCategoryPlayback error:nil];
audioPlayer = [[AVAudioPlayer alloc] initWithData:audioCurrentVerse.data error:&error];
audioPlayer.numberOfLoops = 0;
audioPlayer.delegate = self;
if([audioPlayer prepareToPlay]){
[audioPlayer play];
}
}
}];
[query stopQuery];
query = nil;
}];
[query startQuery];
}
}
-(void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)player successfully:(BOOL)flag{
//get an identifier for the next audio item
audioItemIndex++;
if(audioItemIndex >= rsAudioItems.recordCount){
verseIndex = 0;
}
audioItemGUID = [rsAudioItems getRow:audioItemIndex andColumnNamed:#"audioItemGUID"];
//recursive call to play the next audio item
NSLog([NSString stringWithFormat:#"%d", playCount++]);
[self playNextAudioItem];
}

Blocks take a snapshot of the stack that is not released until the block is freed. Don't use recursive blocks (or really recursion at all, if it doesn't really make sense in the situation) because the blocks never get freed so the snapshots stick around forever.
Using a for loop would alleviate this issue. You could add them to a serial dispatch queue and then they'd be freed after execution.

Related

freeing memory in a simple avaudioengine program?

I'm wondering how to free memory in this simple program that plays a file through a buffer and then stops it.
-(void)setupAudioOne
{
NSError *error;
BOOL success = NO;
_player = [[AVAudioPlayerNode alloc] init];
NSURL *hiphopOneURL = [NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:#"Hip Hop 1" ofType:#"caf"]];
AVAudioFile *hiphopOneFile = [[AVAudioFile alloc] initForReading:hiphopOneURL error:&error];
_playerLoopBuffer = [[AVAudioPCMBuffer alloc] initWithPCMFormat:[hiphopOneFile processingFormat] frameCapacity:(AVAudioFrameCount)[hiphopOneFile length]];
success = [hiphopOneFile readIntoBuffer:_playerLoopBuffer error:&error];
_engine = [[AVAudioEngine alloc] init];
[_engine attachNode:_player];
AVAudioMixerNode *mainMixer = [_engine mainMixerNode];
AVAudioFormat *stereoFormat = [[AVAudioFormat alloc] initStandardFormatWithSampleRate:44100 channels:2];
[_engine connect:_player to:mainMixer fromBus:0 toBus:0 format:stereoFormat];
[self startEngine];
}
Above is the general setup of the engine and the player node.
Then we implement the player with a play button:
- (IBAction)play:(id)sender {
if (!self.playerIsPlaying)
{
[self setupAudioOne];
[_player scheduleBuffer:_playerLoopBuffer atTime:nil options:AVAudioPlayerNodeBufferLoops completionHandler:nil];
[_player play];
}
}
And finally we stop the player with a stop button:
- (IBAction)stopHipHopOne:(id)sender {
if (self.playerIsPlaying) {
[_player stop];
}
playerIsPlaying is just a simple BOOL that determines if the _player is playing.
So basically my question is, when you hit the stop button as this program is written now, no memory will be freed.
Surely there is a simple line of code that I can add to the stop button that frees up the memory the engine and player is using?
Any thoughts?
Yes, there is. After stopping the player node, you can call:
[_engine disconnectNodeInput:_player];
[_engine detachNode:_player];
I saw you're also keeping a reference to the audio buffer, so you might want to nil that one as well. Let me know if that doesn't work for you. Something else could be leaking.

AVQueuePlayer (or AVPlayer) with a Soundcloud stream item block the UI (Main Thread)

Thank to those two posts, I resolve this problem during the creation of the AVPLayerItem
AVQueuePlayer playback without gap and freeze
AVPlayer "freezes" the app at the start of buffering an audio stream
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[NSURL URLWithString:urlString] options:#{AVURLAssetPreferPreciseDurationAndTimingKey : #(YES)}];
NSArray *keys = #[#"playable", #"tracks",#"duration" ];
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^()
{
for (NSString *thisKey in keys) {
NSError *error = nil;
AVKeyValueStatus keyStatus = [asset statusOfValueForKey:thisKey error:&error];
if (keyStatus == AVKeyValueStatusFailed) {
return ;
}
}
AVPlayerItem *item = [[AVPlayerItem alloc] initWithAsset:asset];
dispatch_async(dispatch_get_main_queue(), ^ {
[item addObserver:self forKeyPath:#"status" options:NSKeyValueObservingOptionNew context:nil];
[player insertItem:item afterItem:nil];
});
}];
});
But, with a stream from Soundcloud (https://api.soundcloud.com/tracks/146924238/stream?client_id=76395c48f97de903ff44861e230116bd), after the item's status is ready to play (AVPlayerStatusReadyToPlay), the AVQueueplayer keep doing something in the main thread "freezing" the UI (Track keep playing in background thread).
I noticed this problem is more important when the network is low, Then I made the conclusion that this freeze exist when the item is buffering the stream.
Someone have the solution or a trick?

Downloading multiple files in batches in iOS

I have an app that right now needs to download hundreds of small PDF's based on the users selection. The problem I am running into is that it is taking a significant amount of time because every time it has to open a new connection. I know that I could use GCD to do an async download, but how would I go about doing this in batches of like 10 files or so. Is there a framework that already does this, or is this something I will have to build my self?
This answer is now obsolete. Now that NSURLConnection is deprecated and NSURLSession is now available, that offers better mechanisms for downloading a series of files, avoiding much of the complexity of the solution contemplated here. See my other answer which discusses NSURLSession.
I'll keep this answer below, for historical purposes.
I'm sure there are lots of wonderful solutions for this, but I wrote a little downloader manager to handle this scenario, where you want to download a bunch of files. Just add the individual downloads to the download manager, and as one finishes, it will kick off the next queued one. You can specify how many you want it to do concurrently (which I default to four), so therefore there's no batching needed. If nothing else, this might provoke some ideas of how you might do this in your own implementation.
Note, this offers two advantages:
If your files are large, this never holds the entire file in memory, but rather streams it to persistent storage as it's being downloaded. This significantly reduces the memory footprint of the download process.
As the files are being downloaded, there are delegate protocols to inform you or the progress of the download.
I've attempted to describe the classes involved and proper operation on the main page at the Download Manager github page.
I should say, though, that this was designed to solve a particular problem, where I wanted to track the progress of downloads of large files as they're being downloaded and where I didn't want to ever hold the entire in memory at one time (e.g., if you're downloading a 100mb file, do you really want to hold that in RAM while downloading?).
While my solution solves those problem, if you don't need that, there are far simpler solutions using operation queues. In fact you even hint at this possibility:
I know that I could use GCD to do an async download, but how would I go about doing this in batches of like 10 files or so. ...
I have to say that doing an async download strikes me as the right solution, rather than trying to mitigate the download performance problem by downloading in batches.
You talk about using GCD queues. Personally, I'd just create an operation queue so that I could specify how many concurrent operations I wanted, and download the individual files using NSData method dataWithContentsOfURL followed by writeToFile:atomically:, making each download it's own operation.
So, for example, assuming you had an array of URLs of files to download it might be:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
for (NSURL* url in urlArray)
{
[queue addOperationWithBlock:^{
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
[data writeToFile:filename atomically:YES];
}];
}
Nice and simple. And by setting queue.maxConcurrentOperationCount you enjoy concurrency, while not crushing your app (or the server) with too many concurrent requests.
And if you need to be notified when the operations are done, you could do something like:
NSOperationQueue *queue = [[NSOperationQueue alloc] init];
queue.maxConcurrentOperationCount = 4;
NSBlockOperation *completionOperation = [NSBlockOperation blockOperationWithBlock:^{
[[NSOperationQueue mainQueue] addOperationWithBlock:^{
[self methodToCallOnCompletion];
}];
}];
for (NSURL* url in urlArray)
{
NSBlockOperation *operation = [NSBlockOperation blockOperationWithBlock:^{
NSData *data = [NSData dataWithContentsOfURL:url];
NSString *filename = [documentsPath stringByAppendingString:[url lastPathComponent]];
[data writeToFile:filename atomically:YES];
}];
[completionOperation addDependency:operation];
}
[queue addOperations:completionOperation.dependencies waitUntilFinished:NO];
[queue addOperation:completionOperation];
This will do the same thing, except it will call methodToCallOnCompletion on the main queue when all the downloads are done.
By the way, iOS 7 (and Mac OS 10.9) offer URLSession and URLSessionDownloadTask, which handles this quite gracefully. If you just want to download a bunch of files, you can do something like:
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:configuration];
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSFileManager *fileManager = [NSFileManager defaultManager];
for (NSString *filename in self.filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url completionHandler:^(NSURL *location, NSURLResponse *response, NSError *error) {
NSString *finalPath = [documentsPath stringByAppendingPathComponent:filename];
BOOL success;
NSError *fileManagerError;
if ([fileManager fileExistsAtPath:finalPath]) {
success = [fileManager removeItemAtPath:finalPath error:&fileManagerError];
NSAssert(success, #"removeItemAtPath error: %#", fileManagerError);
}
success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&fileManagerError];
NSAssert(success, #"moveItemAtURL error: %#", fileManagerError);
NSLog(#"finished %#", filename);
}];
[downloadTask resume];
}
Perhaps, given that your downloads take a "significant amount of time", you might want them to continue downloading even after the app has gone into the background. If so, you can use backgroundSessionConfiguration rather than defaultSessionConfiguration (though you have to implement the NSURLSessionDownloadDelegate methods, rather than using the completionHandler block). These background sessions are slower, but then again, they happen even if the user has left your app. Thus:
- (void)startBackgroundDownloadsForBaseURL:(NSURL *)baseURL {
NSURLSession *session = [self backgroundSession];
for (NSString *filename in self.filenames) {
NSURL *url = [baseURL URLByAppendingPathComponent:filename];
NSURLSessionTask *downloadTask = [session downloadTaskWithURL:url];
[downloadTask resume];
}
}
- (NSURLSession *)backgroundSession {
static NSURLSession *session = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSURLSessionConfiguration *configuration = [NSURLSessionConfiguration backgroundSessionConfiguration:kBackgroundId];
session = [NSURLSession sessionWithConfiguration:configuration delegate:self delegateQueue:[NSOperationQueue mainQueue]];
});
return session;
}
#pragma mark - NSURLSessionDownloadDelegate
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didFinishDownloadingToURL:(NSURL *)location {
NSString *documentsPath = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES)[0];
NSString *finalPath = [documentsPath stringByAppendingPathComponent:[[[downloadTask originalRequest] URL] lastPathComponent]];
NSFileManager *fileManager = [NSFileManager defaultManager];
BOOL success;
NSError *error;
if ([fileManager fileExistsAtPath:finalPath]) {
success = [fileManager removeItemAtPath:finalPath error:&error];
NSAssert(success, #"removeItemAtPath error: %#", error);
}
success = [fileManager moveItemAtURL:location toURL:[NSURL fileURLWithPath:finalPath] error:&error];
NSAssert(success, #"moveItemAtURL error: %#", error);
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didResumeAtOffset:(int64_t)fileOffset expectedTotalBytes:(int64_t)expectedTotalBytes {
// Update your UI if you want to
}
- (void)URLSession:(NSURLSession *)session downloadTask:(NSURLSessionDownloadTask *)downloadTask didWriteData:(int64_t)bytesWritten totalBytesWritten:(int64_t)totalBytesWritten totalBytesExpectedToWrite:(int64_t)totalBytesExpectedToWrite {
// Update your UI if you want to
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error {
if (error)
NSLog(#"%s: %#", __FUNCTION__, error);
}
#pragma mark - NSURLSessionDelegate
- (void)URLSession:(NSURLSession *)session didBecomeInvalidWithError:(NSError *)error {
NSLog(#"%s: %#", __FUNCTION__, error);
}
- (void)URLSessionDidFinishEventsForBackgroundURLSession:(NSURLSession *)session {
AppDelegate *appDelegate = (id)[[UIApplication sharedApplication] delegate];
if (appDelegate.backgroundSessionCompletionHandler) {
dispatch_async(dispatch_get_main_queue(), ^{
appDelegate.backgroundSessionCompletionHandler();
appDelegate.backgroundSessionCompletionHandler = nil;
});
}
}
By the way, this assumes your app delegate has a backgroundSessionCompletionHandler property:
#property (copy) void (^backgroundSessionCompletionHandler)();
And that the app delegate will set that property if the app was awaken to handle URLSession events:
- (void)application:(UIApplication *)application handleEventsForBackgroundURLSession:(NSString *)identifier completionHandler:(void (^)())completionHandler {
self.backgroundSessionCompletionHandler = completionHandler;
}
For an Apple demonstration of the background NSURLSession see the Simple Background Transfer sample.
If all of the PDFs are coming from a server you control then one option would be to have a single request pass a list of files you want (as query parameters on the URL). Then your server could zip up the requested files into a single file.
This would cut down on the number of individual network requests you need to make. Of course you need to update your server to handle such a request and your app needs to unzip the returned file. But this is much more efficient than making lots of individual network requests.
Use an NSOperationQueue and make each download a separate NSOperation. Set the maximum concurrent operations property on your queue to however many downloads you want to be able to run simultaneously. I'd keep it in the 4-6 range personally.
Here's a good blog post that explains how to make concurrent operations.
http://www.dribin.org/dave/blog/archives/2009/05/05/concurrent_operations/
What came as a big surprise is how slow dataWithContentsOfURL is when downloading multiple files!
To see it by yourself run the following example:
(you don't need the downloadQueue for downloadTaskWithURL, its there just for easier comparison)
- (IBAction)downloadUrls:(id)sender {
[[NSOperationQueue new] addOperationWithBlock:^{
[self download:true];
[self download:false];
}];
}
-(void) download:(BOOL) slow
{
double startTime = CACurrentMediaTime();
NSURLSessionConfiguration* config = [NSURLSessionConfiguration defaultSessionConfiguration];
static NSURLSession* urlSession;
if(urlSession == nil)
urlSession = [NSURLSession sessionWithConfiguration:config delegate:nil delegateQueue:nil];
dispatch_group_t syncGroup = dispatch_group_create();
NSOperationQueue* downloadQueue = [NSOperationQueue new];
downloadQueue.maxConcurrentOperationCount = 10;
NSString* baseUrl = #"https://via.placeholder.com/468x60?text=";
for(int i = 0;i < 100;i++) {
NSString* urlString = [baseUrl stringByAppendingFormat:#"image%d", i];
dispatch_group_enter(syncGroup);
NSURL *url = [NSURL URLWithString:urlString];
[downloadQueue addOperationWithBlock:^{
if(slow) {
NSData *urlData = [NSData dataWithContentsOfURL:url];
dispatch_group_leave(syncGroup);
//NSLog(#"downloaded: %#", urlString);
}
else {
NSURLSessionDownloadTask* task = [urlSession downloadTaskWithURL:url completionHandler:^(NSURL * _Nullable location, NSURLResponse * _Nullable response, NSError * _Nullable error) {
//NSLog(#"downloaded: %#", urlString);
dispatch_group_leave(syncGroup);
}];[task resume];
}
}];
}
dispatch_group_wait(syncGroup, DISPATCH_TIME_FOREVER);
double endTime = CACurrentMediaTime();
NSLog(#"Download time:%.2f", (endTime - startTime));
}
There is nothing to "build". Just loop through the next 10 files each time in 10 threads and get the next file when a thread finishes.

AVAudioRecorder doesn't record while screen is locked

I've tried to overcome this for a while. I'm trying to record sound, but the AVAudioRecorder doesn't record while screen is locked. It does continue to record once screen is unlocked, but the audio recorded when screen was locked is lost forever. I can't find anything wrong with what I'm doing:
-(void) startRecording
{
// Begin the recording session.
_session = [AVAudioSession sharedInstance];
NSError *setCategoryError = nil;
NSError *startRecordError;
[_session setActive:YES error:&startRecordError];
[self GKLog:[NSString stringWithFormat:#"recorder session error? :%#", startRecordError]];
[_session setCategory: AVAudioSessionCategoryRecord error: &setCategoryError];
if (setCategoryError) { NSLog(#"some error");}
//set me as delegate
_session.delegate=(id <AVAudioSessionDelegate>) self;
NSMutableDictionary* recordSetting = [[NSMutableDictionary alloc] init];
[recordSetting setValue :[NSNumber numberWithInt:kAudioFormatAppleIMA4] forKey:AVFormatIDKey];
[recordSetting setValue :[NSNumber numberWithInt:8] forKey:AVEncoderBitRateKey];
[recordSetting setValue:[NSNumber numberWithFloat:8000.0] forKey:AVSampleRateKey];
[recordSetting setValue:[NSNumber numberWithInt: 1] forKey:AVNumberOfChannelsKey];
if (!self.currentPath)
{
NSLog(#"can't record, no path set!");
return;
}
NSError *error;
NSURL *url=[NSURL fileURLWithPath:self.currentPath];
//Setup the recorder to use this file and record to it.
_recorder = [[ AVAudioRecorder alloc] initWithURL:url settings:recordSetting error:&error];
[self GKLog:[NSString stringWithFormat:#" recorder:%#",_recorder]];
_recorder.delegate=(id <AVAudioRecorderDelegate>) self;
[_recorder prepareToRecord];
//Start the actual Recording
[_recorder record];
}
Any ideas, please?
Ok, so the answer to my own question, which took me a long time to find out, is the following: The code I posted is good, but to actually work it needs to work in the background after screen was locked. For this one needs to add a UIBackgroundModes array in the app's plist file, and add 'audio' as one of its objects. This tells the system to let the app work with audio in the background.
Here's the not-so-easy to find documentation. Unfortunately apple doesn't specify that in their documentation of the audio session categories where they claim certain categories work in the background. Anyway, hopefully this answer will be available for others who have a similar problem...
You may want to consider setting the category as AVAudioSessionCategoryRecord to the AudioSession
How about disabling the screen lock until you are done recording?
[UIApplication sharedApplication].idleTimerDisabled = YES;
// Do recording here
[UIApplication sharedApplication].idleTimerDisabled = NO;
Just don't forget to re-enable the screen lock when you're done!

problem in playing next song in the avaudioplayer

my delegate method looks like this. after the first song is played it goes into this method and plays the second song , however when the second song is done playing it stops. it does not go into the delegate method.i need to play all the songs continuously. i am not sure, why. can someone help me.
- (void)audioPlayerDidFinishPlaying:(AVAudioPlayer *)p successfully:(BOOL)flag
{
if (flag == NO)
NSLog(#"Playback finished unsuccessfully");
else
{
//[player stop];
index++;
NSLog(#"%d",index);
path=[[NSBundle mainBundle] pathForResource:[songlist objectAtIndex:index] ofType:#"mp3"];
[player initWithContentsOfURL:[NSURL fileURLWithPath:path] error:NULL];
[songlabel2 setTitle:[songlist objectAtIndex:index]];
[endtime setText:[NSString stringWithFormat:#"%.2f",[player duration]/100]];
[player play];
}
}
Since you’re not defining a local variable called player, I’m assuming that player is an instance variable that was used to play the first song, and you’ve created it with
player = [[AVAudioPlayer alloc] initWithContentsOfURL:firstSongURL error:&error];
or something similar, and you’ve set the delegate. In your audioPlayerDidFinishPlaying: successfully: method, you have
[player initWithContentsOfURL:[NSURL fileURLWithPath:path] error:NULL];
which means you’re sending -initWithContentsOfURL:error: again to the same instance. Do not call an initialiser twice on a given object. The results are unpredictable and there is potential for memory leaking. You should release the previous instance of AVAudioPlayer, e.g.
[player release];
and then create a new instance with +alloc and -initWithContentsOfURL:error: again, just like you did with the first song, and set the appropriate delegate:
player = [[AVAudioPlayer alloc] initWithContentsOfURL:secondSongURL error:&error];
player.delegate = self;
[player play];