Reputation: 1698
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.
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");
}
}
My application is very graphical because each page displays:
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:
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:
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.
Upvotes: 6
Views: 8429
Reputation: 173
Sorry for reviving an old post, bu I thought I'd contribute my approch.
I solved it in my app by using
tiv.setImageBitmap(BitmapFactory.decodeFile(outFile.getPath()));
instead of
tiv.setImageDrawable(Drawable.createFromPath(outFile.getAbsolutePath()));
to set the image to my ImageView. My app now regularly cleares heap to ~25-30 MB. With my previous approach my heap grew endlessly. It seems that Bitmapfactory can handle it's memory usage better.
Upvotes: 0
Reputation: 2666
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;
}
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;
}
Upvotes: 4
Reputation:
You have added the intent filter MAIN to the Splash activity. It ends,and the next thread that does all the heavy work(MainActivity) has the default priority(0). Android activity priority can be from 0 to 1000.Along with this I've added android:largeHeap as well to your manifest.
Also you can have two activity's with intent filter MAIN. Update your manifest like 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>
Upvotes: 0
Reputation: 987
Try using https://github.com/nostra13/Android-Universal-Image-Loader to populate your ImageViews instead of the default method. This library takes care of the memory, caches the images and much more. Your plroblem possibly lies in
mLeft.setImageResource(this.dayUtilPrev.getDrawable());
mCenter.setImageResource(dayUtil.getDrawable());
mRight.setImageResource(this.dayUtilNext.getDrawable());
Those funcions, which are called each time a new fragment is called, use whole lot of bitmap decoding functions like
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 notime if not handled properly
Upvotes: 0
Reputation: 2488
With the code you provided, things look fine but there's one method that I suggest you to check it carefully:
ViewUtil.setTypeFace(app.getTrebuchet(), vg);
Be careful when using custom typeface since it may causes memory leak. You can read these two topic for more details and solution:
Memory leaks with custom font for set custom font
https://code.google.com/p/android/issues/detail?id=9904
Upvotes: 1
Reputation: 6462
I looked at the sources and first that comes to mind is that no one really calls destroyItem()
. You should probably do it yourself.
Upvotes: 1
Reputation: 86
I ran into a similar problem earlier with my app. The solution that worked for me was to have my adapter implement SmartFragmentStatePagerAdapter. https://github.com/thecodepath/android_guides/wiki/ViewPager-with-FragmentPagerAdapter
Upvotes: 0