Memory Leaks In AVSpeechSynthesizer

"The fastest way to tank a watchOS app? Use AVSpeechSynthesizer"

Last year I wrote a new app, BackCountry Workout, my first entry using HealthKit and workouts on watchOS. This app worked very well on release and I was quite excited to add some enhancements to it, except that I ran into a very puzzling issue: the app would randomly crash. Worse still, I wouldn't get a crash report. This was even more harrowing when I started getting reports in December from users experiencing the same issue.

An audit of my code revealed no obvious problems. I safely unwrap my optionals, have a appropriate use of do/try. The lack of a crash report is a smell of a memory leak issue, but I could not find anything in my code that looked obvious. I made sure that I didn't have circular references, not capturing self in escaping closures, etc. Nothing.

Eventually I started seeing a pattern. My crashes frequently occurred after occurring whole mileage. For example, after completing five miles, or six miles, or even seven. But it wasn't always consistent. Then, I was on a trail and heard my app voice that I had completed 6 miles, looked up at the app, and it had crashed. Maybe AVSpeechSynthesizer was the culprit?

I created a sample project that put AVSpeechSynthesizer on a loop creating new instances with each iteration. The memory graph looked like this:

memory leak graph

Smoking. Gun.

While I have validated that the actual instance gets deallocated, something in its internals is very much not and is creating a terrible, terrible leak. On Apple Watch, it only takes 5-7 instances to tank it when each instantiation eats up >8.5MB.

In my case, creating a singleton helps me get around this issue, but something awful is going on under the hood. I have created Feedback #7586175 with a sample project illustrating this leak. I have tested this on iOS 13.4 and it is NOT fixed in the current beta of that release.

You can clone the watchOS sample project from Github that demonstrates this leak: SpeechLeak

After implementing my singleton workaround, my workout application now works the way it should, like it did on watchOS 5. It's memory characteristics are as follows:

memory leak graph

Huzzah.

It would appear, however, that there are very small leaks associated with AVSpeechUtterance, as you can see a very gentle increase in memory usage as multiple iterations of speech are generated. It is very evident, however, that the memory spikes are no where near those of creating new AVSpeechSynthesizer instances.

Posted on Feb 18
Written by Wayne Hartman