2016-06-10 4 views
2

Я пишу игру, чтобы помочь учить моего сына некоторой фонетике: это моя первая попытка программирования в Java, хотя я ранее использовал другие языки. В игре четыре действия: заставка, которая инициализирует массив переменных, прежде чем вы его убьете; другой - выбрать пользователя; третий, чтобы выбрать, какой уровень игры играть; и четвертый, чтобы на самом деле сыграть в игру.Утечка памяти Android на устройстве, а не на эмуляторе

Моя проблема заключалась в том, что если вы снова и снова входите в и из игровой активности, эта деятельность в конечном итоге будет аварийна - logcat показал ошибку OOM. Наблюдая за размером кучи, как я это делал, и глядя на кучу кучи с MAT, казалось, что я пропустил всю четвертую активность - GC просто не запускался.

Я пробовал много вещей, чтобы отследить и устранить утечку - большинство из них, я уверен, что улучшения (например, избавление от всех нестатических внутренних классов от этого действия) без исправления проблемы. Тем не менее, я просто попробовал запустить ту же самую вещь на эмуляторе (та же цель и API как мое устройство), и нет утечки - размер кучи идет вверх и вниз, GC регулярно запускается, он не падает.

Так что я собирался опубликовать здесь код для этой деятельности и попросить о помощи узнать, что может вызвать утечку, но я не уверен, что это правильный вопрос. Вместо этого мне интересно, почему это работает на эмуляторе, но не на телефоне ... У кого-нибудь есть идеи?

IDE: Android студия 2,1
Target: Android 6, API 23 (Minimum SDK 8)
Emulator: Android Студия
устройства: Sony Xperia Z2 (сейчас работает 6.0.1, но у меня был тот же вопрос предварительно последнее обновление , то есть на API 22)

Код для деятельности:

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(); 
    } 
} 

статическая переменная gameActivity используется так, что, когда вы закончили го уровня на внешний класс может гр все GameActivity.gameActivity.finish() после того, как будет показано, сколько звезд у вас есть для уровня (он также используется для вызова GameActivity.gameActivity.findViewById в другом внешнем классе).

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); 
    } 
} 

}

ответ

1

В общем, вы хотите, чтобы избежать какой-либо статической ссылки на Context где-нибудь в вашем приложении (это включает в себя Activity классов, конечно). Единственная ссылка на Контекст, который МОЖЕТ быть приемлемым, ссылается на контекст приложения (поскольку есть только один, и он всегда находится в памяти, пока ваше приложение все равно живое).

Если вам нужна ссылка на вызывающую активность у одного из ваших детей, вам необходимо передать контекст в качестве параметра или использовать один из методов дочерних представлений для извлечения контекста (например, getContext() для просмотра и фрагменты).

Более подробная информация, которая должна помочь понять утечки памяти и почему это важно здесь: http://android-developers.blogspot.com/2009/01/avoiding-memory-leaks.html

В качестве примера, в вашем коде для вызова finish() можно смело изменить его к этому:

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

Чтобы подвести итог, чтобы исправить утечки памяти, вам нужно удалить ключевое слово static для всех полей Context.

+0

Благодарим за это. Я внедрил ваше предложение и смог использовать 'getContext' всюду, полностью избавляясь от полей' Context' (а не только ключевого слова 'static'). К сожалению, это не имеет никакого значения: у меня все еще есть (а) отсутствие утечки памяти на эмуляторе (b) утечка памяти на моем телефоне. Размер кучи увеличивается с шагом около 30 МБ каждый раз, когда я запускаю GameActivity до тех пор, пока телефон не даст up ... –

+0

Возможно, вам потребуется повторная публикация обновленного кода. Есть много вещей, которые могут содержать ссылки на контексты, такие как чертежи, компоненты просмотра и т. Д. Если у вас есть какие-либо статические объявления вообще, которые не являются примитивными типами, вам нужно будет переоценить (из вашего кода выше, я см., что 'myTTS' является статическим, что также вызовет утечку, поскольку содержит ссылку на контекст). Это помимо других возможных источников. –

+1

ОК - прошел и избавился от (как я думаю) всех «статических» непримитивов - конечно, включая «myTTS» - плюс некоторые другие хитрости, и это произошло. –

Смежные вопросы