音频回调线程中的内存泄漏(iOS)

Posted

技术标签:

【中文标题】音频回调线程中的内存泄漏(iOS)【英文标题】:Memory leak in audio callback thread (iOS) 【发布时间】:2011-01-20 21:21:03 【问题描述】:

我的应用有一个由 RemoteIO AudioUnit 框架调用的录音回调。 AudioUnit 在不同于主线程的线程中调用回调,因此它没有自动释放池。在此回调中,我执行以下操作:

为记录的样本分配一个缓冲区。 调用AudioUnitRender 填满这个缓冲区。 使用freopen 打开一个用于记录的文件。 调用音频处理方法。 根据音频,在主线程上更新 UI,使用 performSelectorOnMainThread(发送到视图控制器)。

此外,我有一个名为testFilter() 的函数,它会在音频会话初始化之前,即在第一次调用音频回调之前,在视图控制器的viewDidLoad 中调用它。在这个函数中,我分配了一个缓冲区(使用 malloc/free)并调用我上面提到的相同的音频处理方法。

现在,问题是(在设备上,而不是模拟器上):

如果我注释掉对 testFilter() 的调用,我不会收到任何与内存泄漏相关的消息。 如果我调用 testFilter(),我会开始从音频回调中收到一堆消息(前 5 条消息是我来自 testFilter() 方法的日志):

2011-01-20 23:05:10.358 TimeKeeper[389:307] 初始化缓冲区...

2011-01-20 23:05:10.693 TimeKeeper[389:307] 完成...

2011-01-20 23:05:10.696 TimeKeeper[389:307] 处理缓冲区...

2011-01-20 23:05:15.772 TimeKeeper[389:307] 完成...

2011-01-20 23:05:15.775 TimeKeeper[389:307] 经过时间 5.073843

2011-01-20 23:05:16.319 TimeKeeper[389:660f] * __NSAutoreleaseNoPool():__NSCFData 类的对象 0x137330 自动释放,没有适当的池 - 只是泄漏

2011-01-20 23:05:16.327 TimeKeeper[389:660f] * __NSAutoreleaseNoPool(): __NSCFData 类的对象 0x1373a0 自动释放,没有适当的池 - 只是泄漏

等等。回调有不同的线程,从日志中可以看出。

为什么只有在我调用一个在音频会话初始化之前调用并完成的函数时才会出现这些警告?如何检测泄漏?

附录

相关方法:

void testFilter () 
#if TARGET_IPHONE_SIMULATOR == 0
    freopen([@"/tmp/console.log" cStringUsingEncoding:NSASCIIStringEncoding],"a",stderr);
#endif
    int bufSize = 2048;
    int numsec=100;
    OnsetDetector * onsetDetector = [[OnsetDetector alloc] init];
    AudiosampleDataType *buffer = (AudioSampleDataType*) malloc (44100*numsec * sizeof(AudioSampleDataType)); // numsec seconds of audio @44100

    AudioBufferList bufferList;
    bufferList.mNumberBuffers = 1;
    bufferList.mBuffers[0].mData = buffer;
    bufferList.mBuffers[0].mDataByteSize = sizeof (AudioSampleDataType) * bufSize;
    bufferList.mBuffers[0].mNumberChannels = 1; 

    //--- init buffer
    NSLog(@"\n\n---***---***---");
    NSLog(@"initializing buffer...");
    for (int i = 0; i < 44100*numsec; ++i) 
        *(buffer+i) = (AudioSampleDataType)rand();
    
    NSLog(@"done...");

    NSLog(@"processing buffer...");
    CFAbsoluteTime t0 = CFAbsoluteTimeGetCurrent();
    for (int i = 0; (i+1)*bufSize < 44100*numsec; ++i) 
        bufferList.mBuffers[0].mData = buffer + i * bufSize;
        [onsetDetector process:&bufferList];
       
    CFAbsoluteTime t1 = CFAbsoluteTimeGetCurrent();
    NSLog(@"done...");
    NSLog(@"elapsed time %1.6f",(double)(t1-t0));
    free(buffer);
    [onsetDetector release];

还有:

