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

为何ncurses的curs_refresh()运行缓慢?文件浏览器滚动卡顿排查

Ncurses文件浏览器滚动卡顿问题优化方案

问题背景

我基于ncurses开发了一款简易文件浏览器,支持滚动浏览文件并在底部显示状态栏,但遇到滚动卡顿的问题:通过getch()触发的refresh()操作速度缓慢,滚动流畅度远不如Vim,偶尔出现停滞现象。移除start_color()或缩小终端窗口后性能有明显提升,调整按键重复速率(xset r rate 200 50)也无法让程序跟上滚动速度。

源代码与编译方式

编译命令:

gcc main.c -lncurses

完整源代码:

#include <ncurses.h>
#include <stdlib.h>
#include <stdio.h>
#include <locale.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>

#define MIN(a, b) ({ 
    __auto_type _a = (a); 
    __auto_type _b = (b); 
    _a < _b ? _a : _b; 
})

struct line {
    int indent;
    chtype *text;
    size_t ntext;
};

struct buffer {
    struct line *lines;
    size_t nlines;
    size_t nbytes;
};

struct buffer buf;

WINDOW *view_win;
int view_width, view_height;
size_t v_scroll;

void buf_draw_lines(int y, size_t from, size_t to);

void update_size(void)
{
    view_width = COLS;
    view_height = MIN((size_t) (LINES - 1), buf.nlines);
    if (view_win != NULL)
        delwin(view_win);
    view_win = newwin(view_height, view_width, 0, 0);
    scrollok(view_win, true);
    buf_draw_lines(0, 0, buf.nlines - 1);
    /* Draw full status bar */
    mvprintw(LINES - 1, 0,
        "%4zu-%4zu/%4zu (%3d%%) %zub (view=%dx%d) (term=%dx%d)",
        v_scroll + 1, v_scroll + view_height,
        buf.nlines,
        buf.nlines == (size_t) view_height ? 100 :
            (int) (100 *
                v_scroll / (buf.nlines - view_height)),
        buf.nbytes,
        view_width, view_height,
        COLS, LINES);
    hline(' ', COLS - getcurx(stdscr));
}

int line_append(struct line *line, char ch)
{
    chtype *newtext;

    if (line->ntext == 0 && isblank(ch)) {
        line->indent += ch == ' ' ? 1 : 8;
        return 0;
    }
    newtext = realloc(line->text, sizeof(*line->text) * (line->ntext + 1));
    if (newtext == NULL)
        return -1;
    line->text = newtext;
    line->text[line->ntext++] = ch;
    return 0;
}

struct line *buf_newline(void)
{
    struct line *newlines;

    newlines = realloc(buf.lines, sizeof(*buf.lines) * (buf.nlines + 1));
    if (newlines == NULL)
        return NULL;
    buf.lines = newlines;
    newlines += buf.nlines;
    memset(newlines, 0, sizeof(*newlines));
    buf.nlines++;
    return newlines;
}

int buf_read_file(const char *file)
{
    int fd;
    char b[1024];
    struct line *line;
    ssize_t r;

    if ((fd = open(file, O_RDONLY)) < 0)
        return -1;

    buf.nlines = 0;
    buf.nbytes = 0;
    if ((line = buf_newline()) == NULL) {
        close(fd);
        return -1;
    }
    while ((r = read(fd, b, sizeof(b))) > 0)
        for(const char *p = b; r; r--, p++) {
            if (*p == '\n' && ((line = buf_newline()) == NULL))
                goto sudden_out_of_mem;
            if (*p != '\n' && line_append(line, *p) < 0)
                goto sudden_out_of_mem;
            buf.nbytes++;
        }

/* fall through */
sudden_out_of_mem:

    close(fd);
    return 0;
}

void buf_draw_lines(int y, size_t from, size_t to)
{
    if (to >= buf.nlines)
        to = buf.nlines - 1;
    if ((size_t) (view_height - y) <= to - from)
        to = view_height - y + from - 1;
    for (; from <= to; from++, y++) {
        const struct line *const line = buf.lines + from;
        mvwaddchnstr(view_win, y, line->indent,
            line->text, line->ntext);
    }
}

void handle_char(int c)
{
    switch (c) {
    case KEY_RESIZE:
        update_size();
        break;
    case 'j':
    case KEY_DOWN:
        if (v_scroll == buf.nlines - view_height)
            break;
        v_scroll++;
        wscrl(view_win, 1);
        buf_draw_lines(view_height - 1,
            v_scroll + view_height - 1,
            v_scroll + view_height - 1);
        break;
    case KEY_UP:
    case 'k':
        if (v_scroll == 0)
            break;
        v_scroll--;
        wscrl(view_win, -1);
        buf_draw_lines(0, v_scroll, v_scroll);
        break;
    case KEY_HOME:
    case 'g':
        v_scroll = 0;
        werase(view_win);
        buf_draw_lines(0, 0, buf.nlines - 1);
        break;
    case KEY_END:
    case 'G':
        v_scroll = buf.nlines - view_height;
        werase(view_win);
        buf_draw_lines(0, buf.nlines - view_height, buf.nlines - 1);
        break;
    }
}

