The following is a stripped down version of a Fragment I’m using to display a simple stopwatch. The app is working perfectly on a tablet. However, on a phone, where an orientation change will cause the fragment to be re-created in a new activity, I’m getting a memory leak.
public class TimerFragment extends BaseFragment {
private static final int MESSAGE_UPDATE_TEXT = 0;
private TextView text;
private static final String KEY_START_TIME = "KEY_START_TIME";
private static final String KEY_ELAPSED_TIME = "KEY_ELAPSED";
private long startTime;
private long elapsedTime;
private static boolean running;
private final TimerHandler handler = new TimerHandler(this);
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
setHasOptionsMenu(true);
setUpHomeButton(false);
View view = inflater.inflate(R.layout.fragment_timer, container, false);
text = (TextView) view.findViewById(R.id.timerText);
if(savedInstanceState != null) {
startTime = savedInstanceState.getLong(KEY_START_TIME);
elapsedTime = savedInstanceState.getLong(KEY_ELAPSED_TIME);
}
if(running) {
handler.removeCallbacks(timer);
handler.postDelayed(timer, 0);
}
updateText();
return view;
}
private void updateText() {
if(elapsedTime == 0L) {
text.setText("0:00");
return;
}
int sec = (int) (elapsedTime / 1000);
int min = sec / 60;
sec = sec % 60;
text.setText(min + ":" + sec);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
handler.removeCallbacks(timer);
outState.putLong(KEY_START_TIME, startTime);
outState.putLong(KEY_ELAPSED_TIME, elapsedTime);
}
private final Runnable timer = new Runnable() {
@Override
public void run() {
long now = System.currentTimeMillis();
elapsedTime = now - startTime;
running = true;
handler.postDelayed(this, 1000);
handler.sendEmptyMessage(MESSAGE_UPDATE_TEXT);
}
};
private static class TimerHandler extends Handler {
private final WeakReference<TimerFragment> weakFragment;
public TimerHandler(TimerFragment f) {
weakFragment = new WeakReference<TimerFragment>(f);
}
@Override
public void handleMessage(Message msg) {
TimerFragment fragment = weakFragment.get();
switch (msg.what) {
case MESSAGE_UPDATE_TEXT:
if(fragment != null) {
fragment.updateText();
}
break;
default:
break;
}
}
}
}
Eclipse MAT even gives me this:
One instance of "android.os.MessageQueue" loaded by "<system class loader>" occupies 1 529 600 (14,19%) bytes. The memory is accumulated in one instance of "android.os.Message" loaded by "<system class loader>".
Keywords
android.os.Message
android.os.MessageQueue
However, the above does not appear when I analyze memory when running on a tablet (single activity at all times).
Any ideas for how to fix this? If I understand the problem correctly, which I might not, I think there are multiple handlers getting created when an orientation change happens on a phone.
If my problem is hard to understand give me a hint and I will try to explain better.
There is a simple rule you should always follow when using
Handler. Every time you writehandler.postDelayedyou should also writehandler.removeCallbackssomewhere.In your case you are calling
handler.postDelayed(timer, 0)inonCreateViewmethod soonDestroyViewis the most appropriate place to callhandler.removeCallbacks(timer)