Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ViewPager, PagerAdapter and Bitmap cause memory leak (OutOfMemoryError)

I have build a Android application which display weather data (I can give you the app name in private if you want to test the problem). The user can browse from one day to other to see weather of a specific day.

App Architecture

My application uses fragments (single MainActivity with Navigation Drawer which calls specific fragments).

DayPagerFragment uses a ViewPager with an unlimited number of pages (dynamic fragments). A page represent a day.

DayPagerFragment

public class DayPagerFragment extends Fragment {

    private ViewPager mViewPager;

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_day, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        mViewPager = (ViewPager) view.findViewById(R.id.pager);
        mViewPager.setOffscreenPageLimit(1);
        mViewPager.setAdapter(new DayAdapter(getChildFragmentManager()));
    }

    private static class DayAdapter extends FragmentStatePagerAdapter {

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

        @Override
        public Fragment getItem(int position) {
            return DayFragment.newInstance(null);
        }

        @Override
        public int getCount() {
            // I don't know the number to put here becauseI don't have 
            // a defined number of fragments (= dynamic fragments)
            return 1; 
        }

        @Override
        public int getItemPosition(Object object){
            return DayAdapter.POSITION_NONE;
        }

    }

    public void setCurrentPagerItemPrev() {
        //mViewPager.setCurrentItem(mViewPager.getCurrentItem() - 1);
        mAdapterViewPager.getRegisteredFragment(mViewPager.getCurrentItem() - 1);
    }

    public void setCurrentPagerItemNext() {
        //mViewPager.setCurrentItem(mViewPager.getCurrentItem() + 1);
        mAdapterViewPager.getRegisteredFragment(mViewPager.getCurrentItem() + 1);
    }

}

First optimisation: it is managed using a FragmentStatePagerAdapter because FragmentPagerAdapter is not suitable for my use / dynamic fragments (stores the whole Fragments in memory).

Second optmisation: I have set the number of pages that should be retained to either side of the current page with setOffscreenPageLimit(1).

DayFragment

public class DayFragment extends Fragment {

    private TextView mDay;
    private TextView mMonth;
    private Button mPrevDay;
    private Button mNextDay;
    private ImageView mCenter;
    private ImageView mLeft;
    private ImageView mRight;
    ...
    private DayRepository dayRepository;
    private Day currentDay;
    private Day prevDay;
    private Day nextDay;
    private DayUtil dayUtil;
    private DayUtil dayUtilPrev;
    private DayUtil dayUtilNext;
    private Calendar cal;
    private Calendar calPrev;
    private Calendar calNext;

    public static DayFragment newInstance(Calendar calendar) {
        DayFragment dayFragment = new DayFragment();

        Bundle args = new Bundle();
        args.putInt("year", calendar.get(Calendar.YEAR));
        args.putInt("month", calendar.get(Calendar.MONTH));
        args.putInt("day", calendar.get(Calendar.DAY_OF_MONTH));
        dayFragment.setArguments(args);

        return dayFragment;
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
        View view = inflater.inflate(R.layout.fragment_day_nested, container, false);

        mDay = (TextView) view.findViewById(R.id.textView_day);
        mMonth = (TextView) view.findViewById(R.id.textView_month);
        mCenter = (ImageView) view.findViewById(R.id.imageView_center); // Weather symbol (sun, cloud...) of D-Day
        mLeft = (ImageView) view.findViewById(R.id.imageView_left); // Weather symbol of D-1
        mRight = (ImageView) view.findViewById(R.id.imageView_right);  // Weather symbol of D-2

        //... get 6 others TextView/ImageView

        MyApplication app = (MyApplication) getActivity().getApplicationContext();

        // Get bundle args
        int day = getArguments().getInt("day");
        int month = getArguments().getInt("month");
        int year = getArguments().getInt("year");

        // Date
        this.cal = new GregorianCalendar(year, month, day);

        // Get prev/next day (for nav arrows)
        this.calPrev = (GregorianCalendar) this.cal.clone();
        this.calPrev.add(Calendar.DAY_OF_YEAR, -1);
        this.calNext = (GregorianCalendar) this.cal.clone();
        this.calNext.add(Calendar.DAY_OF_YEAR, 1);

        // Get data from database
        //...

        // Utils
        this.dayUtil = new DayUtil(currentDay, getActivity());
        this.dayUtilPrev = new DayUtil(this.prevDay, getActivity());
        this.dayUtilNext = new DayUtil(this.nextDay, getActivity());
        String dateCurrentDayName = FormatUtil.getDayName(app.getLocale()).format(this.cal.getTime());
        String dateCurrentDayNameCap = dateCurrentDayName.substring(0,1).toUpperCase() + dateCurrentDayName.substring(1);
        String dateCurrentMonthName = FormatUtil.getMonthName(month, app.getLocale());

        // Update UI
        //... lot of setText(...) using day object and utils
        mLeft.setImageResource(this.dayUtilPrev.getDrawable());
        mCenter.setImageResource(dayUtil.getDrawable());
        mRight.setImageResource(this.dayUtilNext.getDrawable());

        return view;
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        // Custom fonts
        MyApplication app = (MyApplication) getActivity().getApplication();
        ViewGroup vg = (ViewGroup)getActivity().getWindow().getDecorView();
        ViewUtil.setTypeFace(app.getTrebuchet(), vg);

        // Navigation between days
        mMoonPrevDay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {

                ((MainActivity)getActivity()).viewDay(calPrev);
            }
        });
        mMoonNextDay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                ((MainActivity) getActivity()).viewDay(calNext);
            }
        });
    }

    // Never called!
    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.w("com.example", "Fragment day destroyed");
    }

}

