Search code examples
androidandroid-fragmentsmemory-leaksandroid-viewpagerandroid-pageradapter

why viewPager cause memory leak?


In my app, I'm using in viewPager which adapts fragments using FragmentStatePagerAdapter. Each fragment contains gridView and GridViewAdapter.

everything works fine but each time I am swapping, the memory usage increases a lot and wastes the memory. Why it happens and how can I fix it?

FragmentAdapter class:

import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;

import java.util.Calendar;


public class FragmentAdapter extends FragmentStatePagerAdapter {

    private static final int NUM_ITEMS = 12;


    public FragmentAdapter(FragmentManager fm) {
        super(fm);
    }

    @Override
    public Fragment getItem(int position) {
        switch (position) {
            case Calendar.JANUARY:
                return MonthFragment.newInstance(Calendar.JANUARY);
            case Calendar.FEBRUARY:
                return  MonthFragment.newInstance(Calendar.FEBRUARY);
            case Calendar.MARCH:
                return MonthFragment.newInstance(Calendar.MARCH);
            case Calendar.APRIL:
                return  MonthFragment.newInstance(Calendar.APRIL);
            case Calendar.MAY:
                return  MonthFragment.newInstance(Calendar.MAY);
            case Calendar.JUNE:
                return  MonthFragment.newInstance(Calendar.JUNE);
            case Calendar.JULY:
                return  MonthFragment.newInstance(Calendar.JULY);
            case Calendar.AUGUST:
                return  MonthFragment.newInstance(Calendar.AUGUST);
            case Calendar.SEPTEMBER:
                return  MonthFragment.newInstance(Calendar.SEPTEMBER);
            case Calendar.OCTOBER:
                return  MonthFragment.newInstance(Calendar.OCTOBER);
            case Calendar.NOVEMBER:
                return MonthFragment.newInstance(Calendar.NOVEMBER);
            case Calendar.DECEMBER:
                return MonthFragment.newInstance(Calendar.DECEMBER);
            default:
                throw new IllegalArgumentException("there is problem with position", new Throwable("NUM_ITEMS is not 12"));
        }
    }

    @Override
    public int getCount() {
        return NUM_ITEMS;
    }

}

MonthFragment class:

import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.GridView;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;


public class MonthFragment extends Fragment {

    private static final String ARG_MONTH = "arg_month";
    private int fragmentMonthId;

    private GridView gridView;
    private GridViewAdapter adapter;
    private ArrayList<Date> monthDates;

    public MonthFragment() {
        // Required empty public constructor
    }


    public static MonthFragment newInstance(int monthId) {
        MonthFragment fragment = new MonthFragment();
        Bundle args = new Bundle();
        args.putInt(ARG_MONTH, monthId);
        fragment.setArguments(args);
        return fragment;
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (getArguments() != null) {
            fragmentMonthId = getArguments().getInt(ARG_MONTH);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        // Inflate the layout for this fragment
        return inflater.inflate(R.layout.fragment_month, container, false);
    }

    @Override
    public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        gridView = view.findViewById(R.id.days);

        monthDates = getDates();
        adapter = new GridViewAdapter(getContext(), R.layout.day, 0, monthDates);

        gridView.setAdapter(adapter);
    }

    private ArrayList<Date> getDates() {
        ArrayList<Date> arrayList = new ArrayList<>();
        Calendar calendar = Calendar.getInstance();
        calendar.set(Calendar.DAY_OF_MONTH, 1);
        calendar.set(Calendar.MONTH, fragmentMonthId);
        calendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
        for (int i = 0; i < calendar.getMaximum(Calendar.DAY_OF_MONTH); i++) {
            arrayList.add(calendar.getTime());
            calendar.add(Calendar.DATE, 1);
        }
        return arrayList;
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        monthDates.clear();
        adapter = null;
        gridView.setAdapter(null);
        gridView = null;
    }
}

GridViewAdapter class:

package com.example.kobimoshe.calendarwithpager;

import android.content.Context;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;


public class GridViewAdapter extends ArrayAdapter<Date> {

    ArrayList<Date> datesList;

    public GridViewAdapter(@NonNull Context context, int resource, int textViewResourceId, @NonNull List<Date> objects) {
        super(context, resource, textViewResourceId, objects);
        datesList = (ArrayList<Date>) objects;
    }

    @NonNull
    @Override
    public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) {
        if (convertView == null) {
            convertView = LayoutInflater.from(getContext()).inflate(R.layout.day, parent, false);
        }
        TextView dateView = (TextView) convertView.findViewById(R.id.date);
        dateView.setText(datesList.get(position).toString());

        return convertView;
    }

}

UPDATE :

after experiment and research, I assume two things:

  1. the GC doesn't remove the elements inside the fragment even though I remove them when the fragment was destroyed (i assume that because when onDestroyView has been called the memory usage graph isn't decreasing).

  2. every swapping also make memory leaks (i have created a new just with ViewPager which uses FragmentStatePagerAdapter in order to set empty fragment. every time swapped fragments in this app the memory usage increased).

can anyone explain to me why the elements can't completely be removed? how can I fix the code in order to clean them when the fragment is removed? even though the fragment is deleted somehow or something from it, saved in the memory. how can I remove the fragment from the memory in order to save on constant memory size?


Solution

  • Check your references, 99% of the time a memory leak is caused by persisting objects that should have been garbage collected but are still alive. Make sure that you get rid of objects that you are no longer using.

    Also FragmentStatePagerAdapter works best when you have a large number of fragments and there is some added overhead when switching between pages. Consider trying FragmentPagerAdapter which is more sutable for a smaller amount of fragments. And as already mentioned you should implement the viewholder pattern since you have a list of views.