第17章:SWT 性能优化
作者:步子哥 (steper@foxmail.com)
17.1 避免不必要的重绘
redraw() 的滥用
// 错误示例:频繁调用 redraw()
for (int i = 0; i < 1000; i++) {
text.setText("文本 " + i);
text.redraw(); // 每次都重绘!性能差
}
// 正确示例:减少 redraw() 调用
for (int i = 0; i < 1000; i++) {
text.setText("文本 " + i);
}
text.redraw(); // 最后统一重绘一次
费曼解释:redraw() 的性能影响
redraw() = 重新粉刷墙壁
- 频繁粉刷:刷了1000次(性能差)
- 粉刷一次:只刷1次(性能好)
类比:
redraw() = 反复修改作业
- 每写一个字就擦一次(性能差)
- 写完后再擦(性能好)
批量更新
// 错误示例:逐个更新
for (int i = 0; i < 1000; i++) {
TableItem item = new TableItem(table, SWT.NONE);
item.setText(new String[]{"行 " + i});
// 每次添加都会重绘!性能差
}
// 正确示例:批量更新
TableItem[] items = new TableItem[1000];
for (int i = 0; i < 1000; i++) {
items[i] = new TableItem(table, SWT.NONE);
items[i].setText(new String[]{"行 " + i});
}
table.setRedraw(false); // 暂停重绘
for (int i = 0; i < 1000; i++) {
items[i].setText(new String[]{"行 " + i, "内容 " + i});
}
table.setRedraw(true); // 恢复重绘,统一重绘一次
费曼解释:setRedraw(false) 的作用
setRedraw(false) = 暂停粉刷
- 你要刷1000次墙
- 先暂停粉刷(setRedraw(false))
- 刷完1000次,再恢复粉刷(setRedraw(true))
- 只重绘一次(性能好)
类比:
setRedraw(false) = 暂停修改作业
- 你要修改1000处作业
- 先暂停自动保存(setRedraw(false))
- 改完1000处,再恢复自动保存(setRedraw(true))
- 只保存一次(性能好)
代码示例:避免不必要的重绘
public class RedrawOptimizationExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("重绘优化");
shell.setLayout(new GridLayout(1, false));
// 表格
Table table = new Table(shell, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
table.setHeaderVisible(true);
table.setLinesVisible(true);
GridData tableData = new GridData();
tableData.horizontalAlignment = GridData.FILL;
tableData.verticalAlignment = GridData.FILL;
tableData.grabExcessHorizontalSpace = true;
tableData.grabExcessVerticalSpace = true;
tableData.heightHint = 200;
table.setLayoutData(tableData);
// 创建列
TableColumn column1 = new TableColumn(table, SWT.NONE);
column1.setText("列 1");
column1.setWidth(200);
TableColumn column2 = new TableColumn(table, SWT.NONE);
column2.setText("列 2");
column2.setWidth(200);
// 按钮面板
Composite buttonPanel = new Composite(shell, SWT.NONE);
buttonPanel.setLayout(new FillLayout(SWT.HORIZONTAL));
// 错误示例:频繁 redraw
Button wrongButton = new Button(buttonPanel, SWT.PUSH);
wrongButton.setText("频繁 redraw(错误)");
wrongButton.addListener(SWT.Selection, event -> {
long startTime = System.currentTimeMillis();
table.removeAll();
for (int i = 0; i < 100; i++) {
TableItem item = new TableItem(table, SWT.NONE);
item.setText(new String[]{"行 " + i, "内容 " + i});
table.redraw(); // 错误!频繁 redraw
}
long endTime = System.currentTimeMillis();
System.out.println("频繁 redraw 耗时:" + (endTime - startTime) + "ms");
});
// 正确示例:批量更新
Button correctButton = new Button(buttonPanel, SWT.PUSH);
correctButton.setText("批量更新(正确)");
correctButton.addListener(SWT.Selection, event -> {
long startTime = System.currentTimeMillis();
table.setRedraw(false); // 暂停重绘
table.removeAll();
for (int i = 0; i < 100; i++) {
TableItem item = new TableItem(table, SWT.NONE);
item.setText(new String[]{"行 " + i, "内容 " + i});
}
table.setRedraw(true); // 恢复重绘,统一重绘
long endTime = System.currentTimeMillis();
System.out.println("批量更新 耗时:" + (endTime - startTime) + "ms");
});
shell.setBounds(100, 100, 500, 300);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
17.2 虚拟控件的使用
VirtualTable、VirtualTree
// 错误示例:创建所有 TableItem
Table table = new Table(shell, SWT.BORDER);
for (int i = 0; i < 10000; i++) {
TableItem item = new TableItem(table, SWT.NONE);
item.setText(new String[]{"行 " + i});
}
// 10000 个 TableItem!内存占用大,性能差
// 正确示例:使用 VirtualTable
Table virtualTable = new Table(shell, SWT.BORDER | SWT.VIRTUAL);
virtualTable.setItemCount(10000); // 只设置行数
virtualTable.addListener(SWT.SetData, event -> {
TableItem item = (TableItem) event.item;
int index = virtualTable.indexOf(item);
// 只创建需要的项
item.setText(new String[]{"行 " + index});
});
费曼解释:虚拟控件的原理
虚拟控件 = 电子书
- 普通表格:一次性加载所有页(10000 页)→ 内存占用大
- 虚拟表格:只加载当前页(10 页)→ 内存占用小
类比:
虚拟控件 = 电视节目
- 普通表格:全部录到硬盘(10000 集)→ 硬盘占用大
- 虚拟表格:只录当前集(1 集)→ 硬盘占用小
延迟加载
// 使用 VirtualTable + 延迟加载
Table virtualTable = new Table(shell, SWT.BORDER | SWT.VIRTUAL);
virtualTable.setItemCount(10000);
virtualTable.addListener(SWT.SetData, event -> {
TableItem item = (TableItem) event.item;
int index = virtualTable.indexOf(item);
// 延迟加载数据(从数据库、文件等)
String[] data = loadData(index);
item.setText(data);
});
// 模拟从数据库加载数据
private String[] loadData(int index) {
// 模拟延迟
try {
Thread.sleep(1); // 模拟数据库查询延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
return new String[]{"行 " + index, "内容 " + index, "时间 " + System.currentTimeMillis()};
}
费曼解释:延迟加载的原理
延迟加载 = 按需订餐
- 普通表格:提前订好10000份菜(内存占用大)
- 虚拟表格:只订当前桌的菜(内存占用小)
- 用户翻到哪桌,就订哪桌的菜(按需加载)
类比:
延迟加载 = 按需点播
- 普通表格:下载所有视频(硬盘占用大)
- 虚拟表格:只下载当前视频(硬盘占用小)
- 用户看到哪一集,就下载哪一集(按需加载)
代码示例:虚拟控件
public class VirtualControlExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("虚拟控件示例");
shell.setLayout(new GridLayout(1, false));
// 普通表格
Group normalGroup = new Group(shell, SWT.NONE);
normalGroup.setText("普通表格(创建 1000 行)");
normalGroup.setLayout(new GridLayout(1, false));
normalGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
Table normalTable = new Table(normalGroup, SWT.BORDER | SWT.MULTI | SWT.FULL_SELECTION);
normalTable.setHeaderVisible(true);
normalTable.setLinesVisible(true);
normalTable.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true, 1, 1));
TableColumn normalColumn1 = new TableColumn(normalTable, SWT.NONE);
normalColumn1.setText("列 1");
normalColumn1.setWidth(200);
TableColumn normalColumn2 = new TableColumn(normalTable, SWT.NONE);
normalColumn2.setText("列 2");
normalColumn2.setWidth(200);
// 虚拟表格
Group virtualGroup = new Group(shell, SWT.NONE);
virtualGroup.setText("虚拟表格(10000 行)");
virtualGroup.setLayout(new GridLayout(1, false));
virtualGroup.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true, 1, 1));
Table virtualTable = new Table(virtualGroup, SWT.BORDER | SWT.VIRTUAL | SWT.MULTI | SWT.FULL_SELECTION);
virtualTable.setHeaderVisible(true);
virtualTable.setLinesVisible(true);
virtualTable.setItemCount(10000); // 只设置行数
virtualTable.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true, 1, 1));
TableColumn virtualColumn1 = new TableColumn(virtualTable, SWT.NONE);
virtualColumn1.setText("列 1");
virtualColumn1.setWidth(200);
TableColumn virtualColumn2 = new TableColumn(virtualTable, SWT.NONE);
virtualColumn2.setText("列 2");
virtualColumn2.setWidth(200);
// 虚拟表格的 SetData 监听器
virtualTable.addListener(SWT.SetData, event -> {
TableItem item = (TableItem) event.item;
int index = virtualTable.indexOf(item);
// 延迟加载数据
item.setText(new String[]{"行 " + index, "内容 " + index});
});
// 按钮面板
Composite buttonPanel = new Composite(shell, SWT.NONE);
buttonPanel.setLayout(new FillLayout(SWT.HORIZONTAL));
// 加载普通表格
Button loadNormalButton = new Button(buttonPanel, SWT.PUSH);
loadNormalButton.setText("加载普通表格(1000 行)");
loadNormalButton.addListener(SWT.Selection, event -> {
long startTime = System.currentTimeMillis();
normalTable.removeAll();
for (int i = 0; i < 1000; i++) {
TableItem item = new TableItem(normalTable, SWT.NONE);
item.setText(new String[]{"行 " + i, "内容 " + i});
}
long endTime = System.currentTimeMillis();
System.out.println("普通表格 耗时:" + (endTime - startTime) + "ms,内存占用:大");
});
// 加载虚拟表格
Button loadVirtualButton = new Button(buttonPanel, SWT.PUSH);
loadVirtualButton.setText("加载虚拟表格(10000 行)");
loadVirtualButton.addListener(SWT.Selection, event -> {
long startTime = System.currentTimeMillis();
// 只设置行数
virtualTable.setItemCount(10000);
long endTime = System.currentTimeMillis();
System.out.println("虚拟表格 耗时:" + (endTime - startTime) + "ms,内存占用:小");
});
shell.setBounds(100, 100, 600, 500);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
17.3 减少布局计算
缓存布局数据
// 错误示例:频繁修改布局
for (int i = 0; i < 100; i++) {
GridData data = new GridData();
data.horizontalAlignment = GridData.FILL;
data.grabExcessHorizontalSpace = true;
button.setLayoutData(data);
button.getParent().layout(); // 每次都重新计算布局!性能差
}
// 正确示例:缓存布局数据
GridData data = new GridData();
data.horizontalAlignment = GridData.FILL;
data.grabExcessHorizontalSpace = true;
button.setLayoutData(data);
button.getParent().layout(); // 只计算一次布局
费曼解释:布局计算的代价
布局计算 = 计算桌子的摆放
- 频繁计算:每次摆放都重新计算(性能差)
- 缓存计算:只计算一次(性能好)
类比:
布局计算 = 排队
- 频繁排队:每次都重新排(性能差)
- 记住位置:只排一次(性能好)
避免频繁修改布局
// 错误示例:频繁修改控件大小
for (int i = 0; i < 100; i++) {
button.setSize(100 + i, 30);
// 每次都重新计算布局!性能差
}
// 正确示例:只修改一次
button.setSize(200, 30);
费曼解释:避免频繁修改布局的原因
频繁修改布局 = 频繁搬家
- 每次都重新计算家具位置(性能差)
- 一次性摆放好(性能好)
类比:
频繁修改布局 = 频繁搬家
- 每次搬家都重新摆放家具(性能差)
- 一次性搬家后,不再搬家(性能好)
代码示例:减少布局计算
public class LayoutOptimizationExample {
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("布局优化");
shell.setLayout(new GridLayout(1, false));
// 频繁修改布局
Button frequentButton = new Button(shell, SWT.PUSH);
frequentButton.setText("频繁修改布局");
frequentButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
frequentButton.addListener(SWT.Selection, event -> {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 100; i++) {
frequentButton.setSize(100 + i, 30 + i / 2);
frequentButton.getParent().layout(); // 频繁重新计算布局!
}
long endTime = System.currentTimeMillis();
System.out.println("频繁修改布局 耗时:" + (endTime - startTime) + "ms");
});
// 避免频繁修改布局
Button avoidButton = new Button(shell, SWT.PUSH);
avoidButton.setText("避免频繁修改布局");
avoidButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
avoidButton.addListener(SWT.Selection, event -> {
long startTime = System.currentTimeMillis();
// 只修改一次布局
avoidButton.setSize(200, 30);
avoidButton.getParent().layout();
for (int i = 0; i < 100; i++) {
// 只修改内容,不修改布局
avoidButton.setText("点击次数:" + (i + 1));
}
long endTime = System.currentTimeMillis();
System.out.println("避免频繁修改布局 耗时:" + (endTime - startTime) + "ms");
});
shell.setBounds(100, 100, 400, 150);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
17.4 资源复用
资源池模式
// 错误示例:反复创建和释放资源
for (int i = 0; i < 100; i++) {
Color color = new Color(display, 255, 0, 0);
button.setBackground(color);
color.dispose(); // 反复创建和释放!性能差
}
// 正确示例:使用资源池
Color red = new Color(display, 255, 0, 0);
for (int i = 0; i < 100; i++) {
button.setBackground(red); // 复用同一个颜色
}
red.dispose(); // 最后释放一次
费曼解释:资源复用的好处
资源复用 = 借书复用
- 每次都借书还书(反复创建和释放,性能差)
- 借一本书,看完再还(复用,性能好)
类比:
资源复用 = 借水杯复用
- 每次都借水杯还水杯(反复创建和释放,性能差)
- 借一个水杯,喝完再还(复用,性能好)
缓存常用资源
// 资源池
class ResourcePool {
private Map<String, Color> colorCache = new HashMap<>();
private Map<String, Font> fontCache = new HashMap<>();
private Display display;
public ResourcePool(Display display) {
this.display = display;
}
public Color getColor(int red, int green, int blue) {
String key = red + "," + green + "," + blue;
Color color = colorCache.get(key);
if (color == null) {
color = new Color(display, red, green, blue);
colorCache.put(key, color);
}
return color;
}
public Font getFont(String name, int height, int style) {
String key = name + "," + height + "," + style;
Font font = fontCache.get(key);
if (font == null) {
font = new Font(display, name, height, style);
fontCache.put(key, font);
}
return font;
}
public void dispose() {
for (Color color : colorCache.values()) {
if (!color.isDisposed()) {
color.dispose();
}
}
colorCache.clear();
for (Font font : fontCache.values()) {
if (!font.isDisposed()) {
font.dispose();
}
}
fontCache.clear();
}
}
// 使用资源池
ResourcePool pool = new ResourcePool(display);
Color red = pool.getColor(255, 0, 0);
button.setBackground(red);
费曼解释:资源池的原理
资源池 = 工具箱
- 所有工具都放在工具箱里
- 你需要工具,从工具箱里拿
- 用完后,放回工具箱
- 用完所有工具,统一清理工具箱
类比:
资源池 = 餐厅的餐具
- 所有餐具都放在消毒柜里
- 你需要餐具,从消毒柜里拿
- 用完后,放回消毒柜
- 打烊后,统一清理消毒柜
代码示例:资源复用
public class ResourceReuseExample {
private static int resourceCreated = 0;
private static int resourceDisposed = 0;
public static void main(String[] args) {
Display display = new Display();
Shell shell = new Shell(display);
shell.setText("资源复用");
shell.setLayout(new GridLayout(1, false));
// 不复用资源
Button noReuseButton = new Button(shell, SWT.PUSH);
noReuseButton.setText("不复用资源");
noReuseButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
noReuseButton.addListener(SWT.Selection, event -> {
long startTime = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
Color color = new Color(display, 255, 0, 0);
noReuseButton.setBackground(color);
color.dispose();
resourceCreated++;
resourceDisposed++;
}
long endTime = System.currentTimeMillis();
System.out.println("不复用资源 耗时:" + (endTime - startTime) + "ms,创建:" + resourceCreated + ",释放:" + resourceDisposed);
});
// 复用资源
Button reuseButton = new Button(shell, SWT.PUSH);
reuseButton.setText("复用资源");
reuseButton.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
reuseButton.addListener(SWT.Selection, event -> {
long startTime = System.currentTimeMillis();
Color red = new Color(display, 255, 0, 0);
resourceCreated++;
for (int i = 0; i < 1000; i++) {
reuseButton.setBackground(red);
}
red.dispose();
resourceDisposed++;
long endTime = System.currentTimeMillis();
System.out.println("复用资源 耗时:" + (endTime - startTime) + "ms,创建:" + resourceCreated + ",释放:" + resourceDisposed);
});
shell.setBounds(100, 100, 400, 150);
shell.open();
while (!shell.isDisposed()) {
if (!display.readAndDispatch()) {
display.sleep();
}
}
display.dispose();
}
}
17.5 性能分析工具
VisualVM
# 使用 VisualVM 分析 SWT 应用
jvisualvm
# 或者:
jvisualvm --pid <pid>
# VisualVM 可以分析:
# - 内存使用情况
# - CPU 使用情况
# - 线程分析
# - 堆栈信息
费曼解释:VisualVM 的作用
VisualVM = 医生
- 医生检查你的身体(应用)
- 告诉你哪里有问题(内存泄漏、CPU 占用高)
- 建议你如何改善(优化建议)
类比:
VisualVM = 汽车技师
- 技师检查你的车(应用)
- 告诉你哪里有问题(引擎过热、轮胎磨损)
- 建议你如何保养(优化建议)
SWT 专门的性能分析
// 启用 SWT 性能跟踪
System.setProperty("org.eclipse.swt.performance", "true");
System.setProperty("org.eclipse.swt.performance.track", "true");
// 运行应用后,会打印性能信息到控制台
// 例如:
// - 控件创建时间
// - 布局计算时间
// - 重绘时间
// - 事件处理时间
费曼解释:SWT 性能跟踪的作用
SWT 性能跟踪 = 比赛计时
- 记录每个选手的用时(控件创建时间、布局计算时间、重绘时间)
- 找出用时最长的选手(性能瓶颈)
- 针对性训练慢的选手(优化瓶颈)
类比:
SWT 性能跟踪 = 跑步计时
- 记录每圈的用时(布局计算时间、重绘时间、事件处理时间)
- 找出最慢的一圈(性能瓶颈)
- 针对性改进最慢的一圈(优化瓶颈)
17.6 本章小结
性能优化总结
| 优化方式 | 作用 | 类比 |
|---|---|---|
| 避免不必要的重绘 | 减少 UI 更新次数 | 减少粉刷 |
| 虚拟控件 | 按需加载数据 | 电子书 |
| 减少布局计算 | 缓存布局数据 | 计算摆放 |
| 资源复用 | 避免反复创建和释放 | 借书复用 |
| 性能分析工具 | 找出性能瓶颈 | 医生 |
费曼测试:你能解释清楚吗?
- setRedraw(false) 的作用?
- 暂停重绘,统一重绘,减少重绘次数
- 虚拟控件的原理?
- 只加载当前可见的数据,减少内存占用
- 资源复用的好处?
- 避免反复创建和释放资源,提高性能
- VisualVM 的作用?
- 分析应用的内存、CPU 使用情况,找出性能瓶颈
下一章预告
现在你已经掌握了 SWT 性能优化,
可以让应用运行更快、更流畅。
下一章,我们将进行项目实战,
从零构建完整的应用,
把学到的知识真正应用起来。
练习题:
- 创建一个应用,对比频繁 redraw() 和批量更新的性能差异。
- 创建一个虚拟表格,支持 10000 行数据。
- 创建一个应用,对比反复创建资源和复用资源的性能差异。
(提示:使用 System.currentTimeMillis() 测量耗时,使用 Table 的 VIRTUAL 样式)