sinek
sinek

Reputation: 2488

Android canvas.draw leaks graphic memory

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).

Graphics

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: profiler

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): enter image description here

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.

Upvotes: 0

Views: 1634

Answers (3)

Miguel Sesma
Miguel Sesma

Reputation: 747

This is happening to me in compose canvas too. Using drawPath with a style Stroke and a stroke width > 1 there are memory leaks. With stroke width == 1 It works fine.

In my case the workaround has been using drawPoints with pointMode = PointMode.Polygon instead drawPath. It allows me the desired stroke width with no leaks.

Upvotes: 1

Eddy Gorbunov
Eddy Gorbunov

Reputation: 21

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.

Upvotes: 2

Perraco
Perraco

Reputation: 17340

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.

enter image description here

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.

Upvotes: 2

Related Questions