I developed an application that uses lots of images on Android.
The app runs once, fills the information on the screen (Layouts, Listviews, Textviews, ImageViews, etc) and user reads the information.
There is no animation, no special effects or anything that can fill the memory.
Sometimes the drawables can change. Some are android resources and some are files saved in a folder in the SDCARD.
Then the user quits (the onDestroy method is executed and app stays in memory by the VM ) and then at some point the user enters again.
Each time the user enters to the app, I can see the memory growing more and more until user gets the java.lang.OutOfMemoryError.
So what is the best/correct way to handle many images?
Should I put them in static methods so they are not loaded all the time?
Do I have to clean the layout or the images used in the layout in a special way?
One of the most common errors that I found developing Android Apps is the “java.lang.OutOfMemoryError: Bitmap Size Exceeds VM Budget” error. I found this error frequently on activities using lots of bitmaps after changing orientation: the Activity is destroyed, created again and the layouts are “inflated” from the XML consuming the VM memory available for bitmaps.
Bitmaps on the previous activity layout are not properly de-allocated by the garbage collector because they have crossed references to their activity. After many experiments I found a quite good solution for this problem.
First, set the “id” attribute on the parent view of your XML layout:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:id="#+id/RootView"
>
...
Then, on the onDestroy() method of your Activity, call the unbindDrawables() method passing a reference to the parent View and then do a System.gc().
#Override
protected void onDestroy() {
super.onDestroy();
unbindDrawables(findViewById(R.id.RootView));
System.gc();
}
private 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();
}
}
This unbindDrawables() method explores the view tree recursively and:
Removes callbacks on all the background drawables
Removes children on every viewgroup
It sounds like you have a memory leak. The problem isn't handling many images, it's that your images aren't getting deallocated when your activity is destroyed.
It's difficult to say why this is without looking at your code. However, this article has some tips that might help:
http://android-developers.blogspot.de/2009/01/avoiding-memory-leaks.html
In particular, using static variables is likely to make things worse, not better. You might need to add code that removes callbacks when your application redraws -- but again, there's not enough information here to say for sure.
To avoid this problem you can use native method Bitmap.recycle() before null-ing Bitmap object (or setting another value). Example:
public final void setMyBitmap(Bitmap bitmap) {
if (this.myBitmap != null) {
this.myBitmap.recycle();
}
this.myBitmap = bitmap;
}
And next you can change myBitmap w/o calling System.gc() like:
setMyBitmap(null);
setMyBitmap(anotherBitmap);
I've ran into this exact problem. The heap is pretty small so these images can get out of control rather quickly in regards to memory. One way is to give the garbage collector a hint to collect memory on a bitmap by calling its recycle method.
Also, the onDestroy method is not guaranteed to get called. You may want to move this logic/clean up into the onPause activity. Check out the Activity Lifecycle diagram/table on this page for more info.
This explanation might help:
http://code.google.com/p/android/issues/detail?id=8488#c80
"Fast Tips:
1) NEVER call System.gc() yourself. This has been propagated as a fix here, and it doesn't work. Do not do it. If you noticed in my explanation, before getting an OutOfMemoryError, the JVM already runs a garbage collection so there is no reason to do one again (its slowing your program down). Doing one at the end of your activity is just covering up the problem. It may causes the bitmap to be put on the finalizer queue faster, but there is no reason you couldn't have simply called recycle on each bitmap instead.
2) Always call recycle() on bitmaps you don't need anymore. At the very least, in the onDestroy of your activity go through and recycle all the bitmaps you were using. Also, if you want the bitmap instances to be collected from the dalvik heap faster, it doesn't hurt to clear any references to the bitmap.
3) Calling recycle() and then System.gc() still might not remove the bitmap from the Dalvik heap. DO NOT BE CONCERNED about this. recycle() did its job and freed the native memory, it will just take some time to go through the steps I outlined earlier to actually remove the bitmap from the Dalvik heap. This is NOT a big deal because the large chunk of native memory is already free!
4) Always assume there is a bug in the framework last. Dalvik is doing exactly what its supposed to do. It may not be what you expect or what you want, but its how it works. "
I had the exact same problem. After a few testing I found that this error is appearing for large image scaling. I reduced the image scaling and the problem disappeared.
P.S. At first I tried to reduce the image size without scaling the image down. That did not stop the error.
Following points really helped me a lot. There might be other points too, but these are very crucial:
Use application context(instead of activity.this) where ever possible.
Stop and release your threads in onPause() method of activity
Release your views / callbacks in onDestroy() method of activity
I suggest a convenient way to solve this problem.
Just assign the attribute "android:configChanges" value as followed in the Mainfest.xml for your errored activity.
like this:
<activity android:name=".main.MainActivity"
android:label="mainActivity"
android:configChanges="orientation|keyboardHidden|navigation">
</activity>
the first solution I gave out had really reduced the frequency of OOM error to a low level. But, it did not solve the problem totally. And then I will give out the 2nd solution:
As the OOM detailed, I have used too much runtime memory. So, I reduce the picture size in ~/res/drawable of my project. Such as an overqualified picture which has a resolution of 128X128, could be resized to 64x64 which would also be suitable for my application. And after I did so with a pile of pictures, the OOM error doesn't occur again.
I too am frustrated by the outofmemory bug. And yes, I too found that this error pops up a lot when scaling images. At first I tried creating image sizes for all densities, but I found this substantially increased the size of my app. So I'm now just using one image for all densities and scaling my images.
My application would throw an outofmemory error whenever the user went from one activity to another. Setting my drawables to null and calling System.gc() didn't work, neither did recycling my bitmapDrawables with getBitMap().recycle(). Android would continue to throw the outofmemory error with the first approach, and it would throw a canvas error message whenever it tried using a recycled bitmap with the second approach.
I took an even third approach. I set all views to null and the background to black. I do this cleanup in my onStop() method. This is the method that gets called as soon as the activity is no longer visible. If you do it in the onPause() method, users will see a black background. Not ideal. As for doing it in the onDestroy() method, there is no guarantee that it will get called.
To prevent a black screen from occurring if the user presses the back button on the device, I reload the activity in the onRestart() method by calling the startActivity(getIntent()) and then finish() methods.
Note: it's not really necessary to change the background to black.
The BitmapFactory.decode* methods, discussed in the Load Large Bitmaps Efficiently lesson, should not be executed on the main UI thread if the source data is read from disk or a network location (or really any source other than memory). The time this data takes to load is unpredictable and depends on a variety of factors (speed of reading from disk or network, size of image, power of CPU, etc.). If one of these tasks blocks the UI thread, the system flags your application as non-responsive and the user has the option of closing it (see Designing for Responsiveness for more information).
Well I've tried everything I found on the internet and none of them worked. Calling System.gc() only drags down the speed of app. Recycling bitmaps in onDestroy didn't work for me too.
The only thing that works now is to have a static list of all the bitmap so that the bitmaps survive after a restart. And just use the saved bitmaps instead of creating new ones every time the activity if restarted.
In my case the code looks like this:
private static BitmapDrawable currentBGDrawable;
if (new File(uriString).exists()) {
if (!uriString.equals(currentBGUri)) {
freeBackground();
bg = BitmapFactory.decodeFile(uriString);
currentBGUri = uriString;
bgDrawable = new BitmapDrawable(bg);
currentBGDrawable = bgDrawable;
} else {
bgDrawable = currentBGDrawable;
}
}
I had the same problem just with switching the background images with reasonable sizes. I got better results with setting the ImageView to null before putting in a new picture.
ImageView ivBg = (ImageView) findViewById(R.id.main_backgroundImage);
ivBg.setImageDrawable(null);
ivBg.setImageDrawable(getResources().getDrawable(R.drawable.new_picture));
FWIW, here's a lightweight bitmap-cache I coded and have used for a few months. It's not all-the-bells-and-whistles, so read the code before you use it.
/**
* Lightweight cache for Bitmap objects.
*
* There is no thread-safety built into this class.
*
* Note: you may wish to create bitmaps using the application-context, rather than the activity-context.
* I believe the activity-context has a reference to the Activity object.
* So for as long as the bitmap exists, it will have an indirect link to the activity,
* and prevent the garbaage collector from disposing the activity object, leading to memory leaks.
*/
public class BitmapCache {
private Hashtable<String,ArrayList<Bitmap>> hashtable = new Hashtable<String, ArrayList<Bitmap>>();
private StringBuilder sb = new StringBuilder();
public BitmapCache() {
}
/**
* A Bitmap with the given width and height will be returned.
* It is removed from the cache.
*
* An attempt is made to return the correct config, but for unusual configs (as at 30may13) this might not happen.
*
* Note that thread-safety is the caller's responsibility.
*/
public Bitmap get(int width, int height, Bitmap.Config config) {
String key = getKey(width, height, config);
ArrayList<Bitmap> list = getList(key);
int listSize = list.size();
if (listSize>0) {
return list.remove(listSize-1);
} else {
try {
return Bitmap.createBitmap(width, height, config);
} catch (RuntimeException e) {
// TODO: Test appendHockeyApp() works.
App.appendHockeyApp("BitmapCache has "+hashtable.size()+":"+listSize+" request "+width+"x"+height);
throw e ;
}
}
}
/**
* Puts a Bitmap object into the cache.
*
* Note that thread-safety is the caller's responsibility.
*/
public void put(Bitmap bitmap) {
if (bitmap==null) return ;
String key = getKey(bitmap);
ArrayList<Bitmap> list = getList(key);
list.add(bitmap);
}
private ArrayList<Bitmap> getList(String key) {
ArrayList<Bitmap> list = hashtable.get(key);
if (list==null) {
list = new ArrayList<Bitmap>();
hashtable.put(key, list);
}
return list;
}
private String getKey(Bitmap bitmap) {
int width = bitmap.getWidth();
int height = bitmap.getHeight();
Config config = bitmap.getConfig();
return getKey(width, height, config);
}
private String getKey(int width, int height, Config config) {
sb.setLength(0);
sb.append(width);
sb.append("x");
sb.append(height);
sb.append(" ");
switch (config) {
case ALPHA_8:
sb.append("ALPHA_8");
break;
case ARGB_4444:
sb.append("ARGB_4444");
break;
case ARGB_8888:
sb.append("ARGB_8888");
break;
case RGB_565:
sb.append("RGB_565");
break;
default:
sb.append("unknown");
break;
}
return sb.toString();
}
}
Related
According to Romain Guy this kind of code is prone to memory leak due to the fact that
.... views have a reference to the entire activity and therefore to
anything your activity is holding onto; usually the entire View
hierarchy and all its resources.
#Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
setContentView(label);
}
I am not clear about this.
Assuming an application with 1 activity, this is the longest lived object and can be recreated as needed. Which means that all of its instance fields (which can and usually are Views) can be null at any time.
And any static instance field will live for the same duration as the activity itself.
So how can we get a memory leak with code like the above or the following:
private static Drawable sBackground;
#Override
protected void onCreate(Bundle state) {
super.onCreate(state);
TextView label = new TextView(this);
label.setText("Leaks are bad");
if (sBackground == null) {
sBackground = getDrawable(R.drawable.large_bitmap);
}
label.setBackgroundDrawable(sBackground);
setContentView(label);
}
Assuming an application with 1 activity, this is the longest lived object
No, it is not. There are other objects in your process (e.g., Application, content providers) that will outlive an activity instance.
In particular, note that activities get destroyed and recreated by default on a configuration change (e.g., screen rotation).
And any static instance field will live for the same duration as the activity itself.
No. Static fields are around as long as the process is around. Your activity instances can be shorter-lived than that.
So how can we get a memory leak with code like the above or the following:
There is no static field in your first example.
Romain Guy explains the second scenario in the blog post that you linked to:
This code is very fast and also very wrong; it leaks the first activity created upon the first screen orientation change. When a Drawable is attached to a view, the view is set as a callback on the drawable. In the code snippet above, this means the drawable has a reference to the TextView which itself has a reference to the activity (the Context) which in turns has references to pretty much anything (depending on your code.)
And, if you added LeakCanary to your project, you would see the leak.
So, let's walk through this:
User taps on the home screen launcher icon for your app, which is tied to this activity
Your process is started
Your activity instance is created, and is then called with onCreate()
sBackground is null, and so you assign it the getDrawable() result
Your activity UI appears on the screen
The user sneezes and accidentally rotates the screen of the device as part of reacting to the sneeze
Your old activity instance is destroyed
A new activity instance is created, and is then called with onCreate()
sBackground is not null, and so you leave sBackground alone
And you have your leak. As Romain explained, sBackground has a not-very-obvious reference back to your original activity instance. So, now you have two outstanding instances of this activity: the leaked original, plus the new one created due to the configuration change.
TLDR This is no longer a memory leak, since 2010. Also, I would have said this was a bug (memory leak) in Android (since this reference was an implementation detail), not in your app. We didn't even know that setBackgroundDrawable (deprecated in favor of setBackground) calls setCallback which sets a strong reference to the view. Here I explain why Drawable has a reference to View anyway.
Romain Guy made a fix to the library in 2010 to prevent this from causing a memory leak by using a WeakReference, so this hasn't been a problem for many years:
I am currently developing a game with the android development enviornment. And for the past couple of months I've been dealing with a nasty OOM error. My first problem was that I was placing my drawables in the wrong folder (Drawable-xhdpi in drawable folder). But now, the OOM error eventually happens as you go through the game.
It is a rpg, basically compoed of menus in activity layouts with animations and things. and I've tried everything I could to fix it. I've tried the unbindDrawables method:
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();
}
}
which does help a lot, but it does not fix the issue. And I cannot use any of the bitmap.factory options or anything, since I load my images through xml in my drawables folder. My images aren't that big by the way, as activities have a background of 720x1280, with some smaller images, and the most total images I'll have on screen at a time is around 8.
So this lead me to think that I may have a memory leak. I did ALOT of research, and I found out that use this(the activity context) will cause a leak, and I should use the application context. However, If I make the switch, there is almost no difference.
So I used MAT to figure out what was going on, and most of my memory is going to byte[], android.graphics.bitmap. And if I drill down to find the cause of this, it seems that java.ref.finalizer is causing all of the retained memory in the VM.
The only reason I could think this is happening, is because whenever I start a new activity I use,
Intent fight = new Intent(this, StartScreen.class);
//add this flag to remove all previous activities
fight.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(fight);
finish();
which opens a new activity, but closes the one we were just in. So, I'm guessing the bitmaps are not being recycled correctly whenever I finish an activity? or is closing and starting activities like this bad for memory?
I've been on Google all day trying to find the solution to this problem and I can't find it. Any soulutions are appreciated, thank you for reading this!
P.S if you would like to see any snippets of my logcat or code or anything, I am more than happy to post it.
P.S P.S My game has about 10-12 different activities I switch between. For example If I have activities A,B,C I open A, Open B close A, Open C close B, open B close C, open A close B.
EDIT: As request about my activities. Usually it is a menu, and when you press a button, that activity finishes, and then moves into another activity. Or buttons will do some math for things like selling, or doing damage to an enemy. One thing about my activity architecture, is that since I am closing every activity as I go to a new one, when I go back to the ones I closed, I am re-creating them. So I don't know if the old activities I finished still have memory in the VM that over time causes the Out Of Memory error, since it all builds up and keeps expanding. I explained My call for a new activity above. And the intent flag closes all past activities (if there are any) in the stack.
EDIT EDIT: As per request my oncreate and onDestroy:
OnCreate:
#Override
protected void onCreate(Bundle savedInstanceState)
{
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_start_screen);
//setting fonts
//a function that binds views by findview by Id and then sets their typeface
setFont();
//set up the music service
//connects the app to the background music service
playMusic();
//aquire wakelock
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
OnDestroy:
#Override
public void onDestroy()
{
super.onDestroy();
//unbinds the service
unbindService(musicConnection);
//unbind drawables (function above)
unbindDrawables((LinearLayout) findViewById(R.id.container));
}
you should destroy the activities that you don't need anymore it takes place in the memory. or if you dont want your user/player to go back to the recent activity you should finish the activity. and if you will notice. if you dont finish the activity and press the back button several times it is layered.
second you mentioned that it is a game. in android programming you need to consider your bitmap resources so to avoid getting an OOM error in your game make use of sprite sheet it will save a lot of memory and usage of bitmaps in your application. i encountered those kind of situation and bitmaps mainly causes the OOM error.
I will say it depends. but with your problem your answer is right here
Quick quote from the site
Note: In most cases, you should not explicitly finish an activity using these methods.
As discussed in the following section about the activity lifecycle,the Android
system manages the life of an activity for you, so you do not need to finish your own
activities. Calling these methods could adversely affect the expected user experience
and should only be used when you absolutely do not want the user to return to this
instance of the activity.
Read for more info
well i get you lucidly now, but im thinking of what you are tryna do here, so y dont you use FragmentActivity for B and C.. So, Activity A opens B..B is opened as fragment but works like activity-(thats fragmentActivity), and opens C which is most likely a Fragment..which i think would be perfect for your situation..
for more info about FragmentActivity click Here
no more activities back and forth.. and its gonna work like an activity..
I have a very frustrating problem and I don't have an idea what is wrong.
I build simple XML which contains only GridView. This gridview should show images (ImageView) downloaded from specific urls which I retreive with my code.
So the idea of this APP is only to download images from URLS and show those images in GridView. First I must say that I'm using Universal Image Loader library. First please look at my getView code:
#Override
public View getView(int position, View convertView, ViewGroup parent) {
ImageView iv;
if(convertView == null) {
iv = new ImageView(MyActivity.this);
iv.setLayoutParams(new GridView.LayoutParams(size,size));
iv.setScaleType(ImageView.ScaleType.CENTER_CROP);
Log.d("APP", "convertView is NULL");
}
else {
iv = (ImageView) convertView;
Log.d("APP", "convertView is NOT null");
}
loader.displayImage(URLS.get(position), iv);
return iv;
}
PROBLEM:
Code above works, and it shows images in GridView. But when I scroll down, I see that:
All images were downloaded (Even those images who are not visible )
When i scroll back up I see that image which was in place 0, is in place 1 and it has been downloaded again. When I scroll back down, images switched positions again. Scrolling back up, I see again images are switching places again ( and downloading again). This goes infinite.
HOW I SOLVED THIS PROBLEM:
I solved this problem by NOT checking if convertView variable is null. Actually I ommited if/else clause.
I used cache in memory = true. So downloading stops and images are in place for ever.
So why I'm asking for help? Mainly because i know that i should not ommit if/else clause for performance reasons. I don't want to mess with users memory and I would like to bring them fast and reliable user experience. Also i would like to know what could be wrong.
Thank you for help. Much appriciated.
Loader needed some time to load picure in ImageView. Because you reuse view for different images you can see a previous image in the view while new image is loading. You can set resetViewBeforeLoading(true) in DisplayImageOptions for avoid this effect.
Also you can use disk cache to avoid downloading images every time from the network. Also limit size of memory cache and set other settings, but I think memory cache is useful, it improves user experience.
And don't forget to use setOnScrollListener(new PauseOnScrollListener(loader, true, true)) to avoid lags on scrolling.
GridView recycles list items for performance purposes. So when you are scrolling, the list items are getting recycled in different places and then your code is re-populating them. Sometimes this lags for images.
I'd recommend using a library that handles this sort of thing like Picasso.
I was having the exactly same problem as you, and didn't want to disable convertView checking either.
My solution was to increase the memory cache size in ImageLoaderConfiguration.
Before, i used to use it like this:
.memoryCache(new UsingFreqLimitedMemoryCache(2 * 1024 * 1024))
So I changed it to:
.memoryCache(new UsingFreqLimitedMemoryCache(10 * 1024 * 1024))
I don't know if 10*1024*1024 is too much or it will cause any problems, but it seems to have fixed the problem to me and i haven't had any problems until now.
I've read a ton of posts about both these errors and can't seem to find a solution that works for me.
I have a basic listview activity of animals. When you click on an item in the list, it opens the AnimalViewActivity and displays a simple png image inside an ImageView.
Here's where I set the image for the ImageView:
public void getImage() {
String imageName = myService.yourAnimals.get(key).getType();
System.out.println(imageName);
Resources res = getResources();
int resourceId = res.getIdentifier(imageName, "drawable", getPackageName() );
Drawable drawable = res.getDrawable( resourceId );
image.setImageDrawable( drawable );
}
Then when I leave AnimalViewActivity to return to the listview activity, I do this:
#Override
protected void onDestroy() {
super.onDestroy();
//((BitmapDrawable)image.getDrawable()).getBitmap().recycle();
image.getDrawable().setCallback(null);
//System.gc();
System.out.println("Destroy image!");
}
If I uncomment the recycle() line, I get the "cannot draw recycled bitmaps" error.
If I leave it how it is, I get the outofmemoryerror for bitmapfactory.
java.lang.IllegalArgumentException: Cannot draw recycled bitmap
OR
java.lang.OutOfMemoryError
android.graphics.BitmapFactory.nativeDecodeAsset(Native Method)
Either one of these errors occur after I've opened the ViewAnimalActivity about 20 times and the app crashes.
The only thing that seems to work forever is System.gc() , but I know that is not a good solution.
I'm testing it in android4.1, but the minSDK is set to android3.0
I read that the javaVM error for bitmap recycling was fixed in 3.0?
For some reason, garbage collection is not happening fast enough without explicitly calling the gc. These images are small, mostly between 100-300kB.
Any ideas?
EDIT:
It seems happier with
image.setImageDrawable( null );
I think I tried this before when I still had it set on Android2.2, but Android3.0 is happy with it so far.
The documentation for recycle() states explicitly that it is for "advanced use". It's more important to set the references to null, actually.
This is an advanced call, and normally need not be called, since the
normal GC process will free up this memory when there are no more
references to this bitmap.
There is, in addition, the possibility to look if a Bitmap has been recycled before drawing it. (post your logcat to see where you're having the error. You could add code to verify if the Bitmap has been recycled and re-load it).
Overall, I think you'd best look at the Android Training Lessons that tell you how to display Bitmaps efficiently.
Why don't you try setting the drawable to null first, then recycle it? The problem is probably because the drawable is still on the image when you recycled it, thus causing the "cannot draw recycled bitmaps error". If you set that to null first, thereby setting the actual ImageView (or whatever it is that you're using) to null, then recycling it, that will probably solve your issue.
Edit: WOOPS! I totally forgot if you set a drawable to null, then you can't retrieve the bitmap you're trying to recycle. Try assigning that to a variable first, then recycle it.
I have listView and custom ArrayAdapter for it. In my list View are many pictures and I use strong memory cleaning:
#Override
public void onDestroy() {
list.clear();
adapter.notifyDataSetChanged();
listView.setAdapter(null);
super.onDestroy();
}
And use LazyLoader for images loading.
But after some times using my application (when I updating list) I will still got "bitmap size exceeds vm budget" error. Although if I press home button and back to application memory is cleared. How can I clear memory without it?
Make sure you call recycle() on all of the Bitmap objects you are no longer using to free its memory as soon as possible.
Also, use Bitmap.createScaledBitmap() to create a Bitmap as small as your ImageView requires and thus saving some extra memory.
Try to read about Loading Large Bitmaps. This will help avoid problems with memory leaks.