SurfaceView游戏中Canvas调用DrawBitmap绘制缓慢问题求助
Hey there, let's break down why your scaled canvas is causing performance hits when drawing multiple background bitmaps, and walk through actionable fixes to get your 60 FPS target back on track. I've tackled similar SurfaceView bottlenecks before, so here's what to try:
1. Pre-Scale Bitmaps to Display Size (Skip Canvas Scaling)
The biggest issue here is scaling the canvas every frame—this forces the GPU to transform every pixel of each bitmap on the fly, which adds massive per-frame overhead. You mentioned trying pre-scaling, but let's make sure you're doing it at the right time and correctly:
Instead of scaling the canvas in draw(), resize your bitmaps once to match the actual view dimensions (use surfaceChanged() since that's where you get the final width/height):
@Override public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) { // Handle size changes here—surfaceCreated might not have the final view size scaleBackgroundBitmaps(width, height); } private void scaleBackgroundBitmaps(int targetWidth, int targetHeight) { // Clean up old bitmaps to avoid memory leaks for (Bitmap bg : bgsrc) { if (bg != null && !bg.isRecycled()) { bg.recycle(); } } // Load original background with optimized config (more on that next) BitmapFactory.Options options = new BitmapFactory.Options(); options.inPreferredConfig = Bitmap.Config.RGB_565; // No transparency = smaller memory footprint Bitmap originalBg = BitmapFactory.decodeResource(getResources(), R.drawable.mountain_background_v2, options); bgsrc = new Bitmap[5]; for (int i = 0; i < bgsrc.length; i++) { // Crop the original to get the 800x399 slice Bitmap croppedSlice = Bitmap.createBitmap(originalBg, i * 800, 0, 800, 399); // Scale directly to the target display size bgsrc[i] = Bitmap.createScaledBitmap(croppedSlice, targetWidth, targetHeight, true); // Recycle the temporary cropped slice to free memory croppedSlice.recycle(); } originalBg.recycle(); // Refresh your backgrounds list with scaled bitmaps backgrounds.clear(); for (int i = bgsrc.length - 1; i >= 0; i--) { if (i == 0 || i == 2) continue; backgrounds.add(new Background(bgsrc[i], -10 + i * 2)); } }
Then simplify your draw() method to skip canvas scaling entirely:
@Override public void draw(Canvas canvas){ super.draw(canvas); if (canvas != null) { for (Background background : backgrounds) { background.draw(canvas); } } }
2. Optimize Bitmap Configuration
If your backgrounds don't have transparency, switching to RGB_565 instead of the default ARGB_8888 cuts bitmap memory usage in half (2 bytes per pixel vs 4). This reduces memory pressure and makes drawing faster, as the GPU has fewer bytes to process. We already added this in the code above, but it's worth calling out as a standalone fix.
3. Tweak Your GameThread for Efficient Frame Throttling
Make sure your thread's loop is properly throttling to 60 FPS and doesn't hold the canvas lock longer than necessary. A well-optimized loop looks like this:
public class GameThread extends Thread { private SurfaceHolder surfaceHolder; private GameController gameController; private boolean running = false; private final long targetFrameDuration = 1000 / 60; // ~16ms per frame for 60 FPS public GameThread(SurfaceHolder holder, GameController controller) { surfaceHolder = holder; gameController = controller; } public void setRunning(boolean run) { running = run; } @Override public void run() { long frameStartTime; long frameElapsedTime; long sleepTime; while (running) { Canvas canvas = null; frameStartTime = System.currentTimeMillis(); try { // Lock the canvas only when needed canvas = surfaceHolder.lockCanvas(); synchronized (surfaceHolder) { gameController.update(); if (canvas != null) { gameController.draw(canvas); } } } finally { // Always unlock the canvas to avoid deadlocks if (canvas != null) { surfaceHolder.unlockCanvasAndPost(canvas); } } // Calculate sleep time to hit 60 FPS frameElapsedTime = System.currentTimeMillis() - frameStartTime; sleepTime = targetFrameDuration - frameElapsedTime; if (sleepTime > 0) { try { sleep(sleepTime); } catch (InterruptedException e) { e.printStackTrace(); } } } } }
4. Optimize the Background Class's Draw Logic
Make sure your Background class isn't doing expensive operations in its draw() method. Reuse objects like Paint instead of creating new ones every frame:
public class Background { private Bitmap bitmap; private float xPos; private Paint drawPaint; // Reuse this instance public Background(Bitmap bitmap, float startX) { this.bitmap = bitmap; this.xPos = startX; drawPaint = new Paint(); drawPaint.setFilterBitmap(false); // Disable filtering (safe since we pre-scaled) } public void update() { // Your position update logic here } public void draw(Canvas canvas) { canvas.drawBitmap(bitmap, xPos, 0, drawPaint); } }
Disabling setFilterBitmap(false) removes anti-aliasing during drawing, which speeds things up—since we pre-scaled the bitmaps to the exact display size, you won't lose any quality.
5. Verify Hardware Acceleration
SurfaceViews are hardware-accelerated by default, but double-check your AndroidManifest.xml to make sure you haven't disabled it globally:
<application ... android:hardwareAccelerated="true"> ... </application>
These changes should eliminate the per-frame scaling overhead, reduce memory usage, and get your game running at a smooth 60 FPS. The key is shifting as much work as possible to initialization time instead of doing it every frame.
内容的提问来源于stack exchange,提问作者kabir

