Приложение для Android зависает при завершении действия, runOnUiThread() перестает работать

#android #multithreading #user-interface #android-activity

#Android #многопоточность #пользовательский интерфейс #android-активность

Вопрос:

Я нахожусь в процессе написания своего первого приложения для Android, клона asteroids, и у меня есть одна последняя ошибка, которую я не могу понять. Игровая активность иногда зависает, когда игра заканчивается, и пытается перейти к игре над действием. У игрового действия есть несколько представлений пользовательского интерфейса, которые перестают работать до того, как оно зависнет. Есть кнопка для приостановки игры, которая перестает отвечать на ввод. Существует также текстовое представление для партитуры, которое я обновляю с помощью runOnUiThread() (поскольку я запускаю игру в отдельном потоке), которое перестает обновляться.

Есть несколько вещей, которые меня озадачивают. Во-первых, я не вижу никаких сообщений об ошибках. Ошибка возникает только примерно в 1 случае из каждых 5 запусков игрового действия, и это происходит в случайные моменты во время его выполнения. Это происходит, когда игровая активность начинается примерно в половине случаев, но, по-видимому, может произойти в любой момент, хотя, как правило, это происходит раньше, чем позже, даже если это происходит не в самом начале. Кроме того, поток игры и пользовательский вид игры, который я создал, продолжают нормально обновляться при возникновении ошибки. Наконец, у меня есть два виртуальных вида джойстика, которые я создал для управления движением и стрельбой игрока, которые также постоянно обновляются и отлично реагируют на сенсорный ввод. Эти последние два действительно смущают меня, потому что сначала я думал, что основной поток выполняет слишком много работы или застревает в цикле или что-то в этом роде, но если я правильно понимаю (и согласно некоторым тестам, которые я провел), рисование и сенсорный ввод происходят в основном потоке, и, насколько я могу судитьвсе виды отрисовываются нормально, а сенсорный ввод работает для джойстиков, но не для кнопки паузы. Кроме того, если основной поток не отвечал, разве я не получу всплывающее окно, не отвечающее на запросы приложения? Однако этого никогда не происходит.

Я попытался удалить все экземпляры runOnUiThread(), просто чтобы посмотреть, исправило ли это без какого-либо эффекта, кроме нарушения текстового представления результатов и оставления приложения застрявшим в игре, даже если ошибка не возникает. Я попытался напечатать имена потоков, возвращаемых Thread.currentThread() в игровом цикле, в методе onDraw() игрового представления, в методе onTouch () JoystickView и при обновлении score TextView в игровом действии, чтобы убедиться, что я действительно запускаю 2 отдельных потока ивсе выполняется в тех потоках, в которых они должны быть (игровой поток для игрового цикла, основной поток для всего остального), и это проверено. Я пытался сократить создание новых объектов во время игрового цикла, не уверен, что это даст, кроме повышения производительности, если честно, но я подумал, что это не повредит, также безрезультатно.

Все это затрудняет мне понимание того, какой код публиковать. Вот несколько предположений:

Методы onCreate, goToGameOverActivity и updateScoreTextView из моей игровой активности:

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
        setVolumeControlStream(AudioManager.STREAM_MUSIC);
        setContentView(R.layout.activity_play);
        gameView = findViewById(R.id.gameView);
        game = new Game(this, gameView);
        float joystickRadius = 150;
        CartesianPoint2D motionJoystickCenter = new CartesianPoint2D(
                10   joystickRadius,
                Constants.getScreenInformation().getHeight() - 10 - joystickRadius);
        motionJoyStickView = new MotionJoystickView(this, joystickRadius, motionJoystickCenter, game);
        CartesianPoint2D shootJoystickCenter = new CartesianPoint2D(
                Constants.getScreenInformation().getWidth() - 10 - joystickRadius,
                Constants.getScreenInformation().getHeight() - 10 - joystickRadius);
        shootJoystickView = new ShootJoystickView(this, joystickRadius, shootJoystickCenter, game);
        scoreTextView = findViewById(R.id.playActivityScoreTextView);
        pauseButton = findViewById(R.id.playActivityPauseButton);
        soundEffects = new SoundEffects(this);
        game.start();
    }


    public void goToGameOverActivity() {
        game.stop();
        Intent intent = new Intent(this, GameOverActivity.class);
        startActivity(intent);
        finish();
    }

    public void updateScoreTextView(int score) {
        scoreTextView.setText(String.valueOf(score));
        scoreTextView.invalidate();
    }
 

The Runnables I am using to update the score TextView and signal that the game is over using runOnUIThread():

 package com.group18.spacerocks.play.view;

import com.group18.spacerocks.play.PlayActivity;

public class UpdateScoreTextView implements Runnable {

    protected int newScore;
    protected PlayActivity playActivity;

