Android Surfaceview Threads and memory leaks

2020-05-24 05:11发布

问题:

Im creating a game in android, and i noticed that the game has a memory leak. Iv managed to isolate the memory leak into a smaller application so that i can see well try and work out, how to fix it.

The application uses a surfaceview for its view and has a thread attached to that in order to do all the drawing to the screen. The memory leak happens when i start a new activity and close the one that im currently using. I can see this when i do a memory dump on my test application as all it does is open and close an activity (activity a -> activity b -> activity a). Iv kind of ran out of ideas as to how i can fix this as iv tried nulling all my references that i do create to the view (inside the thread), iv tried removing the callback from the surfaceview when i destroy the view, and also inside the activity, it doesn't seem to make any difference.

MemoryLeakActivity.java

package memory.leak;

import memory.leak.view.MemoryLeak;
import android.app.Activity;
import android.os.Bundle;

public class MemoryLeakActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new MemoryLeak(this));
    }
}

MemoryLeakViewThread.java

package memory.leak.thread;

import memory.leak.view.MemoryLeak;
import android.view.SurfaceHolder;
import android.graphics.Canvas;


public class MemoryLeakViewThread extends Thread {
    private MemoryLeak view;
    private boolean run =false;

    public MemoryLeakViewThread(MemoryLeak view) {
        this.view =view;
    }

    public void setRunning(boolean run) {
        this.run =run;
    }

    @Override
    public void run() {
        Canvas canvas =null;
        SurfaceHolder holder =this.view.getHolder();
        while(this.run) {
            canvas =holder.lockCanvas();
            if(canvas !=null) {
                this.view.onDraw(canvas);
                holder.unlockCanvasAndPost(canvas);
            }
        }
        holder =null;
        this.view =null;
    }
}

MemoryLeak.java

package memory.leak.view;

import memory.leak.TestActivity;
import memory.leak.thread.MemoryLeakViewThread;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.GestureDetector.OnGestureListener;


public class MemoryLeak extends SurfaceView implements SurfaceHolder.Callback, OnGestureListener {
    private GestureDetector gesture;
    private MemoryLeakViewThread vThread;
    private Context context;

    public MemoryLeak(Context context) {
        super(context);

        this.getHolder().addCallback(this);
        this.vThread =new MemoryLeakViewThread(this);

        this.gesture =new GestureDetector(this);
        this.context =context;
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

    public void surfaceCreated(SurfaceHolder holder) {
        if(!this.vThread.isAlive()) {
            this.vThread =new MemoryLeakViewThread(this);
            this.vThread.setRunning(true);
            this.vThread.start();
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry = true;

        if(this.vThread.isAlive()) {
            this.vThread.setRunning(false);
            while(retry) {
                try {
                    this.vThread.join();
                    retry =false;
                } catch(Exception ee) {}
            }
        }

        this.vThread =null;
        this.context =null;
    }

     public boolean onTouchEvent(MotionEvent event) {
         return this.gesture.onTouchEvent(event);
     }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    }

    @Override
    public void onDraw(Canvas canvas) {
        canvas.drawColor(Color.WHITE);
    }   

    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {}

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {}

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        Intent helpScreenIntent =new Intent(this.context, TestActivity.class);
        this.context.startActivity(helpScreenIntent);

        if (this.context instanceof Activity)
            ((Activity) this.context).finish();

        return true;
    }
}

TestActivity.java

package memory.leak;

import memory.leak.view.Test;
import android.app.Activity;
import android.os.Bundle;

public class TestActivity extends Activity {
    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(new Test(this));
    }
}

TestViewThread.java

package memory.leak.thread;

import memory.leak.view.Test;
import android.view.SurfaceHolder;
import android.graphics.Canvas;


public class TestViewThread extends Thread {
    private Test panel;
    private boolean run =false;

    public TestViewThread(Test panel) {
        this.panel =panel;
    }

    public void setRunning(boolean run) {
        this.run =run;
    }

