Issue
I have a background animation drawn onto a SurfaceView by another thread in my app. The animation seems to work well except when the screen is rotated. Then the main thread will sometimes hang for a couple of seconds. Using DDMS I see that the main thread is calling Object.wait(), I don't understand where or why it's doing that though.
Below is some abbreviated code, if needed the full source can be found on github at https://github.com/GavinDBrown/Amazing.
Main Activity:
public class StartMenu extends Activity {
/** A handle to the thread that's running the Game Of Life animation. */
private GOLThread mGOLThread;
/** A handle to the View in which the background is running. */
private GOLView mGOLView;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.game_of_life_background);
startGOLBackground();
}
private void startGOLBackground() {
// get handles to the GOLView and it's GOLThread
mGOLView = (GOLView) findViewById(R.id.game_of_life_background);
mGOLThread = new GOLThread(mGOLView.getHolder());
mGOLView.setThread(mGOLThread);
mGOLThread.start();
}
private void stopGOLBackground() {
if (mGOLThread != null) {
mGOLThread.halt(); // stop the animation if it's valid
boolean retry = true;
while (retry) {
try {
mGOLThread.join();
retry = false;
} catch (InterruptedException e) {
}
}
mGOLThread = null;
mGOLView = null;
}
}
@Override
public void onResume() {
super.onResume();
mGOLThread = mGOLView.getThread();
mGOLThread.unpause();
}
@Override
public void onPause() {
super.onPause();
mGOLThread.pause();
}
@Override
protected void onDestroy() {
super.onDestroy();
stopGOLBackground();
}
}
The SurfaceView:
public class GOLView extends SurfaceView implements SurfaceHolder.Callback {
/** The thread that actually draws the animation */
private GOLThread thread;
SurfaceHolder surfaceHolder;
public GOLView(Context context, AttributeSet attrs) {
super(context, attrs);
SurfaceHolder holder = getHolder();
holder.addCallback(this);
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
if (hasWindowFocus){
thread.unpause();
} else {
thread.pause();
}
}
public void surfaceChanged(SurfaceHolder holder, int format, int width,
int height) {
thread.setSurfaceSize(width, height);
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
thread.pause();
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
thread.unpause();
}
}
And finally the Thread:
public class GOLThread extends Thread {
private GameOfLife gameOfLife;
private final Object GOLLock = new Object();
private int mCanvasHeight;
private int mCanvasWidth;
private SurfaceHolder mSurfaceHolder;
public GOLThread(SurfaceHolder surfaceHolder) {
mSurfaceHolder = surfaceHolder;
}
@Override
public void start() {
synchronized (mSurfaceHolder) {
stopped = false;
mSurfaceHolder.notify();
}
super.start();
}
public void halt() {
synchronized (mSurfaceHolder) {
paused = true;
stopped = true;
mSurfaceHolder.notify();
}
}
public void pause() {
synchronized (mSurfaceHolder) {
paused = true;
}
}
public void unpause() {
synchronized (mSurfaceHolder) {
paused = false;
mSurfaceHolder.notify();
}
}
public Bundle saveState(Bundle outState) {
synchronized (GOLLock) {
if (outState != null) {
outState.putParcelable(GAME_OF_LIFE_ID, gameOfLife);
}
}
return outState;
}
public synchronized void restoreState(Bundle savedState) {
synchronized (GOLLock) {
gameOfLife = (GameOfLife) savedState.getParcelable(GAME_OF_LIFE_ID);
}
}
@Override
public void run() {
while (!stopped) {
while (paused && !stopped) {
try {
synchronized (mSurfaceHolder) {
mSurfaceHolder.wait(100L);
}
} catch (InterruptedException ignore) {
}
}
// Check if thread was stopped while it was paused.
if (stopped)
break;
beforeTime = System.nanoTime();
Canvas c = null;
try {
c = mSurfaceHolder.lockCanvas();
synchronized (GOLLock) {
if (gameOfLife != null) {
gameOfLife.drawAndUpdate(c);
} else {
pause();
}
}
} finally {
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
sleepTime = FRAME_DELAY
- ((System.nanoTime() - beforeTime) / 1000000L);
try {
// actual sleep code
if (sleepTime > 0 && !stopped && !paused) {
synchronized (mSurfaceHolder) {
mSurfaceHolder.wait(sleepTime);
}
}
} catch (InterruptedException ex) {
}
}
}
public void setSurfaceSize(int width, int height) {
synchronized (GOLLock) {
if (mCanvasWidth != width || mCanvasHeight != height) {
mCanvasWidth = width;
mCanvasHeight = height;
// reset the GOL
if (mCanvasWidth > 0 && mCanvasHeight > 0) {
gameOfLife = new GameOfLife();
gameOfLife.init(mCanvasWidth, mCanvasHeight);
}
}
}
}
}
Solution
The problem is GOLThread is calling SurfaceHolder.lockCanvas() and getting null as a result too often.
From the docs on SurfaceHolder.lockCanvas()
If you call this repeatedly when the Surface is not ready (before Callback.surfaceCreated or after Callback.surfaceDestroyed), your calls will be throttled to a slow rate in order to avoid consuming CPU.
So the OS was throttling calls by putting my threads to sleep.
I fixed it by updating the code in GOLThread.run() to have
Canvas c = null;
try {
c = mSurfaceHolder.lockCanvas();
if (c == null) {
// Pause here so that our calls do not get throttled by the
// OS for calling lockCanvas too often.
pause();
} else {
synchronized (GOLLock) {
if (gameOfLife != null) {
gameOfLife.drawAndUpdate(c);
} else {
pause();
}
}
}
} finally {
// do this in a finally so that if an exception is thrown
// during the above, we don't leave the Surface in an
// inconsistent state
if (c != null) {
mSurfaceHolder.unlockCanvasAndPost(c);
}
}
Answered By - GDanger
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.