OSStatus recordingCallback (void *inRefCon, 
                            AudioUnitRenderActionFlags *ioActionFlags, 
                            const AudioTimeStamp *inTimeStamp, 
                            UInt32 inBusNumber, 
                            UInt32 inNumberFrames, 
                            AudioBufferList *ioData) 

    AudioBufferList bufferList;

    // redundant
    SInt16 *buffer = (SInt16 *)malloc (sizeof (AudioSampleDataType) * inNumberFrames);
    bufferList.mNumberBuffers = 1;
    bufferList.mBuffers[0].mData = buffer;
    bufferList.mBuffers[0].mDataByteSize = sizeof (AudioSampleDataType) * inNumberFrames;
    bufferList.mBuffers[0].mNumberChannels = 1;

    ioData = &bufferList;

    // Obtain recorded samples  
    OSStatus status;

    MainViewController  *mainViewController = (MainViewController *)inRefCon;
    AudioManager        *audioManager       = [mainViewController audioManager];
    SampleManager       *sampleManager      = [audioManager sampleManager];

    status = AudioUnitRender([audioManager audioUnit], 
                             ioActionFlags, 
                             inTimeStamp, 
                             inBusNumber, //1
                             inNumberFrames, 
                             ioData);

#if TARGET_IPHONE_SIMULATOR == 0
    freopen([@"/tmp/console.log" cStringUsingEncoding:NSASCIIStringEncoding],"a",stdout);
#endif

    // send to onset detector
    SInt32 onset = [[audioManager onsetDetector] process:ioData];
    if (onset > 0) 
        NSLog(@"onset - %ld\n", sampleManager.recCnt + onset);

        //--- updating the UI - must be done on main thread
        [mainViewController performSelectorOnMainThread:@selector(tapButtonPressed) withObject:nil waitUntilDone:NO];
        [mainViewController performSelectorOnMainThread:@selector(onsetLedOn) withObject:nil waitUntilDone:NO];
    

    sampleManager.recCnt += inNumberFrames; 

    free(buffer);
    return noErr;

【问题讨论】:

【参考方案1】:

“只是泄漏”错误消息是在没有自动释放池的情况下,您的方法(或您的方法调用的其他方法)尝试autorelease 对象时得到的信息。如果没有池,就无法跟踪哪些对象需要释放,因此它们只会泄漏。

在主线程上,会自动为您创建和管理这样一个池,但在辅助线程上,您必须自己做。

你需要做的是改变你的函数来分配一个池:

void myFunction() 
   NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];  

   // do stuff here

   [pool release];

这将使错误消息消失,并修复泄漏。

【讨论】:

谢谢。我知道自动释放池,我知道它会使错误消息消失,但我不明白两件事:1. 正在尝试自动释放什么对象,以及 2. 为什么这些消息只有在我调用函数时才会出现主线程,甚至在创建音频线程之前。 ** 更新 ** 即使我将所有回调命令放在自动释放池中(并在最后调用 [pool drain]),错误消息仍然出现。还有其他想法吗?【参考方案2】:

似乎问题是由freopen 引起的,而不是fclose。我将stderr 作为第三个参数传递给freopen,因此日志消息被重定向到一个文件。如果我在回调结束时调用fclose,则不会出现内存泄漏错误。

有些问题仍然需要答案:

为什么使用autorelease pool 没有解决问题?我猜是因为freopenC 函数,所以无论如何都不会发送自动释放消息。 抛出错误消息的唯一情况是: freopen在主线程的一个函数中使用,没有fclose freopen 用于不同线程上的音频回调,没有fclose

也就是说,如果freopen只在音频回调线程中使用(而不是在主线程中),则不会出现错误。

编辑

另一个奇怪的行为是,即使在两个线程中都使用fclose,但我在主线程上传递stdout,在音频回调线程中传递stderr,我会收到错误消息。我认为这与当有多个线程时调用NSLog 的方式有关。欢迎对此提出任何见解。

【讨论】:

我从所有这些头痛中得出的最终结论是根本不使用freopenNSLog,而是实现我自己的日志类,它将使用文件输出(C 的fprintf 或一些 Cocoa 文件 IO类)

以上是关于音频回调线程中的内存泄漏(iOS)的主要内容,如果未能解决你的问题,请参考以下文章

LeakCanary检测内存泄漏

javascript中的关闭和回调内存泄漏

在 Java 中播放音频而没有内存泄漏

一个线程内存泄漏问题定位过程

一个线程内存泄漏问题定位过程

线程关闭期间Win64 Delphi RTL中的内存泄漏?