ViewPager, PagerAdapter and Bitmap cause memory leak (OutOfMemoryError)

I have an Android app that displays weather data (I can give you the name of the app privately if you want to test the problem). The user can browse from one day to another to see the weather of a particular day.

Application architecture

My application uses fragments (the only MainActivity with a navigation box that calls specific fragments).

DayPagerFragment uses a ViewPager with an unlimited number of pages (dynamic fragments). The page is 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 optimization: it is controlled using the FragmentStatePagerAdapter , because the FragmentPagerAdapter not suitable for using / dynamic fragments (saves all fragments in memory).

The second option: I set the number of pages to be saved on both sides of the current page using 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"); } } 

Problem:

My application is very graphical because on every page it displays:

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

I quickly get OutOfMemoryError (after +/- 30 pages) when I view ViewPager pages.

It is like fragments are not freed from memory. The garbage collector does not work as I expected (I think this is because something is referencing 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 a memory leak, but I do not know why and where. I used the Eclipse MAT (Memory Analyzer), but I do not know where to look.

Can you help me?


Edit: to 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 a memory leak:

enter image description here


Edit 2: IMPORTANT!

I use the navigation box in my application, which is only processed by my MainActivity . The navigation box uses fragments (rather than actions).

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

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

The problem is that I am ((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 since I create a new fragment every time, the ViewPager cannot do its job! And MainActivity keeps a reference to each fragment: this is why the garbage collector does not free memory.

Now:

  • I do not know if my theory is correct.
  • How to fix it? How to call setCurrentPagerItemPrev and setCurrentPagerItemNext methods from setOnClickListener (see My updated code in DayPagerFragment )?

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

+6
source share
7 answers

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

Images use a ton of memory, so you need to do some work to quickly free them. I used the following code to help clear my memory, remove links that help clean up faster.

In addition, downloading many high-quality images in a short period of time can lead to errors, as Android slows down the heap growth a bit. See My answer to: Android Understanding Heap Sizes . You may need to install android: largeHeap = "true" in your manifest file, but only after optimizing images and fonts.

Use the DDMS Heap view to check if your memory supports a level, that is, you clear scrolled pages. To do this, you must install the plug-in for Android tools 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 how you get your fonts. Make sure you cache all Typeface.createFromAsset (...) calls, as shown 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: additional information after updating the question

Modify the adapter to use the list of objects:

Return the size of this list to getCount (),

When adding objects to a list call:

 notifyDataSetChanged(); mPagerContainer.invalidate(); 

Then, as you near the end of the current list, request more items and add them to the list by calling the code above again.

I implemented something similar, which is an endless pager with lots of hi-res images, however I don’t use fragments, so I don’t quite understand how they are removed from the list.

I see that you are not overriding isViewFromObject, perhaps you want to add the following method to your pager adapter:

 @Override public boolean isViewFromObject(View view, Object object) { View _view = (View) object; return view == _view; } 
+4
source

With the code you provided, everything looks good, but there is one method that I suggest you check carefully:

 ViewUtil.setTypeFace(app.getTrebuchet(), vg); 

Use caution when using a custom font, as this may result in a memory leak. You can read these two topics for more details and solutions:
Custom font leak for custom font installation
https://code.google.com/p/android/issues/detail?id=9904

+1
source

I ran into a similar problem earlier with my application. The solution that worked for me was for the adapter adapter to implement SmartFragmentStatePagerAdapter. https://github.com/thecodepath/android_guides/wiki/ViewPager-with-FragmentPagerAdapter

0
source

Try using https://github.com/nostra13/Android-Universal-Image-Loader to populate your ImageViews instead of the default method. This library takes care of memory, caches images and more. Perhaps your problem is

 mLeft.setImageResource(this.dayUtilPrev.getDrawable()); mCenter.setImageResource(dayUtil.getDrawable()); mRight.setImageResource(this.dayUtilNext.getDrawable()); 

Those functions that are called every time a new fragment is called use many raster image decoding functions, such as

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)

Those will fill your memory in this case, if not processed correctly

0
source

You have added the MAIN filter to the Splash action. It ends, and the next thread that does all the hard work (MainActivity) has default priority (0). The priority of Android activity can be from 0 to 1000. In addition, I added android: largeHeap, as well as your manifest.

You can also have two actions with the MAIN intent filter. Update the manifest as shown below:

 <?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.example" > <uses-sdk android:minSdkVersion="11" android:targetSdkVersion="19" /> <uses-permission ...> <supports-screens android:smallScreens="false" android:normalScreens="true" android:largeScreens="true" android:xlargeScreens="true" android:anyDensity="true" /> <application android:name="com.example.MyApplication" android:largeHeap="true" android:allowBackup="true" android:icon="@drawable/ic_launcher" android:label="@string/app_name"> <!-- ACTIVITIES --> <activity android:name="com.example.ui.SplashActivity" android:label="@string/app_name" android:screenOrientation="portrait" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <activity android:name="com.example.ui.MainActivity" android:screenOrientation="portrait" android:label="@string/app_name" android:priority:"900"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity> <!-- ROBOSPICE SERVICES --> <service android:name="com.example.network.CalendarSpiceService" android:exported="false" /> </application> </manifest> 
0
source

Sorry for reviving the old post, I thought I contributed.

I solved this in my application using

 tiv.setImageBitmap(BitmapFactory.decodeFile(outFile.getPath())); 

instead

 tiv.setImageDrawable(Drawable.createFromPath(outFile.getAbsolutePath())); 

to set the image in my ImageView. My application now regularly clears a bunch to ~ 25-30 MB. With my previous approach, my pile grew endlessly. Bitmapfactory seems to do better with this memory usage.

0
source

All Articles