☰
SWT高级控件之SWT系统资源:Color、Font、Image 的创建、缓存与释放实战
2026/10/3 11:57:59 网站建设 项目流程

1. SWT 系统资源泄漏:界面越用越卡、内存只涨不降的根因

如果你写过一段时间的 SWT 桌面程序,大概率遇到过这种场景:窗口开了关、关了开,任务管理器里的内存曲线像爬楼梯一样只上不下;或者界面用久了越来越卡,拖动窗口都掉帧。很多人第一反应是「Java 有 GC,不用管内存」,但在 SWT 里这句话是错的。

SWT 和 Swing 最大的区别在于:Swing 是纯 Java 绘制,而 SWT 是薄封装,每个Color、Font、Image、Cursor背后都对应一个操作系统级的本地资源(Windows 上是 GDI 对象,Linux 上是 X11 资源)。JVM 的垃圾回收器只认识 Java 堆里的那个壳对象,它完全不知道操作系统那边还挂着一个句柄。壳对象被回收了,本地句柄依然占着,这就是句柄泄漏。

我见过一个真实案例:一个数据看板程序,每次刷新都new Color(...)给表格行上色,跑一上午之后 Windows 任务管理器里该进程的 GDI 对象数从几百涨到上万,界面直接卡死。原因就是那些 Color 从来没被dispose()。

所以这篇内容聚焦一件事:SWT 里 Color、Font、Image 这些系统资源,怎么创建、怎么缓存、怎么在正确的时机释放,以及怎么用 Sleak 工具验证资源真的回收了。适合正在做 SWT/RCP 桌面应用、被内存增长和界面卡顿困扰的开发者。核心检索词就是 SWT 系统资源管理,围绕 Color、Font、Image 的生命周期展开。

先记住两条铁律,后面所有代码都围绕它们:

通过new创建的资源,必须显式dispose()。通过display.getSystemColor()、display.getSystemFont()这类方法「获取」的资源,绝对不能 dispose,因为那是平台共享的,你释放了别人就崩了。

还有一条容易忽略的:释放父控件会隐式释放子控件(Widget 体系),但不会自动释放你挂在控件上的 Font、Color、Image。控件没了,你 new 出来的字体还在。这就是为什么很多人觉得「我窗口都 dispose 了怎么还泄漏」。

2. TaoToken 前置准备:给 SWT 资源排查配一个稳定的模型后端

在动手改代码之前,先说一个实际开发里绕不开的环节。SWT 资源泄漏的排查往往需要反复读日志、分析堆栈、让模型帮你解释 Sleak 的输出,这时候一个稳定的模型调用入口能省很多事。我平时用 TaoToken 来做这类辅助分析,它的 API 地址是 https://taotoken.net/api ,兼容常见的对话补全格式,接进自己的小工具或者 IDE 插件都方便。

如果你只是想先验证模型能不能用、回答质量如何,可以直接打开模型对话页面试几句,比如把 Sleak 的报错贴进去问「这个 Device 资源没释放是什么意思」。地址是 https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=model_chat 。

对于长期要写 SWT/RCP 代码、经常需要模型帮忙 review 资源释放逻辑的场景,可以考虑 Coding Plan,它更适合高频编码和 Agent 类用法: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=coding_plan 。

真正要落到代码里调用,需要先去控制台创建 API Key: https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys 。拿到 Key 之后,接入文档在 https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc ,里面有完整的请求示例。

这里要强调一点:TaoToken 是模型调用入口,不是用来替代你的 IDE 或 SWT 运行时的。它帮你分析代码、解释报错、生成释放模板,但 Color、Font 到底有没有 dispose,还得靠你自己的代码和 Sleak 工具来验证。别指望模型能替你跑程序。

如果你用的是 Claude Code 这类命令行编码工具,也可以把它接到同一个后端,配置方式参考 https://taotoken.net/claude-code-anthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=claude_code 。这样你在终端里让模型帮你改 SWT 代码时,用的就是同一套 Key 和模型。

