如何在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(); } }
问题根源
- 判断逻辑错误:
imagesPanel.isDisplayable()仅判断整个图片面板是否可显示,而非单个图片组件是否进入JScrollPane的视口可见区域,导致所有加载线程直接启动。 - 动态添加组件引发频繁重绘:每次加载完成后调用
imagesPanel.add()并触发revalidate(),会导致布局频繁计算,加剧卡顿。 - 无并发控制:100个加载线程同时启动,占用大量网络和CPU资源。
解决方案
核心思路
- 先给JPanel添加所有图片的固定尺寸占位符,让JScrollPane能正确计算滚动范围。
- 监听JScrollPane视口的滚动事件,每次滚动时检测哪些占位符进入视口,仅对未加载的图片触发加载。
- 维护图片加载状态,避免重复加载;用线程池控制并发加载数量。
- 加载完成后直接更新对应占位符内容,而非动态添加组件。
修改后的代码实现
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
相关产品推荐
相关产品推荐

