Android canvas.draw leaks graphic memory - android

I've have a custom view which overrides onDraw and I've noticed that Graphics memory keeps increasing overtime, until my app crashes with OOM (it ranges anywhere from 4h to 12h, based on device).
I'm doing a bit complex drawing but for reproducing purposes, this code does the trick:
package com.example.testdrawing;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.util.AttributeSet;
import android.view.View;
import androidx.annotation.Nullable;
import java.util.Random;
public class CustomView extends View {
private Random rand;
private Paint paint;
public CustomView(Context context) {
super(context);
init(context);
}
public CustomView(Context context, #Nullable AttributeSet attrs) {
super(context, attrs);
init(context);
}
public CustomView(Context context, #Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
init(context);
}
public CustomView(Context context, #Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
init(context);
}
private void init(Context context) {
rand = new Random();
paint = new Paint();
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(10);
// Simulate invalidation loop
final Thread thread = new Thread(new Runnable() {
#Override
public void run() {
while (true) {
// I invoke postInvalidate() when the rendering data change.
postInvalidate();
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
thread.start();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// 1) This one leaks memory
canvas.drawOval(0, 0, 500 + (rand.nextInt(100)), 900 + (rand.nextInt(100)), paint);
// 2) This one keeps graphic memory at constant
//canvas.drawOval(0, 0, 500, 900, paint);
}
}
Basically, the memory is retained whenever a drawing location is dynamic. If location is static, the memory remains at constant. In both cases, the graphic memory doesn't go down. Here is the profiler output after ~12 minutes for the CustomView:
Full sample here
EDIT(#PerracoLabs): I don't believe that Random is the culprit. This reproduces by just drawing to dynamic coordinates. I.e:
canvas.drawOval(x++ % 500, y++ % 500, w++ % 1080, h++ % 1000, paint);
Also, if these are just allocations stats, why are they accounted into total memory? If not released, it's a leak, right?
It is also strange that memory increase rate is ~100kb regardless of what's drawn.
EDIT 2:
I've attached full sample app that produces this (On Pixel 4, Android 10):
I've stopped profiling since the profiler slowed down to the point it became unusable.
Note that occasional drops where some memory is indeed freed.
Again, to me it doesn't make sense that for few draw calls, the overhead is ~200MB of allocated graphic memory.
I'd really like to understand what's going on here. Obviously, there is a difference when drawing on dynamic locations on the canvas compared when the location is fixed, at which time the memory consumption stabilises.

After testing your code, instead of a memory leak, seems to be allocated memory space which hasn't been reclaimed yet.
In Android the total used memory is the sum of everything including unused resources, and not necessarily resources allocated directly by yourself, these can be allocated by other methods.
Android uses a Garbage Collector to manage memory. The goal of the garbage collector is to ensure that there is enough free memory when it is needed, reclaiming it with minimal CPU overhead, rather than freeing as much memory as possible in one go.
In the test the draw method is being called nonstop, which keeps on allocating memory, but after a threshold it stops allocating anymore. In such test as it never stops calling the draw method, to be a leak, memory should keep growing always without stopping at any threshold, and eventually the system would kill the app.
Next a 37 minute screenshot. I took a few captures and overlapped the allocation info panels for better understanding. As you can see the memory growth is the native one, yet after 20 minutes there is no more growth staying at around 60Mb.
Note that calling explicitly the garbage collector will not free (reclaim) such memory, as System.gc() only triggers a suggestion and is at the discretion of the system to collect resources, which is basically when the JVM needs memory.

I know this is an old post, but I recently encountered the same problem.
The issue is with the paint object. I haven't tested it with all of the draw functions, but when you use a paint object with style STROKE and using canvas.drawCircle(x, y, radius, paint) where the x and y are changed at random - the graphics memory will be increased and never released until an inevitable OOM. This happens on Android 11, but not on Android 7 - haven't tested other versions.
You can easily reproduce it by creating a custom view like so:
class OOMLayout: FrameLayout {
val paint = Paint().apply {
color = Color.RED
style = Paint.Style.STROKE
strokeWidth = 4f
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val x = (100..400).random().toFloat()
val y = (100..400).random().toFloat()
canvas.drawCircle(x, y, 10f, paint)
invalidate()
return
}
}
This will take some time to reach the OOM but the memory leak is clearly visible in the android profiler.
To speed up the memory consumption put the drawCircle call inside a loop f.e. a 1000 iterations per onDraw - this way the app will OOM in few seconds.
When using paint with style FILL - there are no such problems on Android 11.

Related

How to 'exchange' the Renderer in a GLSurfaceView instance in android?

In android I have taken a rotating sphere example given here. It creates a simple app showing a rotating sphere (the earth).
Now, in a class derived from GLSurfaceView I wait for an even (like a touch-screen event) in order to exchange the renderer. I want the current renderer to stop rendering, and the GLSurfaceView should use a different renderer instead (to display some other object).
I have tried to use the following code:
public class MyGLSurfaceView extends GLSurfaceView {
Context mycontext;
MyGLSurfaceView(Context context) {
super(context);
mycontext = context;
}
public boolean onTouchEvent(MotionEvent event) {
int x = (int)event.getX();
int y = (int)event.getY();
Log.d("onTouchEvent",String.format("keyCode: %d coords: %d %d", event.getActionMasked(), x, y));
GlRenderer renderer = new GlRenderer(mycontext);
setRenderer(renderer);
return super.onTouchEvent(event);
}
}
which gives the following error:
FATAL EXCEPTION: main
Process: com.jimscosmos.opengltexturedsphere, PID: 25708
java.lang.IllegalStateException: setRenderer has already been called for this instance.
I guess I have to stop/remove/destroy the 'old' renderer, but I did not find anything useful for that in the documentation. Maybe my approach is completely wrong? What else to do? How to do it right?
If you insist, technically it can be achieved using a 'mediator' - as suggested here: Change GlSurfaceView renderer.
However - I think this is an overhead you really do not need. Simply prepare various objects and change the various rendering properties in a single renderer.

MPAndroidChart - is it possible to control z-index of the chart elements?

I would like to have the following order of drawing in my MPAndroidChart (from bottom to top):
Data connecting line
Limit line
Data points
Is it possible? I am aware of the method com.github.mikephil.charting.components.AxisBase#setDrawLimitLinesBehindData. It is working as expected except for one case. When the Y value of all data points is the same, the effect is:
or this:
I would like it to look like:
The first 2 pictures are from MPAndroidChart Android library. The 3rd one is from iOS port of the library: Charts
I looked at the order or drawing the chart in Android and iOS versions and they look the same.
Questions:
Is it possible to control the drawing order?
What comes the difference from between system versions?
Is there any other open source library that can do that?
Additional info: all images, lines, circles are drawn by the library, custom images are not used.
Similar to a previous answer here there is no public API exposed to directly set the z-index of the various drawing features.
Instead, components are drawn in order on the canvas with later components being drawn over earlier ones. This means you change the rendering order, you can change the z-index.
You say you would like the following drawing order:
Data connecting line
Limit line
Data points
Let's find the methods in the source code that deal with each of those:
Data connecting line is drawn inside LineChartRenderer in a method called:
protected drawLinear(Canvas c, ILineDataSet dataSet)
The limit line is drawn inside XAxisRenderer called:
public void renderLimitLines(Canvas c)
The data points (circles) are drawn inside LineChartRenderer in a method that looks like this:
public void drawExtras(Canvas c)
The calling order of these three methods is determined inside BarLineChartBase in the method overriden from Android's View:
protected onDraw(Canvas canvas);
So to get the order you want you will have to simply re-arrange the order of calling of the above 3 methods inside onDraw(Canvas canvas):
Here is the complete code for a custom line chart that should meet the requirement. By design, you will still have to call:
com.github.mikephil.charting.components.AxisBase#setDrawLimitLinesBehindData
But you could easily remove the 3 if statements and hard-code the order should you so desire.
CustomZIndexBarLineBase.java
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import com.github.mikephil.charting.charts.BarLineChartBase;
import com.github.mikephil.charting.data.LineData;
/**
* Created by David on 11/01/2017.
*/
public class CustomZIndexLineChartBase extends BarLineChartBase<LineData> {
public CustomZIndexLineChartBase(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public CustomZIndexLineChartBase(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomZIndexLineChartBase(Context context) {
super(context);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (mData == null)
return;
// execute all drawing commands
drawGridBackground(canvas);
if (mAxisLeft.isEnabled())
mAxisRendererLeft.computeAxis(mAxisLeft.mAxisMinimum, mAxisLeft.mAxisMaximum, mAxisLeft.isInverted());
if (mAxisRight.isEnabled())
mAxisRendererRight.computeAxis(mAxisRight.mAxisMinimum, mAxisRight.mAxisMaximum, mAxisRight.isInverted());
if (mXAxis.isEnabled())
mXAxisRenderer.computeAxis(mXAxis.mAxisMinimum, mXAxis.mAxisMaximum, false);
mXAxisRenderer.renderAxisLine(canvas);
mAxisRendererLeft.renderAxisLine(canvas);
mAxisRendererRight.renderAxisLine(canvas);
if (mAutoScaleMinMaxEnabled) {
autoScale();
}
mXAxisRenderer.renderGridLines(canvas);
mAxisRendererLeft.renderGridLines(canvas);
mAxisRendererRight.renderGridLines(canvas);
if (mXAxis.isDrawLimitLinesBehindDataEnabled())
mXAxisRenderer.renderLimitLines(canvas);
if (mAxisLeft.isDrawLimitLinesBehindDataEnabled())
mAxisRendererLeft.renderLimitLines(canvas);
if (mAxisRight.isDrawLimitLinesBehindDataEnabled())
mAxisRendererRight.renderLimitLines(canvas);
int clipRestoreCount = canvas.save();
canvas.clipRect(mViewPortHandler.getContentRect());
mRenderer.drawData(canvas); //NOTE: draws line between points
if (valuesToHighlight())
mRenderer.drawHighlighted(canvas, mIndicesToHighlight);
canvas.restoreToCount(clipRestoreCount);
//NOTE: draws limit line
if (!mXAxis.isDrawLimitLinesBehindDataEnabled())
mXAxisRenderer.renderLimitLines(canvas);
if (!mAxisLeft.isDrawLimitLinesBehindDataEnabled())
mAxisRendererLeft.renderLimitLines(canvas);
if (!mAxisRight.isDrawLimitLinesBehindDataEnabled())
mAxisRendererRight.renderLimitLines(canvas);
mRenderer.drawExtras(canvas); //NOTE: draws circles
mXAxisRenderer.renderAxisLabels(canvas);
mAxisRendererLeft.renderAxisLabels(canvas);
mAxisRendererRight.renderAxisLabels(canvas);
if (isClipValuesToContentEnabled()) {
clipRestoreCount = canvas.save();
canvas.clipRect(mViewPortHandler.getContentRect());
mRenderer.drawValues(canvas);
canvas.restoreToCount(clipRestoreCount);
} else {
mRenderer.drawValues(canvas);
}
mLegendRenderer.renderLegend(canvas);
drawDescription(canvas);
drawMarkers(canvas);
}
}
CustomZIndexLineChart.java
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import com.github.mikephil.charting.data.LineData;
import com.github.mikephil.charting.interfaces.dataprovider.LineDataProvider;
import com.github.mikephil.charting.renderer.LineChartRenderer;
/**
* Created by David on 11/01/2017.
*/
public class CustomZIndexLineChart extends CustomZIndexLineChartBase implements LineDataProvider {
public CustomZIndexLineChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public CustomZIndexLineChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
public CustomZIndexLineChart(Context context) {
super(context);
}
#Override
protected void init() {
super.init();
mRenderer = new LineChartRenderer(this, mAnimator, mViewPortHandler);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
}
#Override
public LineData getLineData() {
return mData;
}
#Override
protected void onDetachedFromWindow() {
// releases the bitmap in the renderer to avoid oom error
if (mRenderer != null && mRenderer instanceof LineChartRenderer) {
((LineChartRenderer) mRenderer).releaseBitmap();
}
super.onDetachedFromWindow();
}
}

How do I receive a callback when drawing graph completes in MPAndroidChart?

I am having an issue where I am refreshing a LineDataSet graph real time by updating the data and calling mMyChart.invalidate().
One thread is requesting the data from a server, processing it, and sending it via a message to the second thread, which is the main activity thread and is responsible for drawing the graph.
The issue is this call to invalidate seems to complete before the chart has actually redrawn, so my other thread requesting the data goes off and requests more data and sends it to the thread to re draw the chart. The thread drawing the charts gets very behind after a while and starts taking a long time to respond to anything, the longer it has been running, the longer it takes.
My question is straight forward in that I want to make it so I don't continue requesting data until the plot has finished drawing. How do I receive a callback when drawing completes? Alternately, is there a way to detect when it is drawing?
Thanks,
Eric
You could try a ViewTreeObserver (related SO question here)
A view tree observer is used to register listeners that can be notified of global changes in the view tree. Such global events include, but are not limited to, layout of the whole tree, beginning of the drawing pass, touch mode change.... A ViewTreeObserver should never be instantiated by applications as it is provided by the views hierarchy. Refer to getViewTreeObserver() for more information.
Or you could try extending the particular Chart type you are using and override onDraw(Canvas canvas) to call a listener when the draw pass completes:
public class ExtendedBarChart extends BarChart {
public ExtendedBarChart(Context context) {
super(context);
}
public ExtendedBarChart(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ExtendedBarChart(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// TODO: Drawing completed, execute callback...
}
}

OPENGL ES MatrixGrabber and GLU.gluUnproject() mayhem

[EDIT] Ignore this post, it was a noob's mistake [/EDIT]
I started Android last week (= I'm pretty new to it :) ) and I'm banging my head on a problem:
I try to perfom a raycasting to get the objects under a point of the screen.
I found out that the GLU.gluUnproject method was what I needed, after a decent amount of failures, I found a solution
I've copied the MatrixGrabber, MatrixStack and MatrixTrackingGL classes in my project, they seem fine.
the method I use goes as follow:
static public Vertex unProject( float x, float y, World world )
{
world.mg = new MatrixGrabber();
world.mg.getCurrentState(world.gl);
float[] pos = new float[4];
GLU.gluUnProject( x, y, 0f,
world.mg.mModelView, 0,
world.mg.mProjection, 0,
world.view().get_size(), 0,
pos, 0);
return new Vertex(pos[0], pos[1], pos[2]);
}
Vertex is a dataHolder with x,y,z floats
World extends GLSurfaceView, I do the GLWrapper replacement in the constructor:
world.mg is a MatrixGrabber()
public World( Context context )
{
super(context);
[...]
//allows matrix manipulation
setGLWrapper( new GLWrapper()
{
public GL wrap(GL gl)
{
return new MatrixTrackingGL(gl);
}
});
}
and I make sure that all the variables are instanciated when I do my call
but still I can't get this to work: the app crashes badly on the
world.mg.getCurrentState(world.gl);
call.
it also crashes it on getCurrentModelView(gl); and getCurrentProjection(gl);
I'm using Android 1.5, but tried with other versions up to 3. same thing.
I don't really know which version of OpenGLI'm using ; the GL10 is used everywhere, I don't know if it is important, all I've read concerned the GL10 "Type".
if anyone has a clue, an advice, a workaround or a solution, I'd be happy happy happy
and anyway, thanks for reading :)
private void getMatrix(GL10 gl, int mode, float[] mat) {
MatrixTrackingGL gl2 = new MatrixTrackingGL(gl);
gl2.glMatrixMode(mode);
gl2.getMatrix(mat, 0);
}
replace the casting on the first line of the MatrixGrabber.java
my World class extended GLSurfaceView and was using the MatrixTrackingGL as a wrapper.
the problem was that the Viewport also extended GLSurfaceView and was NOT using the MatrixTrackingGL... stupid.
now the world doesn't extend anything and the Viewport ( extending GLSurfaceView ) implements the Wrapper's change and everything's fine.

Android drawing cache

Please explain how does the drawing cache work in Android. I'm implementing a custom View subclass. I want my drawing to be cached by the system. In the View constructor, I call
setDrawingCacheEnabled(true);
Then in the draw(Canvas c), I do:
Bitmap cac = getDrawingCache();
if(cac != null)
{
c.drawBitmap(cac, 0, 0, new Paint());
return;
}
Yet the getDrawingCache() returns null to me. My draw() is not called neither from setDrawingCacheEnabled(), nor from getDrawingCache(). Please, what am I doing wrong?
There's a hard limit on drawing cache size, available via the ViewConfiguration class.. My view is larger than allowed for caching.
FYI, the sources of the View class are available via the SDK Manager for some (not all) Android versions.
Hopefully this explains it.
public class YourCustomView extends View {
private String mSomeProperty;
public YourCustomView(Context context) {
super(context);
}
public YourCustomView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public YourCustomView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public void setSomeProperty(String value) {
mSomeProperty = value;
setDrawingCacheEnabled(false); // clear the cache here
invalidate();
}
#Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// specific draw logic here
setDrawingCacheEnabled(true); // cache
}
#Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
...
}
}
Example code explained.
In the setSomeProperty() method call setDrawingCacheEnabled(false) to clear the cache and force a redraw by calling invalidate().
Call setDrawingCacheEnabled(true) in the onDraw method after drawing to the canvas.
Optionally place a log statement in the onDraw method to confirm it is only called once each time you call the setSomeProperty() method. Be sure to remove the log call once confirmed as this will become a performance issue.

Categories

Resources