The problem:

My application is very graphical because each page displays:

  • 10 TextBox (inner text changes depending on the day)
  • 4 ImageView (the weather symbol of D-1, D-Day, D+1) + another ImageView

I get OutOfMemoryError rapidly (after +/- 30 pages) when I browse the ViewPager pages.

It is like the Fragments are not being released from memory. The garbage collector does not work like I expected (I think it is because something references the old fragments).

Logcat

04-06 20:01:21.683  27008-27008/com.example D/dalvikvm﹕ GC_BEFORE_OOM freed 348K, 2% free 194444K/196608K, paused 93ms, total 93ms
04-06 20:01:21.683  27008-27008/com.example E/dalvikvm-heap﹕ Out of memory on a 1790260-byte allocation.
04-06 20:01:21.693  27008-27008/com.example E/AndroidRuntime﹕ FATAL EXCEPTION: main
    Process: com.example, PID: 27008
    java.lang.OutOfMemoryError
            at android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
            at android.graphics.BitmapFactory.decodeStream(BitmapFactory.java:587)
            at android.graphics.BitmapFactory.decodeResourceStream(BitmapFactory.java:422)
            at android.graphics.drawable.Drawable.createFromResourceStream(Drawable.java:840)
            at android.content.res.Resources.loadDrawable(Resources.java:2110)
            at android.content.res.Resources.getDrawable(Resources.java:700)
            at android.widget.ImageView.resolveUri(ImageView.java:638)
            at android.widget.ImageView.setImageResource(ImageView.java:367)
            at com.example.ui.DayFragment.onCreateView(DayFragment.java:126) //...mLeft.setImageResource()
            at android.support.v4.app.Fragment.performCreateView(Fragment.java:1500)
            at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:927)
            at android.support.v4.app.FragmentManagerImpl.moveToState(FragmentManager.java:1104)
            at android.support.v4.app.BackStackRecord.run(BackStackRecord.java:682)
            at android.support.v4.app.FragmentManagerImpl.execPendingActions(FragmentManager.java:1467)
            at android.support.v4.app.FragmentManagerImpl$1.run(FragmentManager.java:440)
            at android.os.Handler.handleCallback(Handler.java:733)
            at android.os.Handler.dispatchMessage(Handler.java:95)
            at android.os.Looper.loop(Looper.java:136)
            at android.app.ActivityThread.main(ActivityThread.java:5017)
            at java.lang.reflect.Method.invokeNative(Native Method)
            at java.lang.reflect.Method.invoke(Method.java:515)
            at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:779)
            at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:595)
            at dalvik.system.NativeStart.main(Native Method)
04-06 20:01:21.773  27008-27008/com.example I/dalvikvm-heap﹕ Clamp target GC heap from 197.480MB to 192.000MB
04-06 20:01:21.773  27008-27008/com.example D/dalvikvm﹕ GC_FOR_ALLOC freed 565K, 2% free 193932K/196608K, paused 73ms, total 73ms

I have memory leak but I don't know why and where. I have used Eclipse MAT (Memory Analyser) but I don't know where to look.

Can you help me?


Edit: for load Typeface, I use the following code:

DayFragment.java

// Custom fonts
MyApplication app = (MyApplication) getActivity().getApplication();
ViewGroup vg = (ViewGroup)getActivity().getWindow().getDecorView();
ViewUtil.setTypeFace(app.getTrebuchet(), vg);

MyApplication.java

public Typeface getTrebuchet() {
    if (trebuchet == null){
        trebuchet = Typeface.createFromAsset(getAssets(), Consts.PATH_TYPEFACE_TREBUCHET);
    }
    return trebuchet;
}

My DDMS show the memory leak:

enter image description here


Edit 2: IMPORTANT!

I use a navigation drawer in my application which is handled by my only activity MainActivity. The navigation drawer uses fragments (and not activities).

This is why DayPagerFragment extends from a Fragment (and not from a FragmentActivity or an Activity).

To swipe between days, the user must touch two buttons (prev/next). I use setOnClickListener on these button in DayFragment (see my updated code).

