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

如何解决JMenu加载大量字体项时的UI阻塞问题

解决Swing字体菜单加载时UI阻塞的问题

问题背景

开发的Swing应用中,Font菜单会加载系统所有可用字体,每个字体对应一个JMenuItem且显示对应字体。使用SwingWorker在后台加载并publish菜单项,但用户在加载期间悬停菜单时UI会阻塞,Windows设备首次加载尤为明显,移除item.setFont(...)后卡顿消失。

核心原因

  1. SwingWorker的publish方法会合并多次调用,导致UI线程一次性处理大量JMenuItem,触发批量布局和渲染操作,阻塞EDT(事件调度线程)。
  2. 系统字体的首次初始化(new Font(...))本身耗时,尤其是Windows平台,大量字体的初始化操作集中推送到UI线程后,进一步加剧阻塞。

有效解决方案

1. 分批次推送菜单项,避免一次性处理全部

在后台线程中积累一定数量的JMenuItem后再publish,减少UI线程单次处理的项数,给EDT留出响应其他事件的时间。

修改doInBackground方法:

@Override
protected Void doInBackground() {
    List<JMenuItem> batch = new ArrayList<>(10); // 每10个项为一批
    for (String fontName : fonts) {
        JMenuItem item = new JMenuItem(fontName);
        try {
            Font f = new Font(fontName, Font.PLAIN, 14);
            item.setFont(f);
            item.addActionListener(new Listener(f, area));
            batch.add(item);
            
            // 达到批次上限时推送
            if (batch.size() >= 10) {
                publish(batch);
                batch.clear();
                // 可选:给后台线程短暂休眠,降低EDT压力
                Thread.sleep(10);
            }
        } catch (Exception e) {
            // 跳过无法加载的字体
            continue;
        }
    }
    // 推送剩余的项
    if (!batch.isEmpty()) {
        publish(batch);
    }
    return null;
}

2. 拆分Process方法的处理逻辑

即使分批次publish,若单批次仍有较多项,可在process方法中进一步拆分处理,让EDT每次只处理少量项,剩余项延迟到下一个EDT周期处理:

@Override
protected void process(List<JMenuItem> chunks) {
    Iterator<JMenuItem> iter = chunks.iterator();
    int processed = 0;
    // 每次只处理5个项
    while (iter.hasNext() && processed < 5) {
        font.add(iter.next());
        processed++;
    }
    // 剩余项延迟处理
    List<JMenuItem> remaining = new ArrayList<>();
    iter.forEachRemaining(remaining::add);
    if (!remaining.isEmpty()) {
        SwingUtilities.invokeLater(() -> process(remaining));
    }
}

3. 延迟加载:仅在菜单首次展开时触发加载

将字体加载逻辑从程序启动时推迟到用户首次打开Font菜单时,避免启动时的后台开销,同时给用户明确的加载提示:

修改Font菜单的定义:

JMenu font = new JMenu("Font") {
    private boolean isLoaded = false;

    @Override
    public void menuSelected(MenuEvent e) {
        super.menuSelected(e);
        if (!isLoaded) {
            isLoaded = true;
            // 添加加载提示项
            JMenuItem loadingItem = new JMenuItem("Loading fonts...");
            add(loadingItem);
            
            new SwingWorker<Void, JMenuItem>() {
                @Override
                protected Void doInBackground() {
                    List<JMenuItem> batch = new ArrayList<>(10);
                    for (String fontName : fonts) {
                        try {
                            JMenuItem item = new JMenuItem(fontName);
                            Font f = new Font(fontName, Font.PLAIN, 14);
                            item.setFont(f);
                            item.addActionListener(new Listener(f, area));
                            batch.add(item);
                            
                            if (batch.size() >= 10) {
                                publish(batch);
                                batch.clear();
                                Thread.sleep(10);
                            }
                        } catch (Exception ex) {
                            continue;
                        }
                    }
                    if (!batch.isEmpty()) {
                        publish(batch);
                    }
                    return null;
                }

                @Override
                protected void process(List<JMenuItem> chunks) {
                    // 移除加载提示(仅第一次处理时)
                    if (getItemCount() > 0 && getItem(0).getText().equals("Loading fonts...")) {
                        remove(0);
                    }
                    // 分批次添加项
                    Iterator<JMenuItem> iter = chunks.iterator();
                    int processed = 0;
                    while (iter.hasNext() && processed < 5) {
                        add(iter.next());
                        processed++;
                    }
                    List<JMenuItem> remaining = new ArrayList<>();
                    iter.forEachRemaining(remaining::add);
                    if (!remaining.isEmpty()) {
                        SwingUtilities.invokeLater(() -> process(remaining));
                    }
                    // 更新菜单布局
                    revalidate();
                    repaint();
                }
            }.execute();
        }
    }
};

4. 预加载字体缓存(可选)

在程序启动时的后台线程中提前初始化所有Font对象,后续创建JMenuItem时直接使用已初始化的Font,减少UI线程的字体初始化开销:

// 类级别的静态缓存
private static final Map<String, Font> FONT_CACHE = new HashMap<>();

static {
    // 在类加载时后台预加载字体缓存
    new Thread(() -> {
        String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment().getAvailableFontFamilyNames();
        for (String fontName : fonts) {
            try {
                FONT_CACHE.put(fontName, new Font(fontName, Font.PLAIN, 14));
            } catch (Exception e) {
                // 跳过无效字体
            }
        }
    }).start();
}

之后在创建JMenuItem时直接从缓存取:

Font f = FONT_CACHE.get(fontName);
if (f != null) {
    item.setFont(f);
}

总结

通过分批次推送和处理菜单项、延迟加载触发时机、预加载字体缓存这几种方式组合,可以有效缓解UI阻塞问题,让字体菜单加载过程更平滑,提升用户体验。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 16:05:03