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

如何在Java Swing中实现图片懒加载?解决批量加载卡顿问题

JScrollPane中图片按需懒加载实现方案

问题描述

需要将多张网络图片加载到JPanel,结合JScrollPane实现按需懒加载(而非一次性全部加载),当前通过String数组存储图片URL,循环调用自定义ImageLoader加载,但使用imagesPanel.isDisplayable()判断可见性完全无效——所有图片仍会同时加载,加载100张10-50KB图片时应用明显卡顿(20张时无问题)。

当前核心代码如下:

加载方法代码

private void loadOnlineImages(String[] images, int maxWidth, int maxHeight) {
    imagesPanel.removeAll();
    imagesPanel.setLayout(new WrapLayout(FlowLayout.LEFT));
    footerPanel.setTotalItems(images.length);

    for (String image : images) {
        ImageLoader onlineImageLoader =
                new ImageLoader(imagesPanel, footerPanel, image, maxWidth, maxHeight);
        onlineImageLoader.loadImage();
    }

    imagesPanel.revalidate();
    imagesPanel.repaint();
}

自定义ImageLoader类

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URI;
import java.util.Iterator;

public class ImageLoader {
    ProgressListener progressListener;
    JPanel footerPanel;
    JPanel imagesPanel;
    String imageUrl;
    int maxWidth;
    int maxHeight;

    public ImageLoader(JPanel imagesPanel, JPanel footerPanel, String imageUrl, int maxWidth, int maxHeight) {
        this.imagesPanel = imagesPanel;
        this.imageUrl = imageUrl;
        this.maxWidth = maxWidth;
        this.maxHeight = maxHeight;
        this.footerPanel = footerPanel;
        progressListener = new ProgressListener(footerPanel);
    }

