Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to release the static var of TextToSpeech in order to avoid leak?

Tags:

android

I write a public function to speak a text, I don't think the following code is good, but I don't know how to improve it, could you give me some suggestions? Thanks!

I think the static var TextToSpeech tts maybe cause leak,I don't know how to release it.

public class SpeechTxt {

    private static TextToSpeech tts;

    public static void SpeakOut(final Context myContext, String s) {        

        tts = new TextToSpeech(myContext, new TextToSpeech.OnInitListener(){
            @Override
            public void onInit(int status) {
                // TODO Auto-generated method stub
                if (status == TextToSpeech.SUCCESS) {   

                    int result = tts.setLanguage(Locale.US);

                    if (result == TextToSpeech.LANG_MISSING_DATA
                            || result == TextToSpeech.LANG_NOT_SUPPORTED) {
                        Toast.makeText(myContext, "Language is not supported",Toast.LENGTH_SHORT).show();
                    } else {
                        tts.speak("Hello, the world! "+s, TextToSpeech.QUEUE_ADD, null);
                    }           

                }else {
                    Toast.makeText(myContext, "Initilization Failed",Toast.LENGTH_SHORT).show();
                }
            }

        });

        /* I must comment the code, or phone can't speak
        if (tts != null) {
            tts.stop();
            tts.shutdown();
        } ;
        */ 

        Toast.makeText(myContext, "This is a test",Toast.LENGTH_SHORT).show();
    }

}
like image 435
HelloCW Avatar asked Dec 25 '22 10:12

HelloCW


2 Answers

Even if you define the TextToSpeech in static methods they are usually be called from an Activity context, so you can still shutdown the TextToSpeech whenever an Activity that uses TextToSpeech is destroyed. The documentation states:

It is good practice for instance to call this method in the onDestroy() method of an Activity so the TextToSpeech engine can be cleanly stopped.

http://developer.android.com/reference/android/speech/tts/TextToSpeech.html#shutdown()

On the other hand it's not necessary to shut it down after each use so I suggest to keep the TextToSpeech object initialized as long as the Activity is running. This prevents the TextToSpeech to be initialized before each "speak" operation. It's usually not a very heavy operation once the TextToSpeech on the device has been initialized but it's still a couple of ms you'll gain.

Starting the TTS on my Nexus 5 takes initially about 1.3 seconds and after that each instantiation takes between 50 and 80 ms and that's time you can really save.

If you are concerned about memory leaks use the application context to initialize the TTS (by using context.getApplicationContext() instead of just context which would usually be an Activity context).

