JavaFX使用Task<>后台加载地图瓦片致程序卡顿问题
问题描述
我正在JavaFX中实现Stepan Rutz的MapPanel组件,目标是实现和原组件一样的效果:地图瓦片未加载完成时用户仍可拖动地图。目前我用Task<>在后台按需执行瓦片绘制任务,但遇到一个问题:每次需要加载新的未绘制瓦片时,程序都会创建新的工作线程去请求瓦片。我需要限制任务创建数量,同时保证地图滑动流畅。
以下是瓦片创建相关代码:
1. 遍历所需瓦片的循环
public void paintMap(Point mapPosition, Point scalePosition) { double width = this.getWidth(); double height = this.getHeight(); int x0 = (int) Math.floor((this.mapPosition.getX()) / TILE_SIZE); int y0 = (int) Math.floor((this.mapPosition.getY()) / TILE_SIZE); int x1 = (int) Math.ceil((mapPosition.getX() + width) / TILE_SIZE); int y1 = (int) Math.ceil((mapPosition.getY() + height) / TILE_SIZE); int dy = y0 * TILE_SIZE - (int) this.mapPosition.getY(); for(int y = y0; y < y1; ++y) { int dx = x0 * TILE_SIZE - (int) this.mapPosition.getX(); for(int x = x0; x < x1; ++x) { paintTile(dx, dy, x, y); dx += TILE_SIZE; } dy += TILE_SIZE; } }
2. 绘制瓦片的函数
private void paintTile(int dx, int dy, int x, int y) { int xTileCount = 1 << zoom; int yTileCount = 1 << zoom; boolean tileInBounds = x >= 0 && x < xTileCount && y >= 0 && y < yTileCount; if(tileInBounds) { TileCache cache = getCache(); Image image = cache.get(x, y, zoom); if(image == null) { String tileURL = getTileString(x,y, zoom); System.out.println(tileURL); invokeTileTaskController(this, tileURL, cache, x, y, dx, dy); } if(image != null) { ImageView iv = new ImageView(image); iv.setX(dx); iv.setY(dy); this.getChildren().add(iv); iv = null; } } }
3. 创建任务的函数
private void invokeTileTaskController(MapPanel mp, String tileURL, TileCache cache, int x, int y, int dx, int dy) { TileTaskController ttc = new TileTaskController(tileURL); Thread ttcThread = new Thread(ttc); ttcThread.start(); ttc.addEventHandler(WorkerStateEvent.WORKER_STATE_SUCCEEDED, new EventHandler<WorkerStateEvent>() { @Override public void handle(WorkerStateEvent t) { Image tileImage = ttc.getValue(); if(tileImage!=null) { cache.put(x, y, zoom, tileImage); ImageView iv = new ImageView(tileImage); iv.setX(dx); iv.setY(dy); mp.getChildren().add(iv); } } }); }
4. Task类
public class TileTaskController extends Task<Image> { private Image tileImage; private final String tileURL; public TileTaskController(String tileURL) { this.tileImage = new Image(getClass().getResourceAsStream("image-error-icon-7.jpg")); this.tileURL = tileURL; } @Override protected Image call() throws Exception { Image tileImage = new Image(this.tileURL); updateValue(tileImage); return new Image(this.tileURL); } }
解决方案
要解决线程过多的问题,核心是用线程池控制并发请求数,同时避免重复请求同一瓦片,还可优化视口内瓦片的加载优先级。
1. 用线程池管理后台任务
JavaFX推荐用ExecutorService统一管理后台线程,避免频繁创建销毁线程带来的开销。可以根据网络请求并发量设置固定大小的线程池(比如4-8个线程,视服务器并发限制调整):
// 在MapPanel类中定义全局线程池 private final ExecutorService tileExecutor = Executors.newFixedThreadPool(4);
修改任务提交逻辑,不再手动创建线程:
private void invokeTileTaskController(MapPanel mp, String tileURL, TileCache cache, int x, int y, int dx, int dy) { TileTaskController ttc = new TileTaskController(tileURL); // 提交任务到线程池 tileExecutor.submit(ttc); ttc.addEventHandler(WorkerStateEvent.WORKER_STATE_SUCCEEDED, t -> { Image tileImage = ttc.getValue(); if(tileImage != null) { cache.put(x, y, zoom, tileImage); // 必须在JavaFX应用线程执行UI操作 Platform.runLater(() -> { ImageView iv = new ImageView(tileImage); iv.setX(dx); iv.setY(dy); mp.getChildren().add(iv); }); } }); }
2. 避免重复请求同一瓦片
用户拖动地图时,同一瓦片可能被多次触发加载请求,需要在缓存中标记“正在加载”状态:
- 给
TileCache添加两个方法:markLoading(String tileKey)和finishLoading(String tileKey),用Set<String>记录正在加载的瓦片键(键格式如x-y-zoom) - 修改
paintTile逻辑,跳过正在加载的瓦片:
private void paintTile(int dx, int dy, int x, int y) { int xTileCount = 1 << zoom; int yTileCount = 1 << zoom; boolean tileInBounds = x >= 0 && x < xTileCount && y >= 0 && y < yTileCount; if(tileInBounds) { TileCache cache = getCache(); String tileKey = String.format("%d-%d-%d", x, y, zoom); Image image = cache.get(x, y, zoom); if(image == null) { // 检查是否正在加载,避免重复请求 if(!cache.isLoading(tileKey)) { cache.markLoading(tileKey); String tileURL = getTileString(x,y, zoom); invokeTileTaskController(this, tileURL, cache, x, y, dx, dy); } } else { Platform.runLater(() -> { ImageView iv = new ImageView(image); iv.setX(dx); iv.setY(dy); this.getChildren().add(iv); }); } } }
- 在任务完成后,记得调用
cache.finishLoading(tileKey)移除标记。
3. 取消无效任务(可选优化)
用户快速拖动地图时,很多正在加载的瓦片会移出当前视口,可取消这些任务节省资源:
- 修改
TileTaskController,添加视口检查逻辑:
public class TileTaskController extends Task<Image> { private final String tileURL; private final String tileKey; private final MapPanel mapPanel; private final int x, y, zoom; public TileTaskController(String tileURL, String tileKey, MapPanel mapPanel, int x, int y, int zoom) { this.tileURL = tileURL; this.tileKey = tileKey; this.mapPanel = mapPanel; this.x = x; this.y = y; this.zoom = zoom; } @Override protected Image call() throws Exception { // 检查任务是否被取消,或瓦片是否已移出视口 if(isCancelled() || !isTileInViewport()) { return null; } Image tileImage = new Image(this.tileURL); // 再次检查,避免加载过程中视口变化 if(isCancelled() || !isTileInViewport()) { return null; } return tileImage; } private boolean isTileInViewport() { double mapX = mapPanel.mapPosition.getX(); double mapY = mapPanel.mapPosition.getY(); double width = mapPanel.getWidth(); double height = mapPanel.getHeight(); int x0 = (int) Math.floor(mapX / MapPanel.TILE_SIZE); int y0 = (int) Math.floor(mapY / MapPanel.TILE_SIZE); int x1 = (int) Math.ceil((mapX + width) / MapPanel.TILE_SIZE); int y1 = (int) Math.ceil((mapY + height) / MapPanel.TILE_SIZE); return x >= x0 && x < x1 && y >= y0 && y < y1; } }
- 维护正在运行的任务列表,用户拖动地图时,遍历列表取消不在当前视口的任务。
4. 清理资源
在MapPanel销毁时关闭线程池,避免资源泄漏:
@Override protected void finalize() throws Throwable { super.finalize(); tileExecutor.shutdown(); }
或者在组件的关闭事件中调用tileExecutor.shutdown()。
内容的提问来源于stack exchange,提问作者sushiwithoutsushi
相关产品推荐
相关产品推荐

