📜 ⬆️ ⬇️

Android SDK: struggling with image memory size limits

In a graphics drawing application, SurfaceView is used and a couple of screen-sized Bitmap (for example, I want to portray the smooth scrolling of book pages).

On many devices with a higher screen resolution, the application crashes
AndroidRuntime: java.lang.OutOfMemoryError: bitmap size than VM budget

The problem is that the memory for Bitmap, as well as for SurfaceView, is reserved from the common process heap. The heap size limit is small, usually a little more than 10Mb. And this limit is set when building the system.
')
Attempts to improve the situation by cutting down the pixel format from 32 bits to 16 do not help much. The problem just comes out later - for example, when you open a window on top of SurfaceView (apparently, this creates another Bitmap the size of the screen).

Limiting the size of the program's graphic buffers in 3-4 screens is a little offensive! Let's try to correct such an injustice.

In fact, most of the Bitmap functionality is implemented in the native code (JNI), and the buffer is allocated not in the Java Heap, but with the help of the usual sish malloc. Why is it imposed restrictions on the total size of the heap?

We smoke source.

It turns out that each memory allocation for Bitmap is registered in dalvik.system.VMRuntime with the help of trackExternalAllocation / trackExternalFree methods. It is the trackExternalAllocation method that throws an exception when trying to allocate memory over the limit.

What if you try to fool a stupid robot? After placing the picture, say that the external memory that you just occupied has already been released. And before freeing the image, imitate that the corresponding memory size is just occupied.

It remains to overcome a small problem - the trackExternalAllocation and trackExternalFree methods are not visible. We'll have to call them "hacker" methods - through Reflection.

We try to implement this idea.
Create an empty Android Application project.

For convenience, access to VMRuntime is implemented in a separate class.

static class VMRuntimeHack { private Object runtime = null; private Method trackAllocation = null; private Method trackFree = null; public boolean trackAlloc(long size) { if (runtime == null) return false; try { Object res = trackAllocation.invoke(runtime, Long.valueOf(size)); return (res instanceof Boolean) ? (Boolean)res : true; } catch (IllegalArgumentException e) { return false; } catch (IllegalAccessException e) { return false; } catch (InvocationTargetException e) { return false; } } public boolean trackFree(long size) { if (runtime == null) return false; try { Object res = trackFree.invoke(runtime, Long.valueOf(size)); return (res instanceof Boolean) ? (Boolean)res : true; } catch (IllegalArgumentException e) { return false; } catch (IllegalAccessException e) { return false; } catch (InvocationTargetException e) { return false; } } public VMRuntimeHack() { boolean success = false; try { Class cl = Class.forName("dalvik.system.VMRuntime"); Method getRt = cl.getMethod("getRuntime", new Class[0]); runtime = getRt.invoke(null, new Object[0]); trackAllocation = cl.getMethod("trackExternalAllocation", new Class[] {long.class}); trackFree = cl.getMethod("trackExternalFree", new Class[] {long.class}); success = true; } catch (ClassNotFoundException e) { } catch (SecurityException e) { } catch (NoSuchMethodException e) { } catch (IllegalArgumentException e) { } catch (IllegalAccessException e) { } catch (InvocationTargetException e) { } if (!success) { Log.i(TAG, "VMRuntime hack does not work!"); runtime = null; trackAllocation = null; trackFree = null; } } } private static final VMRuntimeHack runtime = new VMRuntimeHack(); 


Creation / release of Bitmap will be arranged as a separate class - a factory.
Here we will remember for which of the bitmaps we corrected the information about the occupied memory - so as not to forget to return it when released.

  static class BitmapFactory { public BitmapFactory(boolean useHack) { this.useHack = useHack; } //   public Bitmap alloc(int dx, int dy) { Bitmap bmp = Bitmap.createBitmap(dx, dy, Bitmap.Config.RGB_565); if (useHack) { runtime.trackFree(bmp.getRowBytes() * bmp.getHeight()); hackedBitmaps.add(bmp); } allocatedBitmaps.add(bmp); return bmp; } //   public void free(Bitmap bmp) { bmp.recycle(); if (hackedBitmaps.contains(bmp)) { runtime.trackAlloc(bmp.getRowBytes() * bmp.getHeight()); hackedBitmaps.remove(bmp); } allocatedBitmaps.remove(bmp); } //    (  ) public void freeAll() { for (Bitmap bmp : new LinkedList<Bitmap>(allocatedBitmaps)) free(bmp); } private final boolean useHack; private Set<Bitmap> allocatedBitmaps = new HashSet<Bitmap>(); private Set<Bitmap> hackedBitmaps = new HashSet<Bitmap>(); } 


Now we will write a test that checks whether this method works. The testAllocation () method will try to create the maximum number of megabyte images (until OutOfMemory crashes or the specified limit is reached). UseHack checkbox sets the test with a hack or without. The method returns the volume that was managed to take under the pictures.

  public int testAllocation(boolean useHack, int maxAlloc) { System.gc(); BitmapFactory factory = new BitmapFactory(useHack); int allocated = 0; // AndroidRuntime: java.lang.OutOfMemoryError: bitmap size exceeds VM budget while (allocated < maxAlloc) { try { Bitmap bmp = factory.alloc(1024, 512); allocated += bmp.getRowBytes() * bmp.getHeight(); Log.i(TAG, "Bitmap bytes allocated " + allocated); } catch (OutOfMemoryError e) { Log.e(TAG, "Exception while allocation of bitmap, total size = " + allocated, e); break; } } factory.freeAll(); return allocated; } 


Call the test in Activity.onCreate () - with and without hack.
The results show on the screen and in the log. (Layout “main” needs to be corrected - add to TextView ID, according to which we will change the text).

  public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // perform test int allocatedNormally = testAllocation(false, 48 * MB); int allocatedWithHack = testAllocation(true, 48 * MB); String msg = "normally: " + (allocatedNormally / MB) + " MB allocated " + "\nwith hack: " + (allocatedWithHack / MB) + " MB allocated"; Log.i(TAG, msg); // display results LayoutInflater inflater = LayoutInflater.from(this); View main = (View)inflater.inflate(R.layout.main, null); TextView text = (TextView)main.findViewById(R.id.text); text.setText(msg); setContentView(main); } 


So, run and see the result:
03-07 09:43:37.233: I/bmphack(17873): normally: 10 MB allocated
03-07 09:43:37.233: I/bmphack(17873): with hack: 48 MB allocated


Without a hack, we managed to create only 10 images of 1 megabyte. With hack - the maximum amount we specified (48 megabytes).
With large sizes, the test still hung on my simulator - after ~ 58Mb.

I hope someone this article will be useful.

Source: https://habr.com/ru/post/139717/


All Articles