为何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 |
|---|---|---|
| 79x48 | 51 | 50 |
| 159x48 | 51 | 39 |
测试环境:
- 显示器: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
相关产品推荐
相关产品推荐

