Android memory leak on device, not on emulator

Richard Buck

I'm writing a game to help teach my son some phonics: it's my first attempt at programming in Java, although I've previously used other languages. The game has four activities: a splash screen which initializes an array of variables before you dismiss it; another to choose a user; a third to choose which level of the game to play; and a fourth to actually play the game.

My problem was that if you go in and out of the game activity repeatedly, that activity would eventually crash -- logcat showed an OOM error. Watching the heap size as I did this, and looking at a heap dump with MAT, it looked as though I was leaking the whole of the fourth activity -- GC was just not being triggered.

I've tried lots of things to track down and fix the leak -- most of which are, I'm sure improvements (e.g. getting rid of all non-static inner classes from that activity) without fixing the problem. However, I've just tried running the same thing on an emulator (same target and API as my device) and there's no leak -- heap size goes up and down, GC is regularly triggered, it doesn't crash.

So I was going to post the code for the activity on here and ask for help spotting what might be causing the leak, but I'm no longer sure that's the right question. Instead I'm wondering why it works on the emulator, but not the phone... Does anyone have any ideas?

IDE: Android Studio 2.1
Target: Android 6, API 23 (Minimum SDK 8)
Emulator: Android Studio
Device: Sony Xperia Z2 (Now running 6.0.1, but I had the same issue pre recent update, i.e. on API 22)

Code for the activity:

public class GameActivity extends AppCompatActivity implements TextToSpeech.OnInitListener {

//TTS Object
private static TextToSpeech myTTS;
//TTS status check code
private int MY_DATA_CHECK_CODE = 0;
//LevelChooser request code
public static Context gameContext;
private int level;
public static String user;
private Typeface chinacat;
public static Activity gameActivity = null;
private static int[] goldstars = {R.drawable.goldstar1, R.drawable.goldstar2, R.drawable.goldstar3};

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    requestWindowFeature(Window.FEATURE_NO_TITLE);
    Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
    setSupportActionBar(toolbar);

    gameActivity = this;
    gameContext = this;
    level = getIntent().getIntExtra("level", 1);
    user = getIntent().getStringExtra("user");
    chinacat = Typeface.createFromAsset(getAssets(), "fonts/chinrg__.ttf");

    Intent checkTTSIntent = new Intent();
    checkTTSIntent.setAction(TextToSpeech.Engine.ACTION_CHECK_TTS_DATA);
    startActivityForResult(checkTTSIntent, MY_DATA_CHECK_CODE);
}

@Override
public void onStop() {
    if (myTTS != null) {
        myTTS.stop();
    }
    super.onStop();
}

@Override
public void onDestroy() {
    if (myTTS != null) {
        myTTS.shutdown();
    }
    Button ok_button = (Button) findViewById(R.id.button);
    ok_button.setOnClickListener(null);
    ImageView tickImageView = (ImageView) findViewById(R.id.tickImageView);
    tickImageView.setOnClickListener(null);

    super.onDestroy();
}

protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == MY_DATA_CHECK_CODE) {
        if (resultCode == TextToSpeech.Engine.CHECK_VOICE_DATA_PASS) {
            myTTS = new TextToSpeech(this, this);
        } else {
            Intent installTTSIntent = new Intent();
            installTTSIntent.setAction(TextToSpeech.Engine.ACTION_INSTALL_TTS_DATA);
            startActivity(installTTSIntent);
        }
    }
}

public void onInit(int initStatus) {
    //if tts initialized, load layout and level and assign listeners for layout elements
    if (initStatus == TextToSpeech.SUCCESS) {
        myTTS.setLanguage(Locale.ENGLISH);

        setContentView(R.layout.activity_main);

        ImageView imageView = (ImageView) findViewById(R.id.myImageView);

        PhonemeGroup levelGroup = MainActivity.gamelevel[level]; //set possible words
        levelGroup.setSubset(); //randomize subset of possible words for actual test
        PhonicsWord[] testSet = levelGroup.getSubset(); //fill array of test words

        TextView[] targetView = new TextView[3]; //textviews for beginning, middle & end of word
        targetView[0] = (TextView) findViewById(R.id.targetWord0);
        targetView[1] = (TextView) findViewById(R.id.targetWord1);
        targetView[2] = (TextView) findViewById(R.id.targetWord2);

        TextView[] answersView = new TextView[3]; //textviews for possible user answer choices
        answersView[0] = (TextView) findViewById(R.id.letter0);
        answersView[1] = (TextView) findViewById(R.id.letter1);
        answersView[2] = (TextView) findViewById(R.id.letter2);

        //set first target word, image for word, and possible answers
        testSet[0].setWord(levelGroup, targetView, answersView, imageView);
        testSet[0].speakWord(myTTS);
        //subset index is equal to array index for testSet, but visible to & settable by methods
        levelGroup.setSubsetIndex(0);

        for(int i=0; i<3; i++) {
            answersView[i].setTypeface(chinacat);
        }

        TextView letter0 = (TextView) findViewById(R.id.letter0);
        letter0.setOnClickListener(new LetterOnClickListener(testSet, levelGroup, targetView, answersView, 0) );
        TextView letter1 = (TextView) findViewById(R.id.letter1);
        letter1.setOnClickListener(new LetterOnClickListener(testSet, levelGroup, targetView, answersView, 1) );
        TextView letter2 = (TextView) findViewById(R.id.letter2);
        letter2.setOnClickListener(new LetterOnClickListener(testSet, levelGroup, targetView, answersView, 2) );

        Button ok_button = (Button) findViewById(R.id.button);
        ok_button.setOnClickListener(new OKButtonOnClickListener(testSet, levelGroup, targetView, level) );

        ImageView tickImageView = (ImageView) findViewById(R.id.tickImageView);
        tickImageView.setOnClickListener(new TickClick(myTTS, testSet, levelGroup, targetView, answersView, imageView) );
        imageView.setOnClickListener(new WordImageClick(testSet, levelGroup) );
    }
    /*else if TODO*/
}

