第17章:SWT 性能优化

第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 更新次数减少粉刷
虚拟控件按需加载数据电子书
减少布局计算缓存布局数据计算摆放
资源复用避免反复创建和释放借书复用
性能分析工具找出性能瓶颈医生

费曼测试:你能解释清楚吗?

  1. setRedraw(false) 的作用?

- 暂停重绘,统一重绘,减少重绘次数

  1. 虚拟控件的原理?

- 只加载当前可见的数据,减少内存占用

  1. 资源复用的好处?

- 避免反复创建和释放资源,提高性能

  1. VisualVM 的作用?

- 分析应用的内存、CPU 使用情况,找出性能瓶颈

下一章预告

现在你已经掌握了 SWT 性能优化,
可以让应用运行更快、更流畅。

下一章,我们将进行项目实战,
从零构建完整的应用,
把学到的知识真正应用起来。

练习题:

  1. 创建一个应用,对比频繁 redraw() 和批量更新的性能差异。
  2. 创建一个虚拟表格,支持 10000 行数据。
  3. 创建一个应用,对比反复创建资源和复用资源的性能差异。

(提示:使用 System.currentTimeMillis() 测量耗时,使用 Table 的 VIRTUAL 样式)

← 返回目录