Background processing gives me BAD ACCESS crash?
I followed this guide here to doing background processing:
http://evilrockhopper.com/2010/01/iphone-development-keeping-the-ui-responsive-and-a-background-thread-pattern/
And the only 1 line of code which was put to background processing was:
sound = flite_text_to_wave([cleanString UTF8String], voice);
But for some reason i got a bad access signal.
Debugging it shows that it crashes on that line too. This is the code I now have in that part. Bear in mind that most of this is just default Flite stuff from the sfoster project which has had no problems before, when it was all together, not separated into 3.
-(void)speakText:(NSString *)text //This is called by my app
{
cleanString = [NSMutableString stringWithString:@""];
if([text length] > 1)
{
int x = 0;
while (x < [text length])
{
unichar ch = [text characterAtIndex:x];
[cleanString appendFormat:@"%c", ch];
x++;
}
}
if(cleanString == nil)
{ // string is empty
cleanString = [NSMutableString stringWithString:@""];
}
//The next line i've put in from the link开发者_如何学运维
[self performSelectorInBackground:@selector(backgroundTextToSpeech) withObject:nil];
}
-(void)backgroundTextToSpeech {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
//The following line is the one it crashes on
sound = flite_text_to_wave([cleanString UTF8String], voice);
[self performSelectorOnMainThread:@selector(backToForegroundTextToSpeech) withObject:nil waitUntilDone:YES];
[pool release];
}
-(void)backToForegroundTextToSpeech {
NSArray *filePaths = NSSearchPathForDirectoriesInDomains (NSDocumentDirectory, NSUserDomainMask, YES);
NSString *recordingDirectory = [filePaths objectAtIndex: 0];
// Pick a file name
tempFilePath = [NSString stringWithFormat: @"%@/%s", recordingDirectory, "temp.wav"];
// save wave to disk
char *path;
path = (char*)[tempFilePath UTF8String];
cst_wave_save_riff(sound, path);
// Play the sound back.
NSError *err;
[audioPlayer stop];
audioPlayer = [[AVAudioPlayer alloc] initWithContentsOfURL:[NSURL fileURLWithPath:tempFilePath] error:&err];
[audioPlayer setDelegate:self];
[audioPlayer prepareToPlay];
}
Any ideas what i've done wrong/what i can do differently to stop this happening?
EDIT: A picture of the debugger after changing it round with some code posted below:
first thing that jumps out:
your class needs to hold a retain count for cleanString
.
typically, this is accomplished via a retained (or copied, which is generally preferable with NSStrings and other concrete/immutable types) property:
@interface MONSpeaker : NSObject
{
NSString * cleanString;
}
@property (copy) NSString * cleanString;
@end
@implementation MONSpeaker
@synthesize cleanString;
/* ... */
- (void)dealloc
{
[cleanString release], cleanString = nil;
[super dealloc];
}
-(void)speakText:(NSString *)text // This is called by my app
{
NSMutableString * str = [NSMutableString stringWithString:@""];
if([text length] > 1)
{
int x = 0;
while (x < [text length])
{
unichar ch = [text characterAtIndex:x];
[str appendFormat:@"%c", ch];
x++;
}
}
if(str == nil) // why not check for nil at creation instead?
{ // string is empty
str = [NSMutableString stringWithString:@""];
}
self.cleanString = str;
// The next line i've put in from the link
[self performSelectorInBackground:@selector(backgroundTextToSpeech) withObject:nil];
}
-(void)backgroundTextToSpeech {
NSAutoreleasePool* pool = [[NSAutoreleasePool alloc] init];
// The following line is the one it crashes on
sound = flite_text_to_wave([self.cleanString UTF8String], voice);
[self performSelectorOnMainThread:@selector(backToForegroundTextToSpeech) withObject:nil waitUntilDone:YES];
[pool release];
}
@end
ok - so this isn't 100% thread-safe, but it is idiomatic.
the variant which is more resistant to threading issues would pass the string as an argument to backgroundTextToSpeech:(NSString *)text
. backgroundTextToSpeech:(NSString *)text
would then create a copy of the argument text
(and of course release the copy before pool
is destroyed).
You don’t understand how autoreleasing works. You assign the cleanString
variable an autoreleased object and then start some processing on background that uses this value. But when the method that started the background processing returns the control to the main run loop, the autoreleased string stored in the cleanString
gets deallocated and the background thread runs into a zombie.
You could simplify the code using Grand Central Dispatch:
- (void) startProcessing {
NSString *source = [NSString stringWithWhatever…];
dispatch_queue_t targetQ =
dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
dispatch_async(targetQ, ^{
sound = flite_text_to_wave…;
dispatch_async(dispatch_get_main_queue(), ^{
[self speechProcessingDone];
});
});
}
The advantages are that you don’t have to maintain your own autorelease pool, you don’t have to add extra methods just to have one line executed in background and the block will retain your string, so that you won’t crash.
But you should certainly not use GCD just to get around the autoreleasing issue without knowing what’s happening. Memory management is the base, you must be 100% sure what you are doing.
If this is above your head, make sure you know how memory management works in Cocoa, for example by reading this Objective-C tutorial by Scott Stevenson. You have to do this, there is no way around.
Then come back to the code and you will see that the mutable strings that you store into cleanString
are autoreleased, meaning they will get deallocated sometime soon after you leave the current function. After running the selector in background, you exit the current function and the string stored in cleanString
gets deallocated. Soon after that the background thread gets to the following line:
sound = flite_text_to_wave([cleanString UTF8String], voice);
But as the object stored in cleanString
was already deallocated, you get the crash. The solution is simply to retain the object until you are done with it. You can retain it before running the background thread and release when the background thread ends.
精彩评论