    @Override
    public void run() {
        Canvas canvas =null;
        SurfaceHolder holder =this.panel.getHolder();
        while(this.run) {
            canvas =holder.lockCanvas();
            if(canvas !=null) {
                this.panel.onDraw(canvas);
                holder.unlockCanvasAndPost(canvas);
            }
        }
        holder =null;
        this.panel =null;
    }
}

Test.java

package memory.leak.view;

import memory.leak.MemoryLeakActivity;
import memory.leak.thread.TestViewThread;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.graphics.Canvas;
import android.graphics.Color;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.GestureDetector.OnGestureListener;


public class Test extends SurfaceView implements SurfaceHolder.Callback, OnGestureListener {
    private GestureDetector gesture;
    private TestViewThread vThread;
    private Context context;

    public Test(Context context) {
        super(context);

        this.getHolder().addCallback(this);
        this.vThread =new TestViewThread(this);

        this.gesture =new GestureDetector(this);
        this.context =context;
    }

    public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {}

    public void surfaceCreated(SurfaceHolder holder) {
        if(!this.vThread.isAlive()) {
            this.vThread =new TestViewThread(this);
            this.vThread.setRunning(true);
            this.vThread.start();
        }
    }

    public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry = true;

        if(this.vThread.isAlive()) {
            this.vThread.setRunning(false);
            while(retry) {
                try {
                    this.vThread.join();
                    retry =false;
                } catch(Exception ee) {}
            }
        }

        this.vThread =null;
        this.context =null;
    }

     public boolean onTouchEvent(MotionEvent event) {
         return this.gesture.onTouchEvent(event);
     }

    @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
    }

    @Override
    public void onDraw(Canvas canvas) {
        canvas.drawColor(Color.RED);
    }   

    @Override
    public boolean onDown(MotionEvent e) {
        return true;
    }

    @Override
    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
        return false;
    }

    @Override
    public void onLongPress(MotionEvent e) {}

    @Override
    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
        return false;
    }

    @Override
    public void onShowPress(MotionEvent e) {}

    @Override
    public boolean onSingleTapUp(MotionEvent e) {
        Intent helpScreenIntent =new Intent(this.context, MemoryLeakActivity.class);
        this.context.startActivity(helpScreenIntent);

        if (this.context instanceof Activity)
            ((Activity) this.context).finish();

        return true;
    }
}

--Edit-- I made changes to the view class to its surfaceDestroyed(SurfaceHolder holder) so that it will set the view that the thread has to null when the thread is told to stop. The changes i made are

public void surfaceDestroyed(SurfaceHolder holder) {
        boolean retry = true;

        if(this.vThread.isAlive()) {
            this.vThread.setRunning(false);
            while(retry) {
                try {
                    this.vThread.join();
                    retry =false;
                } catch(Exception ee) {}
            }
            this.vThread.setRunning(false, null);
        }

        this.vThread =null;
        this.context =null;
        this.gesture =null;
    }

you also need to change the surfaceCreated(SurfaceHolder holder) method to

public void surfaceCreated(SurfaceHolder holder) {
        if(!this.vThread.isAlive()) {
            this.vThread =new MemoryLeakViewThread();
            this.vThread.setRunning(true, this);
            this.vThread.start();
        }
    }

then on the thread class we need to change the following

public MemoryLeakViewThread() {
    }

    public void setRunning(boolean run) {
        this.run =run;
    }

    public void setRunning(boolean run, MemoryLeak view) {
        this.run =run;
        this.view =view;
    }

By doing this it seemed to of fixed the problem, the only problem now is the thread seems to stay in memory, due to the thread class and thread group. But im thinking this might be due to the debugger.

回答1:

You should not create new Thread in the constructor when you are creating it in onSurfaceCreated. Compare your code to my example: How can I use the animation framework inside the canvas?



回答2:

As you can see here:

http://developer.android.com/resources/articles/avoiding-memory-leaks.html

The easiest way to start a memory leak in Android is to pass a view's constructor the whole activity instead of the application context. Have you try to change this line:

setContentView(new MemoryLeak(this));

into this one:

setContentView(new MemoryLeak(Context.getApplicationContext()));

?

Hope it helps.