【问题标题】:Concurrency error when working with arraylist of sprites使用精灵数组列表时出现并发错误
【发布时间】:2011-10-20 18:39:34
【问题描述】:

目前,我正在研究游戏开发,并创建了(在一些在线教程的帮助下)以下示例游戏。目标是触摸屏幕上的精灵并杀死它们(即它们从屏幕上移除)。截图如下

当我杀死其中几个时,我得到以下异常

E/AndroidRuntime(  277): FATAL EXCEPTION: Thread-8
E/AndroidRuntime(  277): java.util.ConcurrentModificationException
E/AndroidRuntime(  277):        at java.util.ArrayList$ArrayListIterator.next(ArrayList.java:573)
E/AndroidRuntime(  277):        at cz.trada.gd101.GameView.draw(GameView.java:65)
E/AndroidRuntime(  277):        at cz.trada.gd101.GameLoopThread.run(GameLoopThread.java:32)

源代码如下所示。不幸的是,我不知道如何在 SO 上突出显示确切的代码行,所以我在它们前面放了以下评论://ERROR COMING 因此,您可以轻松找到这些行。

请帮助我了解并发错误的原因并找到解决方案。

附:游戏中使用的图片资源my_sprite_girlmy_sprite_boy附在文末。

Main.java

package cz.trada.gd101;
import android.app.Activity;
import android.os.Bundle;
import android.view.Window;

public class Main extends Activity {
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        requestWindowFeature(Window.FEATURE_NO_TITLE);
        setContentView(new GameView(this));
    }
}

GameView.java

package cz.trada.gd101;
import java.util.ArrayList;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Canvas;
import android.graphics.Color;
import android.util.Log;
import android.view.MotionEvent;
import android.view.SurfaceHolder;
import android.view.SurfaceHolder.Callback;
import android.view.SurfaceView;

public class GameView extends SurfaceView {
    private static final String TAG = "GameView";
    SurfaceHolder holder;   

    GameLoopThread gameLoopThread;
    List<Sprite> sprites = new ArrayList<Sprite>();

    long lastClick;

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

        gameLoopThread = new GameLoopThread(this);
        holder = getHolder();
        holder.addCallback(new Callback() {

            @Override
            public void surfaceDestroyed(SurfaceHolder holder) {
                boolean retry = true;
                gameLoopThread.setRunning(false);
                while (retry) {
                    try {
                        gameLoopThread.join();
                        retry = false;
                    } catch (InterruptedException e) {
                        Log.d(TAG, e.getMessage());
                    }
                }   
            }

            @Override
            public void surfaceCreated(SurfaceHolder holder) {
                createSprites();
                gameLoopThread.setRunning(true);
                gameLoopThread.start();             
            }

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

    }

    @Override
    public void draw(Canvas canvas) {
        canvas.drawColor(Color.BLACK);
        for (Sprite sprite : sprites) {
//ERROR COMING
                sprite.draw(canvas);
        }
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
          if (System.currentTimeMillis() - lastClick > 500) {
                 lastClick = System.currentTimeMillis();
                 float x = event.getX();
                 float y = event.getY();
                 synchronized (getHolder()) {
                     for (int i = sprites.size() - 1; i >= 0; i--) {
                         Sprite sprite = sprites.get(i);
                         if (sprite.isCollision(x, y)) {
                             sprites.remove(sprite);
                             break;
                         }
                     }
                 }
          }
          return true;
    }

    private void createSprites() {
        for (int i = 0; i < 10; i++) {
            sprites.add(createSprite(R.drawable.my_sprite_girl));
            sprites.add(createSprite(R.drawable.my_sprite_boy));
        }
    }

    private Sprite createSprite(int resource) {
        Bitmap bmp = BitmapFactory.decodeResource(getResources(), resource);
        return new Sprite(this, bmp);
    }
}

GameLoopThread.java

package cz.trada.gd101;
import android.graphics.Canvas;
import android.util.Log;

public class GameLoopThread extends Thread {
    private static final String TAG = "GameLoopThread";
    private static final int FPS = 10;

    private GameView view;
    private boolean running = false;

    public GameLoopThread(GameView view) {
        this.view = view;       
    }

    public void setRunning(boolean run) {
        running = run;
    }

    @Override
    public void run() {
        long ticksPS = 1000 / FPS;
        long startTime;
        long sleepTime;
        while (running) {
            Canvas c = null;
            startTime = System.currentTimeMillis();                 
            try {
                c = view.getHolder().lockCanvas();
                synchronized (view.getHandler()) {
//ERROR COMING
                    view.draw(c);                   
                }
            } 
            finally {
                if (c != null) {
                    view.getHolder().unlockCanvasAndPost(c);
                }
            }

            sleepTime = ticksPS - (System.currentTimeMillis() - startTime);
            try {
                if (sleepTime > 0)
                    sleep(sleepTime);
                else
                    sleep(10);
            } catch (Exception e) {
                Log.d(TAG, e.getMessage());
            }
        }
    }
}

资源

【问题讨论】:

    标签: android concurrency


    【解决方案1】:

    您的错误可能来自在onTouchEvent() 中对remove() 的调用,而draw() 中的循环正在遍历精灵。

    如果你的Sprite 类已经有equals()hashCode()(或者你添加它们),你可以使用ConcurrentSkipListSet 代替它给你无锁containsremoveadd 操作在日志(n)中。

    CopyOnWriteArrayList 可能也能解决问题,但性能不是很好(由于写时复制部分)。

    作为样式注释,您还可以在onTouchEvent() 循环中使用Iterator 及其remove() 方法:

    Iterator<Sprite> it = sprites.iterator();
    while (it.hasNext()) {
        Sprite sprite = sprites.next();
        if (sprite.isCollision(x, y)) {
            it.remove();
            break;
            }
    }
    

    【讨论】:

    • ConcurrentSkipListSet 在 API 9 及更高版本中可用,那么在较低版本的情况下该怎么办?我最新的想法只是设置一个 flag 表示在触摸事件方法中被点击的精灵已经死了,而不是在 GameView 中的 draw 方法中我遍历精灵来决定是否要调用它的 draw 方法(sprite.isDead() == false) 或将其从精灵列表中删除(当 sprite.isDead() == true 时)。但是,我不确定这是不是正确的想法,因为感觉就像我在 draw 方法中添加了一些逻辑,这些逻辑应该只用于绘制对象。不知道该怎么办。
    • 如果您需要支持 API 级别 volatile,以便跨线程可见更改。您也可以尝试将 ConcurrentSkipListSet 移植到您的 API 级别,基本的 java.util.concurrent.atomic 包从 API 级别 1 开始就存在。
    • 我刚刚检查过,您可以通过复制 ConcurrentSkipListSetConcurrentSkipListMapConcurrentNavigableMapNavigableMapNavigableSet 和 @ 来获得 API 级别 7 上 ConcurrentSkipListSet 的干净编译987654348@ 并删除 ConcurrentSkipListSet 中的 clone() 方法 --- 但我认为您的脏标志解决方法比这更好:)
    猜你喜欢
    • 2017-02-22
    • 2018-09-20
    • 1970-01-01
    • 2017-11-21
    • 2022-01-14
    • 2017-08-26
    • 1970-01-01
    • 2013-11-29
    • 2013-08-27
    相关资源
    最近更新 更多