    public UpdateScoreTextView(int newScore, PlayActivity playActivity) {
        System.out.println("Setting ScoreTextView to "   newScore   ".");
        this.newScore = newScore;
        this.playActivity = playActivity;
    }

    @Override
    public void run() {
        playActivity.updateScoreTextView(newScore);
    }
}

package com.group18.spacerocks.play;

public class GoToGameOverActivity implements Runnable {

    protected PlayActivity playActivity;

    public GoToGameOverActivity(PlayActivity playActivity) {
        this.playActivity = playActivity;
    }

    @Override
    public void run() {
        playActivity.goToGameOverActivity();
    }
}
 

The run() method from the GameThread class extending thread which updates the game:

 public void run() {
        super.run();
        while (running) {
            while (paused) {
                currentTime = System.nanoTime();
                lastUpdateTime = currentTime;
                try {
                    sleep(1);
                }
                catch (InterruptedException e) {
                    e.printStackTrace()
                }
            }
            currentTime = System.nanoTime();
            timeSinceLastUpdate = currentTime - lastUpdateTime;
            if (timeSinceLastUpdate >= TARGET_UPDATE_LENGTH) {
                currentUpdateLength = currentTime - lastUpdateTime;
                game.update(currentUpdateLength);
                gameView.invalidate();
                lastUpdateTime = currentTime;
                UPS  ;
                timeSinceLastUpdate = 0;
                if (currentTime - lastSecondTime >= 1000000000) {
                    System.out.println("UPS: "   UPS);
                    UPS = 0;
                    lastSecondTime = currentTime;
                }
            }
        }
    }
 

И, наконец, код для моего класса JoystickView, который имеет 2 дочерних класса для движения и съемки:

 package com.group18.spacerocks.play.joystick;

import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;

import com.group18.spacerocks.R;
import com.group18.spacerocks.play.CartesianPoint2D;
import com.group18.spacerocks.play.PlayActivity;
import com.group18.spacerocks.play.Game;

public class JoystickView extends View implements View.OnTouchListener {

    protected boolean inUse;
    protected float radius;
    protected float buttonRadius;
    protected CartesianPoint2D center;
    protected CartesianPoint2D buttonOffset;
    protected PlayActivity playActivity;
    protected Game game;
    protected Paint outlinePaint;
    protected Paint buttonPaint;

    public JoystickView(PlayActivity playActivity, float radius, CartesianPoint2D center, Game game) {
        super(playActivity);
        inUse = false;
        this.radius = radius;
        buttonRadius = radius / 2;
        this.center = center;
        this.buttonOffset = CartesianPoint2D.getInstance();
        this.playActivity = playActivity;
        this.game = game;
        setLayoutParams(new ViewGroup.LayoutParams((int) radius * 2, (int) radius * 2));
        setX(center.getX() - radius);
        setY(center.getY() - radius);
        outlinePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        outlinePaint.setAlpha(100);
        outlinePaint.setStyle(Paint.Style.STROKE);
        outlinePaint.setColor(Color.WHITE);
        outlinePaint.setStrokeWidth(1);
        buttonPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        buttonPaint.setAlpha(100);
        buttonPaint.setStyle(Paint.Style.STROKE);
        buttonPaint.setColor(playActivity.getResources().getColor(R.color.textTitleColor));
        buttonPaint.setStrokeWidth(10);
        setOnTouchListener(this);
        ViewGroup viewGroup = playActivity.findViewById(R.id.playActivityLayout);
        viewGroup.addView(this);
    }

    public void onDraw(Canvas canvas) {
        canvas.drawCircle(radius, radius, radius, outlinePaint);
        canvas.drawCircle(radius   buttonOffset.getX(), radius   buttonOffset.getY(), buttonRadius, buttonPaint);
    }

    public boolean onTouch(View view, MotionEvent motionEvent) {
        if (motionEvent.getAction() == MotionEvent.ACTION_UP) {
            inUse = false;
            buttonOffset.setX(0);
            buttonOffset.setY(0);
        }
        else {
            inUse = true;
            CartesianPoint2D motionPosition = new CartesianPoint2D(motionEvent.getX(), motionEvent.getY());
            CartesianPoint2D centerOffset = new CartesianPoint2D(motionPosition.getX() - radius, motionPosition.getY() - radius);
            if (centerOffset.getMagnitude() > radius) {
                buttonOffset = CartesianPoint2D.createFromMagnitudeAndDirection(radius, centerOffset.getDirection());
            } else {
                buttonOffset = centerOffset;
            }
        }
        invalidate();
        return true;
    }
}
 

Любая информация приветствуется, я пытаюсь исправить это уже несколько дней, и я в растерянности.

Ответ №1:

До сих пор точно не знаю, в чем причина, но я исправил это, сделав просмотр игры расширением SurfaceView, чтобы я мог рисовать в потоке, отличном от основного потока.