You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Android中Bitmap.createScaledBitmap导致UI卡顿(ANR)问题求助

问题描述

使用Bitmap.createScaledBitmap(...)将Bitmap适配屏幕尺寸后,绘制并移动SurfaceView时应用出现卡顿甚至ANR;缩小缩放尺寸时一切正常,但业务需求必须让Bitmap适配屏幕。该缩放方法仅在createBitmaps(...)方法中调用一次(标注为HERE IS THE PROBLEM的代码位置),drawTmxMaps(...)在GameThread的循环中执行。


Loader.java

public static Map<Integer, Bitmap[]> differentFramesMap = new HashMap<>();
public static Map<Integer, Canvas[]> differentCanvasesMap = new HashMap<>();

private static void init(List<Integer> animFrames, TileMapData t) {
    for (int i = 0; i < animFrames.size(); i++) {
        differentFramesMap.put(i, new Bitmap[animFrames.get(i)]);
        differentCanvasesMap.put(i, new Canvas[animFrames.get(i)]);
        for (int j = 0; j < animFrames.get(i); j++) {
            differentFramesMap.get(i)[j] = Bitmap.createBitmap(t.width * t.tilewidth, t.height * t.tileheight, Bitmap.Config.ARGB_8888);
            differentCanvasesMap.get(i)[j] = new Canvas(differentFramesMap.get(i)[j]);
        }
    }
}