private static class WordImageClick implements View.OnClickListener {
    //speaks the test word when the test image is clicked
    PhonicsWord[] testSet;
    PhonemeGroup levelGroup;

    public WordImageClick(PhonicsWord[] testSet, PhonemeGroup levelGroup) {
        this.testSet = testSet;
        this.levelGroup = levelGroup;
    }

    @Override
    public void onClick(View view) {
        testSet[levelGroup.getSubsetIndex()].speakWord(myTTS);
    }
}

private static class LetterOnClickListener implements View.OnClickListener {
    PhonemeGroup levelGroup;
    PhonicsWord currentWord;
    PhonicsWord[] testSet;
    TextView[] targetView;
    TextView[] answersView;
    int item;
    int phonemeclicked;

    public LetterOnClickListener(PhonicsWord[] testSet, PhonemeGroup levelGroup, TextView[] targetView, TextView[] answersView, int phonemeclicked) {
        this.testSet = testSet;
        this.levelGroup = levelGroup;
        this.targetView = targetView;
        this.answersView = answersView;
        this.phonemeclicked = phonemeclicked;
    }

    @Override
    public void onClick(View view) {
        this.item = this.levelGroup.getSubsetIndex();
        this.currentWord = this.testSet[item];
        int i = currentWord.getOmit_index();
        targetView[i].setText(answersView[phonemeclicked].getText());
    }
}

private void crossClick(View view) {
    view.setVisibility(View.INVISIBLE);
    if(view.getTag()==4){
        finish();
    }
}

The static variable gameActivity is used so that when you've finished a level an external class can call GameActivity.gameActivity.finish() after it's displayed how many stars you've got for the level (it's also used to call GameActivity.gameActivity.findViewById in another external class).

public class ShowStarsWithDelay extends Handler {

public void handleMessage(Message msg) {
    ImageView starView = (ImageView) ((LevelEndScreens) msg.obj).starView;
    ImageView highscoreView = (ImageView) ((LevelEndScreens) msg.obj).highscoreView;
    int num_currentstars = (int) ((LevelEndScreens) msg.obj).num_currentstars;
    int num_finalstars = (int) ((LevelEndScreens) msg.obj).num_finalstars;
    Boolean highscore = (Boolean) ((LevelEndScreens) msg.obj).highscore;
    int[] goldstars = (int[])((LevelEndScreens) msg.obj).goldstars;

    if(num_currentstars == num_finalstars) {
        if(!highscore) {
            starView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    GameActivity.gameActivity.finish();
                }
            });
        }
        else {
            highscoreView.setImageResource(R.drawable.highscore);
            highscoreView.setVisibility(View.VISIBLE);
            highscoreView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    GameActivity.gameActivity.finish();
                }
            });
        }
    }
    else {
        starView.setImageResource(goldstars[num_currentstars++]);
        Message message = new Message();
        LevelEndScreens endScreens = new LevelEndScreens(starView, highscoreView, num_currentstars, num_finalstars, highscore, goldstars);
        message.obj = endScreens;
        this.sendMessageDelayed(message, 1000);
    }
}

}

John Leehey

In general, you want to avoid having any static reference to a Context anywhere in your application (this includes Activity classes, of course). The only reference to a Context which MAY be acceptable is referencing the application context (as there is only one and it is always in memory while your app is alive anyway).

If you need a reference to the calling activity in one of your children, you'll need to pass the context as a parameter, or else use one of the child views methods to retrieve the context (such as getContext() for views and fragments).

More information that should help understand memory leaks and why this is important is here: http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html

As an example, in your code for calling finish(), you could safely change it to this:

highscoreView.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        if (v.getContext() instanceof Activity) {
            ((Activity)v.getContext()).finish();
        }
    }
});

To sum up, in order to fix your memory leaks, you'll need to remove the static keyword for all of your Context fields.

Collected from the Internet

Please contact [email protected] to delete if infringement.

edited at
0

Comments

0 comments
Login to comment

Related

From Dev

Android - Device Memory Leak with Fragments

From Dev

Android Fragment Webview Memory Leak

From Dev

Android runs on device but crashes on emulator

From Dev

Android runOnUiThread causing memory leak

From Dev

Cant use my Device as a Emulator in android

From Dev

How to reboot android device emulator in Genymotion

From Dev

Memory leak: steady increase in memory usage with simple device motion logging

From Dev

How to run unit tests for Android is not on the device or emulator?

From Dev

Collision work on pc emulator but not on android device

From Dev

Different picture display in real device and emulator Android

From Dev

Android memory leak on textview - LeakCanary (Leak can be ignored)

From Dev

Android animations memory leak

From Dev

Is it a memory leak in android

From Dev

Android camera Bitmap memory leak

From Dev

Android inner classes memory leak and leak by context?

From Dev

Bitmap causing memory leak on device

From Dev

Android progress bar memory leak

From Dev

`Unknown` (`Other`) memory leak in Android?

From Dev

How to release memory in android to avoid memory leak

From Dev

Android runOnUiThread causing memory leak

From Dev

memory leak with Android WebView

From Dev

Memory leak: steady increase in memory usage with simple device motion logging

From Dev

Getting Memory leak on android fragment

From Dev

Is it a memory leak in android

From Dev

Prevent memory leak in Android

From Dev

Android - is this a memory leak?

From Dev

Android memory leak Custom view

From Dev

Android Memory Leak - Anonymous class

From Dev

Avoid memory leak with WeakReference Android