    // Load the image only when it becomes visible
    public void loadImage() {
        new Thread(() -> {
            try {
                URI uri = URI.create(imageUrl);
                ImageInputStream imageInputStream = ImageIO.createImageInputStream(uri.toURL().openStream());
                Iterator<ImageReader> iterator = ImageIO.getImageReaders(imageInputStream);
                if (iterator.hasNext()) {
                    ImageReader reader = iterator.next();
                    reader.setInput(imageInputStream);
                    reader.addIIOReadProgressListener(progressListener);
                    BufferedImage image = reader.read(reader.getMinIndex());
                    final ImageIcon icon = new ImageIcon(image);

                    // Check if the image is still required to be shown
                    if (imagesPanel.isDisplayable()) {
                        SwingUtilities.invokeLater(() -> {
                            JLabel imageLabel = new JLabel(IconScaler.createScaledIcon(icon, maxWidth, maxHeight));
                            imagesPanel.add(imageLabel);
                            imagesPanel.revalidate();
                            imagesPanel.repaint();
                        });
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }).start();
    }
}

问题根源

  1. 判断逻辑错误:imagesPanel.isDisplayable()仅判断整个图片面板是否可显示,而非单个图片组件是否进入JScrollPane的视口可见区域,导致所有加载线程直接启动。
  2. 动态添加组件引发频繁重绘:每次加载完成后调用imagesPanel.add()并触发revalidate(),会导致布局频繁计算,加剧卡顿。
  3. 无并发控制:100个加载线程同时启动,占用大量网络和CPU资源。

解决方案

核心思路

  1. 先给JPanel添加所有图片的固定尺寸占位符,让JScrollPane能正确计算滚动范围。
  2. 监听JScrollPane视口的滚动事件,每次滚动时检测哪些占位符进入视口,仅对未加载的图片触发加载。
  3. 维护图片加载状态,避免重复加载;用线程池控制并发加载数量。
  4. 加载完成后直接更新对应占位符内容,而非动态添加组件。

修改后的代码实现

1. 定义图片状态类及初始化方法

import java.awt.Dimension;
import java.awt.Rectangle;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.swing.JScrollPane;
import javax.swing.JLabel;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;

// 存储图片加载状态的内部类
private static class ImageItem {
    String url;
    JLabel label;
    boolean loaded;

    ImageItem(String url, JLabel label) {
        this.url = url;
        this.label = label;
        this.loaded = false;
    }
}

private List<ImageItem> imageItems = new ArrayList<>();
// 限制并发加载线程数,避免资源耗尽
private ExecutorService imageLoaderPool = Executors.newFixedThreadPool(5);

private void loadOnlineImages(String[] images, int maxWidth, int maxHeight) {
    imagesPanel.removeAll();
    imagesPanel.setLayout(new WrapLayout(FlowLayout.LEFT));
    footerPanel.setTotalItems(images.length);
    imageItems.clear();

    // 先添加所有占位符组件
    for (String imageUrl : images) {
        JLabel placeholder = new JLabel("加载中...");
        // 设置占位符尺寸与最终图片一致,避免布局跳动
        placeholder.setPreferredSize(new Dimension(maxWidth, maxHeight));
        imagesPanel.add(placeholder);
        imageItems.add(new ImageItem(imageUrl, placeholder));
    }

    imagesPanel.revalidate();
    imagesPanel.repaint();

    // 获取JScrollPane并添加视口滚动监听(建议直接持有scrollPane引用,避免链式获取出错)
    JScrollPane scrollPane = (JScrollPane) imagesPanel.getParent().getParent();
    scrollPane.getViewport().addChangeListener(new ChangeListener() {
        @Override
        public void stateChanged(ChangeEvent e) {
            loadVisibleImages(maxWidth, maxHeight);
        }
    });

    // 初始加载当前视口内的图片
    loadVisibleImages(maxWidth, maxHeight);
}

2. 实现视口可见图片加载逻辑

private void loadVisibleImages(int maxWidth, int maxHeight) {
    Rectangle viewportBounds = ((JScrollPane) imagesPanel.getParent().getParent()).getViewport().getViewRect();

    for (ImageItem item : imageItems) {
        // 仅加载未加载且进入视口的图片
        if (!item.loaded && viewportBounds.intersects(item.label.getBounds())) {
            item.loaded = true;
            // 提交到线程池执行加载
            imageLoaderPool.submit(() -> {
                new ImageLoader(item.label, footerPanel, item.url, maxWidth, maxHeight).loadImage();
            });
        }
    }
}

3. 修改ImageLoader类(绑定到具体占位符)

import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.net.URI;
import java.util.Iterator;

public class ImageLoader {
    private final ProgressListener progressListener;
    private final JPanel footerPanel;
    private final JLabel targetLabel;
    private final String imageUrl;
    private final int maxWidth;
    private final int maxHeight;

    public ImageLoader(JLabel targetLabel, JPanel footerPanel, String imageUrl, int maxWidth, int maxHeight) {
        this.targetLabel = targetLabel;
        this.imageUrl = imageUrl;
        this.maxWidth = maxWidth;
        this.maxHeight = maxHeight;
        this.footerPanel = footerPanel;
        this.progressListener = new ProgressListener(footerPanel);
    }

    public void loadImage() {
        try {
            URI uri = URI.create(imageUrl);
            ImageInputStream imageInputStream = ImageIO.createImageInputStream(uri.toURL().openStream());
            Iterator<ImageReader> iterator = ImageIO.getImageReaders(imageInputStream);
            if (iterator.hasNext()) {
                ImageReader reader = iterator.next();
                reader.setInput(imageInputStream);
                reader.addIIOReadProgressListener(progressListener);
                BufferedImage image = reader.read(reader.getMinIndex());
                final ImageIcon icon = new ImageIcon(image);

                SwingUtilities.invokeLater(() -> {
                    // 替换占位符为缩放后的图片
                    targetLabel.setIcon(IconScaler.createScaledIcon(icon, maxWidth, maxHeight));
                    targetLabel.setText(null); // 清除占位文本
                    targetLabel.revalidate();
                    targetLabel.repaint();
                });
            }
        } catch (IOException e) {
            e.printStackTrace();
            SwingUtilities.invokeLater(() -> {
                targetLabel.setText("加载失败");
            });
        }
    }
}

关键注意事项

  • JScrollPane引用优化:建议直接在类中持有JScrollPane的成员变量,而非通过getParent()链式获取,避免UI结构变化导致空指针。
  • 占位符优化:可以用空白图片作为占位符,提升视觉体验。
  • 线程池配置:根据网络环境调整线程池大小,网络好可设为8,网络差设为3。
  • 资源释放:界面关闭时调用imageLoaderPool.shutdown()关闭线程池,避免资源泄漏。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.15 10:04:58