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

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.28 14:47:14