前置准备就这些。核心还是回到 SWT 本身:资源怎么管。下面进入正题。

3. 可复制的资源创建与缓存封装:Color、Font、Image 三件套

这一节给可直接抄的代码。思路是:把资源集中到一个注册表里管理,创建时登记,销毁时统一释放。这样就不会出现「new 了一堆 Color 忘了 dispose」的情况。

先看一个资源注册表的骨架。核心是一个Map缓存加一个List记录所有自建资源:

import org.eclipse.swt.graphics.*; import org.eclipse.swt.widgets.Display; import java.util.*; public class ResourceRegistry { private final Display display; // 缓存:相同参数复用同一个资源,避免重复创建 private final Map<String, Color> colorCache = new HashMap<>(); private final Map<String, Font> fontCache = new HashMap<>(); private final Map<String, Image> imageCache = new HashMap<>(); // 记录所有自建资源,销毁时统一释放 private final List<Resource> owned = new ArrayList<>(); public ResourceRegistry(Display display) { this.display = display; } public Color color(int r, int g, int b) { String key = r + "," + g + "," + b; Color c = colorCache.get(key); if (c == null || c.isDisposed()) { c = new Color(display, r, g, b); colorCache.put(key, c); owned.add(c); } return c; } public Font font(String name, int height, int style) { String key = name + "|" + height + "|" + style; Font f = fontCache.get(key); if (f == null || f.isDisposed()) { f = new Font(display, name, height, style); fontCache.put(key, f); owned.add(f); } return f; } public Image image(String path) { Image img = imageCache.get(path); if (img == null || img.isDisposed()) { img = new Image(display, ResourceRegistry.class.getResourceAsStream(path)); imageCache.put(path, img); owned.add(img); } return img; } // 统一释放,在 Display dispose 之前调用 public void disposeAll() { for (Resource r : owned) { if (r != null && !r.isDisposed()) { r.dispose(); } } owned.clear(); colorCache.clear(); fontCache.clear(); imageCache.clear(); } }

这段代码的关键点有三个。第一,缓存 key 用参数拼,相同颜色只创建一个 Color 对象,避免每次刷新都 new。第二,owned 列表记录所有自建资源,销毁时遍历释放,不会漏。第三,每次取用前检查isDisposed(),防止拿到已经被释放的僵尸对象。

注意display.getSystemColor(SWT.COLOR_RED)这类系统资源不要放进这个注册表,因为它们不该被 dispose。系统颜色直接用就行:

Color sysRed = display.getSystemColor(SWT.COLOR_RED); // 不要 dispose

字体同理,display.getSystemFont()返回的系统字体也不要释放。

再看一个容易踩坑的地方:Image 的创建。如果你用new Image(display, inputStream),流要自己关;如果用new Image(display, filename),文件句柄由 SWT 管。推荐用类路径资源流的方式,配合上面的注册表:

Image icon = registry.image("/icons/eclipse48.gif");

然后在窗口关闭时统一释放。下面是一个完整的 Shell 生命周期模板:

public class MainWindow { private ResourceRegistry registry; public void open() { Display display = Display.getDefault(); registry = new ResourceRegistry(display); Shell shell = new Shell(display); // 用注册表拿资源,不要直接 new Color bg = registry.color(240, 240, 240); Font titleFont = registry.font("微软雅黑", 14, SWT.BOLD); Image logo = registry.image("/icons/logo.png"); shell.setBackground(bg); // ... 控件设置字体、画图等 shell.addDisposeListener(e -> { // 控件销毁时,先释放挂在控件上的资源 registry.disposeAll(); }); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } display.dispose(); } }

这里有个顺序问题要特别注意:registry.disposeAll()必须在display.dispose()之前执行。如果 Display 先没了,再去 dispose Color 会抛异常。放在shell.addDisposeListener里是安全的,因为 Shell 销毁发生在 Display 销毁之前。

如果你不想用注册表这种集中式管理,至少要做到「谁创建谁释放」,并且把释放写在widgetDisposed里:

shell.addDisposeListener(new DisposeListener() { public void widgetDisposed(DisposeEvent e) { if (font != null && !font.isDisposed()) font.dispose(); if (color != null && !color.isDisposed()) color.dispose(); if (image != null && !image.isDisposed()) image.dispose(); } });

两种方式都行,但集中式注册表在大型项目里更不容易漏。我试过在一个有几十个自定义控件的 RCP 项目里用注册表,资源泄漏问题基本绝迹。

4. 验证请求与成功结果:用 Sleak 对比资源是否随控件销毁而回收

代码写完了,怎么证明资源真的释放了?光看代码不够,得用工具验证。SWT 官方提供了一个叫 Sleak 的工具,专门用来监控 Device 上的资源创建和销毁。

Sleak 的原理是包装 Display 的 Device 数据,记录每一次资源创建,然后在界面上对比「已创建」和「已释放」的数量。如果两者相等,说明没有泄漏。

先看怎么把 Sleak 挂到你的程序里。Sleak 的核心类是org.eclipse.swt.tools.Sleak,它需要你传入一个Device和一个Shell。最简单的用法是在你的主窗口旁边开一个 Sleak 窗口:

import org.eclipse.swt.tools.Sleak; public class SleakDemo { public static void main(String[] args) { Display display = new Display(); Shell shell = new Shell(display); shell.setText("主窗口"); shell.setSize(400, 300); // 打开 Sleak 监控窗口 Sleak sleak = new Sleak(); sleak.open(); shell.open(); while (!shell.isDisposed()) { if (!display.readAndDispatch()) display.sleep(); } display.dispose(); } }

运行后你会看到两个窗口:主窗口和 Sleak 窗口。Sleak 窗口里有一个「Snap」按钮,点一下会抓取当前所有存活资源。操作步骤是这样的:

第一步,程序刚启动时点一次 Snap,记录基线。第二步,打开你的业务窗口,创建一堆 Color、Font、Image。第三步,关闭业务窗口,触发 dispose。第四步,再点一次 Snap,对比两次快照。

如果资源释放正确,第二次快照里那些 Color、Font、Image 应该都消失了,数量归零。如果还在,说明有泄漏,Sleak 会列出具体的资源类型和创建时的堆栈,直接定位到哪一行代码 new 的。

我实测下来,最常见的泄漏点有三个。一是new Color写在paintControl里,每次重绘都创建一个新的,从不释放。二是new Font写在循环里,给每一行都创建一个字体。三是 Image 在控件销毁时忘了 dispose,尤其是动态生成的缩略图。

用 Sleak 验证的时候,建议配合一个「压力测试」:反复开关窗口 50 次,然后看 Sleak 里的资源数是否稳定。如果每次开关都涨一点,那就是泄漏。稳定的表现是:开关 50 次后,资源数和开关 1 次后一样。

除了 Sleak,Windows 上还可以用任务管理器的「GDI 对象」列来辅助判断。正常程序 GDI 对象数应该在一个范围内波动,如果只涨不跌,基本可以确定有本地资源没释放。Linux 上可以用xrestop看 X11 资源。

这里给一个验证用的最小复现,你可以直接跑:

// 错误示范:每次重绘都 new Color,不释放 canvas.addPaintListener(e -> { Color c = new Color(e.display, 255, 0, 0); // 泄漏! e.gc.setForeground(c); e.gc.drawText("hello", 10, 10); // 忘了 c.dispose() }); // 正确示范:用注册表缓存 canvas.addPaintListener(e -> { Color c = registry.color(255, 0, 0); // 复用,不泄漏 e.gc.setForeground(c); e.gc.drawText("hello", 10, 10); });

跑起来反复触发重绘,用 Sleak 看 Color 数量。错误示范下数量会一直涨,正确示范下数量稳定在 1。这就是最直观的验证。

5. 本篇常见错误排查:Widget is disposed、OAuth 报错与资源释放顺序

这一节把实际开发中高频出现的报错和坑列出来,对照着排查。

报错一:org.eclipse.swt.SWTException: Widget is disposed

这个报错的意思是你在访问一个已经被释放的控件。常见于异步任务里更新 UI,比如后台线程算完数据后去label.setText(),但此时窗口已经关了。解决办法是用isDisposed()判断:

if (label != null && !label.isDisposed()) { label.setText("done"); }

注意,这个报错和资源释放是两回事。控件被 dispose 了,不代表你挂在它上面的 Font 被释放了。所以即使控件还在,也要检查资源。

报错二:org.eclipse.swt.SWTException: Device is disposed

这个通常发生在 Display 已经 dispose 之后,你还在操作资源。比如在display.dispose()之后调用color.dispose()。顺序必须是:先释放所有自建资源,再释放 Shell,最后释放 Display。前面给的注册表模板里,disposeAll()放在shell.addDisposeListener里就是为了保证这个顺序。

报错三:local proxy failed或连接类错误

如果你在 SWT 程序里集成了模型调用(比如用 TaoToken 的 API 做辅助分析),可能会遇到网络层的报错。这类错误和 SWT 资源无关,排查方向是检查 API 地址、Key 是否正确、网络是否可达。TaoToken 的 API 地址是 https://taotoken.net/api ,Key 在控制台创建。如果报 401,基本是 Key 没带对或者过期了,重新去 https://taotoken.net/console/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=api_keys 生成一个。

报错四:reading choices解析失败

这个一般出现在解析模型返回的 JSON 时,字段结构对不上。检查你用的模型和请求格式是否匹配,接入文档里有标准示例: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=doc 。

坑五:系统资源被误释放

这是最隐蔽的。有人图省事,把display.getSystemColor(SWT.COLOR_RED)拿到的 Color 也 dispose 了,结果程序跑一会儿就崩,因为其他控件还在用这个共享颜色。记住:getSystemColor、getSystemFont返回的资源是平台共享的,永远不要 dispose。只有new出来的才归你管。

坑六:Image 流没关

用new Image(display, inputStream)时,SWT 不会帮你关流。正确做法是用 try-with-resources 或者在创建后手动关:

try (InputStream is = getClass().getResourceAsStream("/icons/a.png")) { Image img = new Image(display, is); // 使用 img }

坑七:GC 对象没释放

new GC(drawable)创建的 GC 也是系统资源,用完必须 dispose。在paintControl里用的是事件自带的e.gc,那个不用你管;但如果你自己new GC(shell),就要自己释放。

排查顺序建议:先用 Sleak 定位是哪种资源泄漏,再看创建堆栈找到具体代码行,然后套用第 3 节的注册表模式改掉。改完再跑一遍 Sleak 验证。这个流程走下来,基本没有搞不定的泄漏。

6. 语义一致收尾:把资源管理变成肌肉记忆

写 SWT 代码,资源管理这件事没有捷径,就是养成习惯。我的做法是:项目里禁止直接new Color、new Font、new Image,全部走注册表。Code Review 的时候看到裸 new 就打回。这样坚持一段时间,团队里没人再犯资源泄漏的错。

如果你在排查过程中需要模型帮你读 Sleak 输出、解释堆栈,或者生成释放模板,可以用前面提到的入口。验证模型能力去模型对话,长期编码去 Coding Plan,创建 Key 去控制台,接入细节看文档。工具是辅助,核心还是你对dispose时机的理解。

最后留一个检查清单,贴在显示器边上:new 出来的资源 dispose 了吗?系统资源有没有被误 dispose?释放顺序对不对(资源 → Shell → Display)?Sleak 里资源数稳定吗?这四个问题都能答「是」,你的 SWT 程序就不会因为资源泄漏而卡顿或崩溃了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询