The problem is that I call ((MainActivity)getActivity()).viewDay(calPrev);

MainActivity

public class MainActivity extends ActionBarActivity implements NavigationDrawerFragment.NavigationDrawerCallbacks {

    private NavigationDrawerFragment mNavigationDrawerFragment;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ...
        // Set up the navigation drawer
        mNavigationDrawerFragment.setUp(R.id.navigation_drawer, (DrawerLayout) findViewById(R.id.drawer_layout));
    }
    ...
    public void viewDay(Calendar calendar) {
        DayFragment dayFragment = DayFragment.newInstance(calendar); // The problem is here I think !
        FragmentManager fragmentManager = getSupportFragmentManager();
        fragmentManager.beginTransaction()
                .setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
                .replace(R.id.container, dayFragment)
                .addToBackStack(null)
                .commit();
    }

}

So... I think that because I instanciate each time a new fragment, the ViewPager cannot do his job! And the MainActivity keeps a reference on each Fragments: this is why the garbage collector don't free the memory.

Now:

  1. I don't know if my theory is correct
  2. How to correct that? How, from setOnClickListener call setCurrentPagerItemPrev and setCurrentPagerItemNext methods (see my updated code in DayPagerFragment)?

NB : I use mAdapterViewPager.getRegisteredFragment() instead of mViewPager.setCurrentItem because my DayAdapter extends from SmartFragmentStatePagerAdapter, but it is the same.

like image 321
Guicara Avatar asked Apr 06 '14 18:04

Guicara


People also ask

Should I use ViewPager or ViewPager2?

ViewPager2 is an improved version of the ViewPager library that offers enhanced functionality and addresses common difficulties with using ViewPager . If your app already uses ViewPager , read this page to learn more about migrating to ViewPager2 .

When should I use ViewPager?

ViewPager in Android is a class that allows the user to flip left and right through pages of data. This class provides the functionality to flip pages in app. It is a widget found in the support library. To use it you'll have to put the element inside your XML layout file that'll contain multiple child views.

Is PagerAdapter deprecated?

This method is deprecated. Called when the host view is attempting to determine if an item's position has changed. This method may be called by the ViewPager to obtain a title string to describe the specified page.

What is PagerAdapter?

PagerAdapter supports data set changes. Data set changes must occur on the main thread and must end with a call to notifyDataSetChanged similar to AdapterView adapters derived from android. widget. BaseAdapter . A data set change may involve pages being added, removed, or changing position.


2 Answers

You need to check 2 things here: images and typefaces.

Images uses loads of memory so you need to do some work to release them quickly. I've used the following code to help cleaning my memory, it removes references helping to clean up faster.

Also, loading many high quality images in a short amount of time might cause errors as Android is a bit slow to grow the heap space. See my answer to: Android Understanding Heap Sizes. You might need to set android:largeHeap="true" in your manifest file, but only do it after optimising images and fonts.

Use the DDMS Heap view to check if you memory is maintaing a level, meaning you are cleaning up scrolled pages. This requires Android tools plugin to be installed on your IDE.

public abstract class SimplePagerAdapter extends PagerAdapter {
// ...
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
    container.removeView((View) object);
    unbindDrawables((View) object);
    object = null;
}
protected void unbindDrawables(View view) {
    if (view.getBackground() != null) {
        view.getBackground().setCallback(null);
    }
    if (view instanceof ViewGroup) {
        for (int i = 0; i < ((ViewGroup) view).getChildCount(); i++) {
            unbindDrawables(((ViewGroup) view).getChildAt(i));
        }
        ((ViewGroup) view).removeAllViews();
    }
}
}

Then, check the way you get your fonts. Make sure you cache all calls to Typeface.createFromAsset(...), like the example below:

public Typeface getFont(String font, Context context) {
    Typeface typeface = fontMap.get(font);
    if (typeface == null) {
        typeface = Typeface.createFromAsset(context.getResources().getAssets(), "fonts/" + font);
        fontMap.put(font, typeface);
    }
    return typeface;
}

Edit: extra info after question was updated

Change your adapter to use a list of objects:

Return the size of that list on getCount(),

When adding objects to the list call:

notifyDataSetChanged();
mPagerContainer.invalidate();

Then, when you are getting closer to the end of the current list, request more items and add them to the list, calling the code above again.

I have implemented something similar, which is an endless pager with loads of hi-res pictures, however, I am not using fragments, so I am not fully aware of how they are removed from the list.

I can see you do not override isViewFromObject, perhaps you want to add the following method in your pager adapter:

@Override
public boolean isViewFromObject(View view, Object object) {
    View _view = (View) object;
    return view == _view;
}
like image 115
tbraun Avatar answered Oct 16 '22 00:10

tbraun


I looked at the sources and first that comes to mind is that no one really calls destroyItem(). You should probably do it yourself.

like image 22
Dmide Avatar answered Oct 15 '22 23:10

Dmide