public static void createBitmaps(TileMapData t, int startLayer, int endLayer, GameSurface gameSurface) {
    try {
        AssetManager assetManager = gameSurface.getContext().getAssets();
        Bitmap[] tilesets = new Bitmap[t.tilesets.size()];

        for (int i = 0; i < tilesets.length; i++) {
            tilesets[i] = BitmapFactory.decodeStream(assetManager.open(t.tilesets.get(i).ImageFilename));
        }

        long currentGID;
        Long localGID;
        Integer currentTileSetIndex;
        Rect source = new Rect(0, 0, 0, 0);
        Rect dest = new Rect(0, 0, 0, 0);

        for (int i = startLayer; i < endLayer; i++) {
            for (int j = 0; j < t.layers.get(i).height; j++) {
                for (int k = 0; k < t.layers.get(i).width; k++) {
                    currentGID = t.getGIDAt(k, j, i);
                    localGID = t.getLocalID(currentGID);
                    currentTileSetIndex = t.getTileSetIndex(currentGID);

                    if (!isAfterInit) {
                        animFrames = Stream.of(t.tilesets.get(currentTileSetIndex).tileAnimations)
                                .map(tileAnimation -> tileAnimation.getTileFrames().size()).collect(Collectors.toList());
                        init(animFrames, t, gameSurface);
                        isAfterInit = true;
                    }

                    if (localGID != null) {
                        if (i == startLayer) {
                            // 省略原有代码
                        } else {
                            for (int l = 0; l < t.tilesets.get(currentTileSetIndex).tileAnimations.size(); l++) {
                                if (localGID == t.tilesets.get(currentTileSetIndex).tileAnimations.get(l).getLocalGID()) {
                                    for (int frame = 0; frame < t.tilesets.get(currentTileSetIndex).tileAnimations.get(l).getTileFrames().size(); frame++) {
                                        source.top = (int) ((t.tilesets.get(currentTileSetIndex).tileAnimations.get(l).getTileFrames().get(frame).getTileId() / ((t.tilesets.get(currentTileSetIndex).imageWidth) / t.tilewidth)) * t.tileheight);
                                        source.left = (int) ((t.tilesets.get(currentTileSetIndex).tileAnimations.get(l).getTileFrames().get(frame).getTileId() % ((t.tilesets.get(currentTileSetIndex).imageWidth) / t.tilewidth)) * t.tilewidth);
                                        source.bottom = source.top + t.tileheight;
                                        source.right = source.left + t.tilewidth;

                                        dest.top = j * t.tileheight;
                                        dest.left = k * t.tilewidth;
                                        dest.bottom = dest.top + t.tileheight;
                                        dest.right = dest.left + t.tilewidth;

                                        differentCanvasesMap.get(l)[frame].drawBitmap(tilesets[currentTileSetIndex], source, dest, new Paint());
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }

        for (int i = 0; i < differentFramesMap.size(); i++) {
            for (int j = 0; j < animFrames.get(i); j++) {
                // HERE IS THE PROBLEM
                differentFramesMap.get(i)[j] = Bitmap.createScaledBitmap(differentFramesMap.get(i)[j], gameSurface.getCellWidth() * t.width, gameSurface.getCellHeight() * t.height, false);
            }
        }
    } catch (IOException e) {
        Log.e(TAG, e.getMessage());
    }
}

public static void drawTmxMaps(Canvas canvas) {
    canvas.drawBitmap(differentFramesMap.get(0)[framesFirstAnimIter], 0, 0, null);
    canvas.drawBitmap(differentFramesMap.get(1)[framesSecondAnimIter], 0, 0, null);
    canvas.drawBitmap(differentFramesMap.get(2)[framesThirdAnimIter], 0, 0, null);

    if (!isIterStarted) {
        iterTmxFrames();
        isIterStarted = true;
    }
}

private static void iterTmxFrames() {
    runnable = () -> {
        framesFirstAnimIter++;
        framesSecondAnimIter++;
        framesThirdAnimIter++;
        if (framesFirstAnimIter == 3)
            framesFirstAnimIter = 0;
        if (framesSecondAnimIter == 2)
            framesSecondAnimIter = 0;
        if (framesThirdAnimIter == 2)
            framesThirdAnimIter = 0;
        handler.postDelayed(runnable, 1000);
    };
    handler.postDelayed(runnable, 1000);
}

GameMainSurface.java

@Override
public boolean onTouchEvent(MotionEvent ev) {
    // 应用在此处停止响应
    if (ev.getAction() == MotionEvent.ACTION_UP) {
        setX(100);
    }
    return true;
}

GameThread.java

@Getter
@Setter
public class GameThread<T extends GameSurface> extends Thread {
    private final SurfaceHolder surfaceHolder;
    private boolean isRunning;
    private long startTime, loopTime;
    private long delay = 33;
    private T gameSurface;
    private boolean a = false;

    public GameThread(SurfaceHolder surfaceHolder, T gameSurface) {
        this.surfaceHolder = surfaceHolder;
        this.gameSurface = gameSurface;
        isRunning = true;
    }

    @Override
    public void run() {
        while (isRunning) {
            startTime = SystemClock.uptimeMillis();
            Canvas canvas = surfaceHolder.lockCanvas();
            if (canvas != null) {
                synchronized (surfaceHolder) {
                    TmxLoader.drawTmxMaps(canvas, gameSurface.getContext(), gameSurface);
                    surfaceHolder.unlockCanvasAndPost(canvas);
                }
            }

            loopTime = SystemClock.uptimeMillis() - startTime;
            if (loopTime < delay) {
                try {
                    Thread.sleep(delay - loopTime);
                } catch (InterruptedException e) {
                    Log.e("Interupted ex", e.getMessage());
                }
            }
        }
    }
}

解决方案

1. 移除预缩放,改用绘制时动态缩放

预缩放会生成尺寸巨大的Bitmap,占用大量内存且增加GPU绘制负担。直接在drawTmxMaps中通过canvas.drawBitmap的重载方法实现缩放,无需提前生成大Bitmap:

修改drawTmxMaps方法:

public static void drawTmxMaps(Canvas canvas, GameSurface gameSurface, TileMapData t) {
    // 计算目标缩放后的矩形区域
    Rect destRect = new Rect(0, 0, gameSurface.getCellWidth() * t.width, gameSurface.getCellHeight() * t.height);
    
    // 绘制时直接缩放
    canvas.drawBitmap(differentFramesMap.get(0)[framesFirstAnimIter], null, destRect, null);
    canvas.drawBitmap(differentFramesMap.get(1)[framesSecondAnimIter], null, destRect, null);
    canvas.drawBitmap(differentFramesMap.get(2)[framesThirdAnimIter], null, destRect, null);

    if (!isIterStarted) {
        iterTmxFrames();
        isIterStarted = true;
    }
}

同时删除createBitmaps中预缩放的代码块:

// 移除这段代码
/*
for (int i = 0; i < differentFramesMap.size(); i++) {
    for (int j = 0; j < animFrames.get(i); j++) {
        differentFramesMap.get(i)[j] = Bitmap.createScaledBitmap(differentFramesMap.get(i)[j], gameSurface.getCellWidth() * t.width, gameSurface.getCellHeight() * t.height, false);
    }
}
*/

2. 优化Bitmap内存占用

  • 将Bitmap配置从ARGB_8888改为RGB_565(如果不需要透明通道),内存占用直接减半:
    differentFramesMap.get(i)[j] = Bitmap.createBitmap(t.width * t.tilewidth, t.height * t.tileheight, Bitmap.Config.RGB_565);
    
  • Android 8.0及以上版本,使用Hardware Bitmap提升绘制性能:
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.RGB_565;
    options.inHardware = true; // 开启硬件Bitmap
    tilesets[i] = BitmapFactory.decodeStream(assetManager.open(t.tilesets.get(i).ImageFilename), null, options);
    

3. 异步执行Bitmap创建逻辑

createBitmaps方法如果在主线程执行,会阻塞UI导致卡顿。将其放到子线程中执行,加载完成后再启动GameThread:

// 在GameMainSurface的初始化中调用
new Thread(() -> {
    TmxLoader.createBitmaps(t, startLayer, endLayer, gameSurface);
    // 加载完成后通知主线程启动渲染线程
    runOnUiThread(() -> {
        gameThread.start();
    });
}).start();

4. 修复SurfaceView移动的线程同步问题

onTouchEvent中直接调用setX会触发UI线程和渲染线程的同步冲突,改为通过GameThread更新位置:

在GameThread中添加位置变量和更新方法:

private float surfaceX = 0;

public void setSurfaceX(float x) {
    synchronized (surfaceHolder) {
        this.surfaceX = x;
    }
}

修改drawTmxMaps中的绘制逻辑,应用偏移:

canvas.translate(surfaceX, 0);
// 然后绘制Bitmap
canvas.drawBitmap(..., null, destRect, null);
canvas.translate(-surfaceX, 0); // 恢复画布矩阵

修改GameMainSurface的onTouchEvent:

@Override
public boolean onTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_UP) {
        gameThread.setSurfaceX(100);
    }
    return true;
}

内容的提问来源于stack exchange,提问作者user21172734

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 21:22:05