Memory Issues
So I am writing an app that should be able to page through detail views that have one large 640 x 480 image on top and 3 images that are part of a gallery that is being lazy loaded. Following Google design guidelines this is what they suggest doing. I can page through maybe 12 - 13 fragments before it crashes because of being out of memory. I think that there are a couple of culprits in this problem.
1.) I am using the FragmentStatePager. Shouldn't this be destroying the fragments that are not being viewed when memory becomes an issue? This is not happening. I thought it was automatic. What do I have to do to make this happen? Could it have something to do with how I have my Fragment implemented? I do all of my Activity config in onCreateView. For the sake of thoroughness I've included the source for this. Plain Vanilla here:
public static class MyAdapter extends FragmentStatePagerAdapter {
public MyAdapter(FragmentManager fm) {
super(fm);
}
#Override
public int getCount() {
return NUM_ITEMS;
}
#Override
public Fragment getItem(int position) {
return InventoryDetailFragment.newInstance(position);
}
}
2.) I have a method that is trying to figure out the size of the image that needs to be downloaded without placing it in memory. Then compresses the image while downloading it to the required size. This is not successfully implemented. But I'm not sure what is going wrong.
private Bitmap downloadBitmap(String url, int width, int height) {
Bitmap bitmap = null;
int scale = 1;
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
bitmap = BitmapFactory.decodeStream((InputStream)new URL (url).getContent(), null, options);
if (options.outHeight > height || options.outWidth > width) {
scale = (int) Math.max(((options.outHeight)/ height), ((options.outWidth)/ width)); }
BitmapFactory.Options o2 = new BitmapFactory.Options();
o2.inSampleSize = scale;
bitmap = BitmapFactory.decodeStream((InputStream)new URL (url).getContent(), null, o2);
cache.put(url, new SoftReference<Bitmap>(bitmap));
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (Error e){
Log.d("TEST", "Garbage Collector called!");
System.gc();
}
return bitmap;
}
I have tried everything that I know how to do but it's beyond my meager grasp of Android/Java. Please help! Thanks!
There are a few things that you need to change:
This is a horrible idea: BitmapFactory.decodeStream((InputStream)new URL (url).getContent(), null, options); You're getting the image from the web each time this is executed (so twice in the code you posted). Instead, you need to download the image and cache it locally.
Add logic to your fragments to call recycle() on the bitmaps as soon as the fragment is detached. Add logic to always reload the image (from the cache) whenever the fragment is attached.
Lastly, your inSampleSize calculation is wrong. inSampleSize should be a value that's a power of two, e.g. 1,2,4,8. You can use logarithms or simple binary logic to get the right one, this is what I use, which will always downsample using at least 2 (only call this if you know that the image is too big):
-
int ratio = (int) Math.max((height/options.outHeight), ( width/options.outWidth); //notice that they're flipped
for (int powerOfTwo = 64; powerOfTwo >=2; powerOfTwo = powerOfTwo >> 1 ) { //find the biggest power of two that represents the ratio
if ((ratio & powerOfTwo) > 0) {
return powerOfTwo;
}
}
if you realize your graphics with opengl, this would not counted to memory.
Another ooption is to use
android:largeHeap="true"
in the manifest. Could be working.
did you use ddvm to search for memory leaks?
http://www.youtube.com/watch?v=_CruQY55HOk
Related
Days, I've spent working on this. Weeks, perhaps. Literally. :(
So I've got an image on an SD card that more than likely came out of the built-in camera. I want to take that image and downsample it to an arbitrary size (but always smaller and never larger). My code uses standard Android Bitmap methods to decode, resize, recompress, and save the image. Everything works fine as long as the final image is smaller than 3MP or so. If the image is larger, or if I try to do several of these at once, the application crashes with an OutOfMemoryError. I know why that's happening, and I know it's happening for a perfectly legitimate reason, I just want it to not happen anymore.
Look, I'm not trying to launch a rocket here. All I want to do is resize a camera image and dump it to an OutputStream or even a temporary file. Surely someone out there must have done such a thing. I don't need you to write my code for me, and I don't need my hand held. But between my various programming abortions and days of obsessed Googling, I don't even know which direction to head in. Roughly speaking, does anyone know how to decode a JPEG, downsample it, re-compress it in JPEG, and send it out on an OutputStream without allocating a massive amount of memory?
Ok I know it's a little bit late but, I had this problem and I found solution. It is actually easy and I am sure it supports back to api 10(I have no idea about before 10). I tried this with my phone. It is a samsung galaxy s2 with an 8mp camera and the code perfectly resized camera images to the 168x168 as well as images i found on web. I checked the images by using file manager too. I never tried resizing images to bigger resoulation.
private Bitmap resize(Bitmap bp, int witdh, int height){
return Bitmap.createScaledBitmap(bp, width, height, false);
}
you can save it like this
private void saveBitmap(Bitmap bp) throws FileNotFoundException{
String state = Environment.getExternalStorageState();
File folder;
//if there is memory card available code choose that
if (Environment.MEDIA_MOUNTED.equals(state)) {
folder=Environment.getExternalStorageDirectory();
}else{
folder=Environment.getDataDirectory();
}
folder=new File(folder, "/aaaa");
if(!folder.exists()){
folder.mkdir();
}
File file=new File(folder, (int)(Math.random()*10000)+".jpg");
FileOutputStream os=new FileOutputStream(file);
bp.compress(Bitmap.CompressFormat.JPEG, 90, os);
}
thanks to this link
The following code is from my previous project. Key point is "options.inSampleSize".
public static Bitmap makeBitmap(String fn, int minSideLength, int maxNumOfPixels) {
BitmapFactory.Options options;
try {
options = new BitmapFactory.Options();
options.inPurgeable = true;
options.inJustDecodeBounds = true;
BitmapFactory.decodeFile(fn, options);
if (options.mCancel || options.outWidth == -1
|| options.outHeight == -1) {
return null;
}
options.inSampleSize = computeSampleSize(
options, minSideLength, maxNumOfPixels);
options.inJustDecodeBounds = false;
//Log.e(LOG_TAG, "sample size=" + options.inSampleSize);
options.inDither = false;
options.inPreferredConfig = Bitmap.Config.ARGB_8888;
return BitmapFactory.decodeFile(fn, options);
} catch (OutOfMemoryError ex) {
Log.e(LOG_TAG, "Got oom exception ", ex);
return null;
}
}
private static int computeInitialSampleSize(BitmapFactory.Options options,
int minSideLength, int maxNumOfPixels) {
double w = options.outWidth;
double h = options.outHeight;
int lowerBound = (maxNumOfPixels == UNCONSTRAINED) ? 1 :
(int) Math.ceil(Math.sqrt(w * h / maxNumOfPixels));
int upperBound = (minSideLength == UNCONSTRAINED) ? 128 :
(int) Math.min(Math.floor(w / minSideLength),
Math.floor(h / minSideLength));
if (upperBound < lowerBound) {
// return the larger one when there is no overlapping zone.
return lowerBound;
}
if ((maxNumOfPixels == UNCONSTRAINED) &&
(minSideLength == UNCONSTRAINED)) {
return 1;
} else if (minSideLength == UNCONSTRAINED) {
return lowerBound;
} else {
return upperBound;
}
}
I know this must be one of the most asked things at SO, but none of the other answers gave me a solution. But from reading the other answers, looks like I'll need to redesign the way the App is working.
It's like this, we have a ScrollView, which will inflate some views. A ListView can't be used in this situation, because to behave the way we want it would require extending the ListView, and this is something we don't want to do (even though this seems to be our only solution to our current way of showing items, because of this OOM exception). The list can have a lot of columns per row, and the bigger the screen, more columns it will have.
Each inflated View has a layout displaying some info from the database, including a picture. This picture is stored through a byte array. It's any picture taken with the device camera. Currently every photo (byte array) is taking 800kb to 1mb, which seems a lot to me. Now the list have 30+ items. I took photos until the OOM happened, and it happened when I took a total of 6 photos (occasionally 7). That would be 8mb-9mb of data. Everytime I go to other Activity, and go back to the Activity the ScrollView is in, the list needs to be repopulated.
This is the snippet of the PopulateList method:
if (item.getImg() != null) {
if (App.debug) {
Log.d(TAG, "Setting bmp.");
}
Bitmap bmp = App.byteArrayToBmp(item.getImg());
imgV.setImageBitmap(bmp);
}
Every inflated View will open an 'Advanced Dialog', which will contain other info. Maybe the Image could be there instead on the list (meaning that there would be only 1 bitmap, as every inflated View shares the same advanced dialog). Or I could extend the ListView and benefit from it recycling method (It's not a good solution as I though it would be considering more than 6 items can be at the screen). Another thing that bothers me is every picture having 800kb. Seems like a lot for a 128x128.
This is the setup for the size:
cameraParams.setPictureSize(App.pxToDpi(128), App.pxToDpi(128));
cameraParams.setPictureFormat(PixelFormat.JPEG);
camera.setParameters(cameraParams);
public static int pxToDpi(int px) {
final int scale = app.getApplicationContext().getResources().getDisplayMetrics().densityDpi;
int pixels = (int) px * (scale / 160);
return pixels;
}
So, do you think there is a solution to my issue keeping the current model of my App, or will I need to reformulate?
EDIT: The bitmap method:
public static Bitmap byteArrayToBmp(byte[] byteArray) {
Bitmap img = null;
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inSampleSize = 2;
img = BitmapFactory.decodeByteArray(byteArray, 0, byteArray.length, opts);
return img;
}
You might want to look at the Official Android Training docs, they've just been updated:
Check out Displaying Bitmaps Efficiently with the lesson: Loading Large Bitmaps Efficiently that goes over this.
Basically you can decode the image using sampleSize to decode it to the width and height you want:
public static int calculateInSampleSize(
BitmapFactory.Options options, int reqWidth, int reqHeight) {
// Raw height and width of image
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth) {
if (width > height) {
inSampleSize = Math.round((float)height / (float)reqHeight);
} else {
inSampleSize = Math.round((float)width / (float)reqWidth);
}
}
return inSampleSize;
}
Explained in much great detail in the links above
Is it a good practice to catch OutOfMemoryError even you have tried some ways to reduce memory usage? Or should we just not catching the exception? Which one is better practice?
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
bitmap = BitmapFactory.decodeFile(file, options);
} catch (OutOfMemoryError e) {
e.printStackTrace();
}
Thanks
It's good practice to catch it once and give decodeFile another chance. Catch it and call System.gc() and try decoding again. There is a high probability that it will work after calling System.gc().
try {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 4;
bitmap = BitmapFactory.decodeFile(file, options);
} catch (OutOfMemoryError e) {
e.printStackTrace();
System.gc();
try {
bitmap = BitmapFactory.decodeFile(file);
} catch (OutOfMemoryError e2) {
e2.printStackTrace();
// handle gracefully.
}
}
I did something like this: I catch the error only for try to scale down the image until it works. Eventually it can not work at all; then returns null; otherwise, in success, returns the bitmap.
Outside I decide what to do with the bitmap whether it's null or not.
// Let w and h the width and height of the ImageView where we will place the Bitmap. Then:
// Get the dimensions of the original bitmap
BitmapFactory.Options bmOptions= new BitmapFactory.Options();
bmOptions.inJustDecodeBounds= true;
BitmapFactory.decodeFile(path, bmOptions);
int photoW= bmOptions.outWidth;
int photoH= bmOptions.outHeight;
// Determine how much to scale down the image.
int scaleFactor= (int) Math.max(1.0, Math.min((double) photoW / (double)w, (double)photoH / (double)h)); //1, 2, 3, 4, 5, 6, ...
scaleFactor= (int) Math.pow(2.0, Math.floor(Math.log((double) scaleFactor) / Math.log(2.0))); //1, 2, 4, 8, ...
// Decode the image file into a Bitmap sized to fill the View
bmOptions.inJustDecodeBounds= false;
bmOptions.inSampleSize= scaleFactor;
bmOptions.inPurgeable= true;
do
{
try
{
Log.d("tag", "scaleFactor: " + scaleFactor);
scaleFactor*= 2;
bitmap= BitmapFactory.decodeFile(path, bmOptions);
}
catch(OutOfMemoryError e)
{
bmOptions.inSampleSize= scaleFactor;
Log.d("tag", "OutOfMemoryError: " + e.toString());
}
}
while(bitmap == null && scaleFactor <= 256);
if(bitmap == null)
return null;
For example, with an image of 3264x2448, the loop iterates 2 times on my phone, and then it works.
You'd want to catch it if you want to display either a smaller image / different image / show a custom error message to the user.
Your image access wrapper can catch these errors and return some custom error codes defined within your code; your activity that uses this code can decide what to do with the error code - warn user, force him to exit with a better error message than the one the android system would provide, etc.
Btw, you are not using the options variable in your sample code.
Though it might not be a good idea to catch OutOfMemoryError using try-catch. But, sometimes you have no choice, because all of us hate app crashes.
So, what you can do is
Catch OutOfMemoryError using try-catch
Since, after this error your activity may become unstable, restart it.
You may disable animations so that user doesn't know that activity is restarted.
You may put some extra data in intent to know that app was crashed during previous run.
How I did is:
try {
//code that causes OutOfMemoryError
} catch (Exception e) {
// in case of exception handle it
e.printStackTrace();
} catch (OutOfMemoryError oome)
{
//restart this activity
Intent i=this.getIntent();
i.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION); //disable animation
//EXTRA_ABNORMAL_SHUTDOWN is user defined
i.putExtra(this.EXTRA_ABNORMAL_SHUTDOWN, true);
//put extra data into intent if you like
finish(); //and finish the activity
overridePendingTransition(0, 0);
startActivity(i); //then start it(there is also restart method in newer API)
return false;
}
And then on onCreate of Activity you can resume(something like this):
boolean abnormalShutdown=getIntent().getBooleanExtra(this.EXTRA_ABNORMAL_SHUTDOWN, false);
if (abnormalShutdown)
{
//Alert user for any error
//get any extra data you put befor restarting.
}
This approach saved my app.
Hope it helps you too!!
I'm having an odd problem with my map pin sizes. To preserve dynamic-ness, the map pins for different categories are stored on a site's server so that they can be changed at any point even after the app is published.
I'm caching the pins every time I download them and I only ever re-download them if the server sends back a bit saying that one has changed since last I downloaded it. The first time I grab the pins, I use the bitmaps before I save them to files and the map markers are the correct size. Every time after that I'm loading a saved version of the pins straight from the image file. These are displaying considerably smaller than they are when using the bitmaps from the first download.
At first, I thought it was a problem with the way I'm saving the PNGs, but their sizes are correct (64 x 64). Is this a dip/px issue or do I need to decompress the image files with some sort of option?
Here's how I grab the images the first time:
public static Bitmap loadMapPin(String category, int width, int height) {
URL imageUrl;
category = category.toLowerCase().replace(" ", "");
try {
imageUrl = new URL(PIN_URL+category+".png");
InputStream is = (InputStream) imageUrl.getContent();
Options options = new Options();
options.inJustDecodeBounds = true; //Only find the dimensions
//Decode without downloading to find dimensions
BitmapFactory.decodeStream(is, null, options);
boolean scaleByHeight = Math.abs(options.outHeight - height) >= Math.abs(options.outWidth - width);
if(options.outHeight * options.outWidth >= width * height){
// Load, scaling to smallest power of 2 that'll get it <= desired dimensions
double sampleSize = scaleByHeight
? options.outHeight / height
: options.outWidth / width;
options.inSampleSize =
(int)Math.pow(2d, Math.floor(
Math.log(sampleSize)/Math.log(2d)));
}
options.inJustDecodeBounds = false; //Download image this time
is.close();
is = (InputStream) imageUrl.getContent();
Bitmap img = BitmapFactory.decodeStream(is, null, options);
return img;
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
And here's how I'm loading them from the cached file:
BitmapFactory.decodeFile(filepath);
Thanks in advance!
I've found that, by default, decompressing an image to a bitmap doesn't scale with high density screens. You have to set the density to none. In other words, you specify that the image is meant for an unknown density.
Solution:
Bitmap b = BitmapFactory.decodeFile(filepath);
b.setDensity(Bitmap.DENSITY_NONE);
In my application I have photos, videos, etc.. In the case of images, I have done scaling, but sometimes I get an OutOfMemoryError. How can I handle the error efficiently?
Check that the image size is smaller than the available memory before attempting to load it. So the most efficient way to handle OutOfMemoryException is to architecture your application in such a way that it never attempts to load lots of data into memory in order to avoid the exception.
There is a method in Activity which is called when the device is coming low of memory, but this can only be used to trigger cache files cleaning. This does not mean that your application process is coming out of memory.
You could also add a try catch block to catch Error or OutOfMemoryError, but this would be too late.
Handling large numbers of Bitmaps or large Bitmaps is really difficult in android applications. You'll find some tips on this subject in this article from Romain Guy.
You can also take care of loading bitmaps directly to the resolution you need by specifying a sample size in the BitmapFactory.options you provide to BitmapFactory.decode*() methods.
When dealing with OutOfMemory errors related to bitmap manipulation, checking the size of the decoded bitmap is the best and as far I know only option. Code follows:
public static BitmapFactory.Options getBitmapOptionsWithoutDecoding(String url){
BitmapFactory.Options opts = new BitmapFactory.Options();
opts.inJustDecodeBounds = true;
BitmapFactory.decodeFile(url, opts);
return opts;
}
public static int getBitmapSizeWithoutDecoding(String url){
BitmapFactory.Options opts = getBitmapOptionsWithoutDecoding(url);
return opts.outHeight*opts.outWidth*32/(1024*1024*8);
}
//ref:http://stackoverflow.com/questions/6073744/android-how-to-check-how-much-memory-is-remaining
public static double availableMemoryMB(){
double max = Runtime.getRuntime().maxMemory()/1024;
Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo();
Debug.getMemoryInfo(memoryInfo);
return (max - memoryInfo.getTotalPss())/1024;
}
public static final long SAFETY_MEMORY_BUFFER = 10;//MB
public static boolean canBitmapFitInMemory(String path){
long size = getBitmapSizeWithoutDecoding(path);
Log.d(TAG, "image MB:"+size);
return size <= availableMemoryMB() - SAFETY_MEMORY_BUFFER;
}
ref: http://developer.android.com/training/displaying-bitmaps/load-bitmap.html
I have started to try this routine which loads a jpeg into an ImageView and checks for Out of Memory and re-scales until it fits.
static public boolean tryJpegRead(ImageView imageView, File fn){
if (!fn.exists()){
Log.d("ANDRO_ASYNC",String.format("missing file %s",fn.getAbsolutePath()));
return false;
}
BitmapFactory.Options o = new BitmapFactory.Options();
for (int i = 1; i<10; i++){
o.inSampleSize = i;
o.inJustDecodeBounds = true;
BitmapFactory.decodeFile(fn.getAbsolutePath(), o);
int h = o.outHeight;
int w = o.outWidth;
Log.d("ANDRO_ASYNC",String.format("going in h=%d w=%d resample = %d",h,w,o.inSampleSize));
o.inJustDecodeBounds = false;
try{
imageView.setImageBitmap(
Bitmap.createScaledBitmap(
BitmapFactory.decodeFile(fn.getAbsolutePath(), o),
w,
h,
true));
return true; // only happens when there is no error
}catch(OutOfMemoryError E){
Log.d("ANDRO_ASYNC",String.format("catch Out Of Memory error"));
// E.printStackTrace();
System.gc();
}
}
return false;
}
In case you have big images like backgrounds or similar, a easy way to prevent Out Of Memory , is to move images from drawable-xhdpi to drawable-nodpi , but take care, this will load the bitmap without any modification.
The good way should be used BitmapFactory.options to fit your necessity
Use android:allowBackup="true", android:hardwareAccelerated="false" and android:largeHeap="true" for solve this
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher"
android:hardwareAccelerated="false"
android:largeHeap="true"
android:theme="#style/AppTheme">