7

I've been having app crashes due to an out-of-memory condition (in the program, not the programmer). MAT shows that copies of my Activity were sometimes being retained across screen rotations, and the only object keeping the bogus copies alive was each instance's TextToSpeech object. I can duplicate this behaviour using this snippet:

public class MainActivity extends Activity {
    TextToSpeech    mTts;
    char[]          mBigChunk = new char[1000000];  // not used; just makes MainActivity instances easier to see in MAT

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onStart() {
        super.onStart();
        if (mTts==null)                             // shouldn't be necessary and doesn't make any difference
            mTts = new TextToSpeech(this, null);        // commenting this out fixes the leak
    }
    @Override
    public void onStop() {
        super.onStop();
        if (mTts != null) {
            mTts.shutdown();
            mTts = null;        // shouldn't be necessary and doesn't make any difference
        }
    }
}

After 30 orientation changes, MAT lists between one and eight instances of net.catplace.tts_leak.MainActivity, and also multiple instance of various TTS objects; eg:

Class Name                                                            | Shallow Heap | Retained Heap | Percentage
------------------------------------------------------------------------------------------------------------------
android.speech.*                                                      |              |               |           
android.speech.tts.TextToSpeech$Connection$1 @ 0x42de94c8 Native Stack|           24 |     2,052,664 |     11.85%
android.speech.tts.TextToSpeech$Connection$1 @ 0x431dd500 Native Stack|           24 |     2,052,664 |     11.85%
android.speech.tts.TextToSpeech$Connection$1 @ 0x435cc438 Native Stack|           24 |           552 |      0.00%
android.speech.tts.TextToSpeech$Connection @ 0x441b3698               |           32 |           528 |      0.00%
android.speech.tts.TextToSpeech @ 0x43fb3c00                          |           64 |           496 |      0.00%
android.speech.tts.TextToSpeech$Connection @ 0x43fb4420               |           32 |            48 |      0.00%
android.speech.tts.TextToSpeech$Connection$1 @ 0x43fb4440 Native Stack|           24 |            24 |      0.00%
android.speech.tts.TextToSpeech$Connection$1 @ 0x441b36b8 Native Stack|           24 |            24 |      0.00%
Total: 8 entries (13,079 filtered)                                    |              |               |           
------------------------------------------------------------------------------------------------------------------

MAT indicates that the bogus copies of MainActivity are being retained by TTS:

Class Name                                                                            | Shallow Heap | Retained Heap
---------------------------------------------------------------------------------------------------------------------
                                                                                      |              |              
net.catplace.tts_leak.MainActivity @ 0x437c6068                                       |          200 |     2,001,352
'- mContext android.speech.tts.TextToSpeech @ 0x431de6d8                              |           64 |           496
   '- this$0 android.speech.tts.TextToSpeech$Connection @ 0x441b3698                  |           32 |           528
      '- this$1 android.speech.tts.TextToSpeech$Connection$1 @ 0x441b36b8 Native Stack|           24 |            24
---------------------------------------------------------------------------------------------------------------------

I get this behavour across a range of real devices and AVDs. The above results are from a Nexus 7.

I've tried different TTS engines, using different events to create and destroy mTts, etc.

My hypothesis is that TextToSpeech doesn't always null its reference to the context that created it, resulting in leaked copies of the context (Activity). But I'm new at this; is there something I'm doing wrong?

Peter McLennan
  • 371
  • 4
  • 11
  • 1
    If the TextToSpeech object is constructed by passing `getApplicationContext()` as opposed to `this` (Activity), the problem doesn't seem to happen. This kinda makes sense since the application isn't destroyed on orientation changes. Methinks it shouldn't be necessary to do this, though. – Peter McLennan Oct 30 '13 at 20:32
  • I had the same problem. Screen rotation was causing my activity to be leaked. Rotate 10 times, 10 copies of the activity in memory. I changed it to pass getApplicationContext() instead of this to the TextToSpeech object as Peter suggested and it solved the problem. Boom! – Bryan Bedard Jul 12 '14 at 05:28

2 Answers2

2

Taking a look at the source code of TextToSpeech here, you'll notice that it actually binds a service to the context passed and the shutdown method actually unbinds it. Now rest ahead is guess, since Service has its own life cycle,the TextToSpeech might be holding back the context.If you did some research keeping this in mind that you're actually running a service,you might crack the question.

Now I'm not also sure what this might imply but I am open for any new findings from your side. Given that TextToSpeech is a service you might want to pass it the application context, since the service would still be running when activity gets destroyed.

Also for further reading ______________

Parvaz Bhaskar
  • 1,367
  • 9
  • 29
  • 1
    Thanks again. I did notice that passing the application context seems to make the problem go away. It's a shame Google's examples use the Activity context. Your second link shows that the service tries to do clever (and asynchronous) things, which could explain the inconsistent memory leak behaviour I notice. It probably all depends how quickly the orientation changes occur. – Peter McLennan Nov 01 '13 at 06:03
  • http://stackoverflow.com/questions/7298731/when-to-call-activity-context-or-application-context...Read the answer by "commonsware", though not directly related to the topic in discussions, it gives a good insight of what context to use when. – Parvaz Bhaskar Nov 01 '13 at 06:25
  • Great find. I think it **could** be directly related since it says to use the application context for services (which TTS is, as you pointed out previously). Google's examples to the contrary would seem to be encouraging leaks. – Peter McLennan Nov 01 '13 at 10:05
  • Thanks,just one question, if you pass application context, does the problem go away? in the sense , does MAT show any leftover instances? – Parvaz Bhaskar Nov 01 '13 at 11:27
  • Weirder and weirder. When I pass the application context, multiple instances of android.speech.tts.TextToSpeech$Connection$1 are still retained (not for every rotation, but maybe 1/3). These don't take much memory because they don't hold a ref to the Activity, and multiple copies of the Activity don't accrue. However, when TTS is created passing the Activity context, instances of android.speech.tts.TextToSpeech$Connection$1 pile up at about the same rate, but are much larger because each includes an Activity instance. MAT also shows multiple Activity instances - but not one per TTS! – Peter McLennan Nov 01 '13 at 22:32
  • It's tempting to think that I'm just observing transient behaviour caused by the non-deterministic behaviour of GC (ie, the GC just hasn't found the need, or got around, to cleaning up unnecessary instances of TTS or my Acitivy). However, if that were the case, the app shouldn't crash because the GC would be forced into action beforehand. However, the app can DEFINITELY crash due to OOM. This can be demonstrated by upping the mBigChunk size. If I double it, my Nexus 7 dies after about 10 orientation changes (when TTS is instantiated using the Application context). – Peter McLennan Nov 01 '13 at 22:36
0

If not implemtned android:configChanges="orientation" then you can override onDestory method:

@Override
protected void onDestroy() {
    super.onDestroy();
    if (ttsEngine != null) {
        ttsEngine.stop();
        ttsEngine.shutdown();
        Log.d(TAG, "TTS destroyed");
    }
}
Mohsen Afshin
  • 13,273
  • 10
  • 65
  • 90