int main(int argc, char **argv)
{
    if (argc < 2) {
        fprintf(stderr, "usage: %s <file name>\n", argv[0]);
        return -1;
    }

    setlocale(LC_ALL, "");

    initscr();
    noecho();
    raw();
    curs_set(0);
    start_color();
    keypad(stdscr, true);
    scrollok(stdscr, true);
    idlok(stdscr, true);

    if (buf_read_file(argv[1]) < 0) {
        endwin();
        fprintf(stderr, "error reading file '%s'\n", argv[1]);
        return -1;
    }

    refresh();
    attr_set(A_REVERSE, 0, NULL);
    update_size();

    /* Main loop */
    while (1) {
        mvprintw(LINES - 1, 0, "%4zu-%4zu",
            v_scroll + 1, v_scroll + view_height);
        wrefresh(view_win);
        const int c = getch();
        if (c == 0x03 || c == 'q')
            break;
        handle_char(c);
    }

    for (size_t i = 0; i < buf.nlines; i++)
        free(buf.lines[i].text);
    free(buf.lines);

    endwin();
    return 0;
}

补充测试信息

我尝试用termios+ANSI转义码编写新程序,性能与Vim几乎无差异,但希望基于ncurses实现滚轮功能及类curses 2.0工具。用Alacritty对比Vim与当前程序的FPS(按住J键滚动):

终端尺寸Vim FPS我的程序 FPS
79x485150
159x485139

测试环境:

  • 显示器:1280x800 60hz
  • CPU:Intel Core 2 Duo P8600 (2) @ 2.401GHz
  • GPU:Intel Mobile 4 Series Chipset

优化方案

1. 减少窗口重建与全量绘制

  • 窗口调整优化:update_size()中每次删除重建窗口的开销极大,改用wresize()调整现有窗口大小,避免资源重新分配:
    void update_size(void)
    {
        int new_width = COLS;
        int new_height = MIN((size_t)(LINES - 1), buf.nlines);
        if (view_win == NULL) {
            view_win = newwin(new_height, new_width, 0, 0);
            scrollok(view_win, true);
            idlok(view_win, true); // 启用硬件滚动支持
        } else {
            wresize(view_win, new_height, new_width);
            werase(view_win); // 仅清除窗口内容,不重建窗口
        }
        view_width = new_width;
        view_height = new_height;
        // 只绘制当前可见区域的行,而非全部行
        buf_draw_lines(0, v_scroll, v_scroll + view_height - 1);
        // 状态栏更新逻辑保留
        mvprintw(LINES - 1, 0,
            "%4zu-%4zu/%4zu (%3d%%) %zub (view=%dx%d) (term=%dx%d)",
            v_scroll + 1, v_scroll + view_height,
            buf.nlines,
            buf.nlines == (size_t)view_height ? 100 :
                (int)(100 * v_scroll / (buf.nlines - view_height)),
            buf.nbytes,
            view_width, view_height,
            COLS, LINES);
        hline(' ', COLS - getcurx(stdscr));
    }
    
  • 绘制范围限制:buf_draw_lines只绘制当前可见的行,而非所有行,减少绘制操作量。

2. 批量刷新减少终端IO

ncurses的wrefresh()会直接触发终端更新,多次调用会增加系统开销。改用wnoutrefresh()标记窗口需要更新,最后用doupdate()批量处理所有更新:

// 主循环中替换原有刷新逻辑,添加状态变化标志位
bool status_changed = true;
while (1) {
    if (status_changed) {
        mvprintw(LINES - 1, 0, "%4zu-%4zu",
            v_scroll + 1, v_scroll + view_height);
        wnoutrefresh(stdscr);
        status_changed = false;
    }
    wnoutrefresh(view_win);
    doupdate(); // 合并所有窗口更新,一次发送到终端
    const int c = getch();
    if (c == 0x03 || c == 'q')
        break;
    handle_char(c);
    status_changed = true; // 滚动后标记状态需要更新
}

3. 滚动逻辑优化

  • 给view_win启用idlok(view_win, true),让ncurses利用终端硬件滚动功能,减少字符重绘操作
  • 滚动时仅绘制新进入视图的单行,无需额外操作,硬件滚动会自动完成其他行的位移

4. 颜色与属性优化

启用start_color()后性能下降,可通过以下方式优化:

  • 调用use_default_colors()让ncurses使用终端默认颜色,减少颜色转换开销
  • 避免频繁切换颜色属性,批量处理相同属性的内容
  • 如果不需要颜色,可仅在必要场景启用颜色,而非全局开启

5. 内存分配优化

line_append()和buf_newline()每次追加单个字符/行都调用realloc(),导致大量内存操作:

  • 每行预分配固定大小的缓冲区(比如128字节),满了再按2倍扩容
  • 批量分配行结构(比如一次分配64行),减少realloc调用次数

6. 高效绘制函数替换

如果不需要chtype的属性支持,改用char数组存储文本,用mvwaddnstr()代替mvwaddchnstr(),减少属性处理开销:

// 修改line结构
struct line {
    int indent;
    char *text;
    size_t ntext;
    size_t capacity; // 预分配容量
};

// 绘制时用mvwaddnstr
mvwaddnstr(view_win, y, line->indent, line->text, line->ntext);

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 10:19:53