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
相关产品推荐
相关产品推荐