Also - as nKn suggested - use a SoftReference to allow the GC to recycle it should the VM run low on memory (it's guaranteed that all SoftReferences will be recycled before the VM throws an OutOfMemoryError, see: http://docs.oracle.com/javase/7/docs/api/java/lang/ref/SoftReference.html).

To further improve your code you should handle the missing language case by allowing the user to install the language.

Here's my suggestion how you could enhance your code:

public class SpeechTxt {

    private static SoftReference<TextToSpeech> sTts;

    public static void speakOut(final Context context, final String s) {
        final Context appContext = context.getApplicationContext();
        if (sTts == null) {
            sTts = new SoftReference<TextToSpeech>(new TextToSpeech(appContext, new TextToSpeech.OnInitListener(){
                @Override
                public void onInit(int status) {
                    if (status == TextToSpeech.SUCCESS) {
                        speak(appContext, s);
                    }
                    else {
                        loadText2SpeechData(appContext);
                    }
                }
            }));
        }
        else {
            speak(appContext, s);
        }
    }

    public static void destroyTTS(Context context) {
        if (sTts != null && ! sTts.get().isSpeaking()) {
            sTts.get().shutdown();
            sTts = null;
        }
    }

    private static void speak(Context context, String s) {
        if (sTts != null) {
            switch (sTts.get().setLanguage(Locale.getDefault())) {
            case TextToSpeech.LANG_COUNTRY_AVAILABLE:
            case TextToSpeech.LANG_COUNTRY_VAR_AVAILABLE:
            case TextToSpeech.LANG_AVAILABLE: {
                sTts.get().speak(s, TextToSpeech.QUEUE_ADD, null);
                break;
            }
            case TextToSpeech.LANG_MISSING_DATA: {
                loadText2SpeechData(context);
                break;
            }
            case TextToSpeech.LANG_NOT_SUPPORTED: // not much to do here
            }
        }
    }

    private static void loadText2SpeechData(Context context) {
        try {
            Intent installIntent = new Intent(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
            installIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            context.startActivity(installIntent);
        }
        catch (ActivityNotFoundException ignore) {}
    }
}

As you can see TextToSpeech is only instantiated if that hasn't been done before or after the object has been destroyed. Also it uses only the application context so no memory leaks there. Starting an activity with the application context isn't a problem either because we are using FLAG_ACTIVITY_NEW_TASK.

Even if you use the TTS in a non Activity context (e.g. a BroadCastReceiver) you'd still have some kind of lifecycle that would allow to initialize and destroy the TextToSpeech object (and free the underlying resources). IMO that's not really necessary especially when using a SoftReference.

Note that while a BroadcastReceiver has a lifecycle the documentation states that:

A BroadcastReceiver object is only valid for the duration of the call to onReceive(Context, Intent). Once your code returns from this function, the system considers the object to be finished and no longer active.

This has important repercussions to what you can do in an onReceive(Context, Intent) implementation: anything that requires asynchronous operation is not available, because you will need to return from the function to handle the asynchronous operation, but at that point the BroadcastReceiver is no longer active and thus the system is free to kill its process before the asynchronous operation completes.

http://developer.android.com/reference/android/content/BroadcastReceiver.html#ReceiverLifecycle

This means you can't stop the TTS in a BroadcastReceiver because the speak call is asynchronous and you can't wait till it finishes to then destroy the TTS object. If you want to destroy the TTS object (again I don't think it's necessary) then you'd need to start e.g. a Service (or an Activity without ui). The service would call the speak method and wait till the OnUtteranceCompletedListener (or OnUtteranceProgressListener) returns, e.g. like that:

sTts.get().setOnUtteranceCompletedListener(sListener);

HashMap<String, String> params = new HashMap<String, String>();
params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, s);
sTts.get().speak(s, TextToSpeech.QUEUE_ADD, params);

private static OnUtteranceCompletedListener sListener = new OnUtteranceCompletedListener() {
    @Override
    public void onUtteranceCompleted(String utteranceId) {
        if (! sTts.get().isSpeaking()) {
            destroyTTS();           
        }
    }
};

BTW if you want to destroy the TTS object after each speak then there's really no need to use static code.

like image 172
Emanuel Moecklin Avatar answered Apr 30 '23 13:04

Emanuel Moecklin


Any object created with new is stored in the Heap. Every time the GC runs, it will free the objects that are elegible to be freed, which means that (amongs other circumstances), it will free any object that is not reacheable by any other reference.

If you're worried about the fact it could produce a leak, simply set it to null once you're done. Another way is using a SoftReference on that object. This will tell the GC that this object has a preference to be freed, so if the Android OS is in lack of memory, it will have some preference to be freed over other objects. In this case, you should simply do something like this:

private static SoftReference<TextToSpeech> tts;
...
tts = new SoftReference<TextToSpeech>(...);

Then the object will be reached with tts.get() instead of just tts. If you use this approach, it's important that each time you want to use this object, you check whether it is null or not, as now it's more probable that it will be freed by the GC.

if (tts != null) { ... }

However, the best way of knowing whether you have a memory leak is testing it in an empirical way. For this, I use DDMS + HPROF which will make a dump of the memory so you can analize it and see if an object has a bigger amount of memory allocated than it should. This topic is very extense and requires some practice, but I'll leave you some links which helped me a lot learning about this topic.

  • Reference for SoftReference
  • Romain Guy - Avoid memory leaks on Android
  • 10 Tips for using the Eclipse Memory Analyzer
  • Attacking memory problems on Android
like image 37
nKn Avatar answered Apr 30 '23 15:04

nKn