如何解决JMenu加载大量字体项时的UI阻塞问题
解决Swing字体菜单加载时UI阻塞的问题
问题背景
开发的Swing应用中,Font菜单会加载系统所有可用字体,每个字体对应一个JMenuItem且显示对应字体。使用SwingWorker在后台加载并publish菜单项,但用户在加载期间悬停菜单时UI会阻塞,Windows设备首次加载尤为明显,移除item.setFont(...)后卡顿消失。
核心原因
- SwingWorker的
publish方法会合并多次调用,导致UI线程一次性处理大量JMenuItem,触发批量布局和渲染操作,阻塞EDT(事件调度线程)。 - 系统字体的首次初始化(
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
相关产品推荐
相关产品推荐

