C有一个鲜为人知的运算符叫”趋向于”, 写作“-->”。比如说如果要实现一个倒数的程序,我们可以定义一个变量x,然后让它趋向与0:
#include <stdio.h> int main(int argc, char** argv) { int x = 10; while (x --> 0) { printf("%d ", x); } return 0; }
会打印出:
9 8 7 6 5 4 3 2 1 0
----------------
好吧我承认我是来恶搞的。。。不过程序真的能run。
评论里说我应该加上参考文献,所以去找了一下我最开始看到这个的stackoverflow - 这个链接里面还有更多脑洞大开的解释。。。
[1]
c++ - What is the name of the "-->" operator?补充一个,
XOR linked list原理很简单,利用C的按位异或只用一个字节的指针信息就实现双向链表
除了开头和结尾,每个节点保存其相邻节点的地址的异或结果,正向遍历时用当前节点地址字段中保存的值异或后一个,反向遍历时异或前一个。
而开头节点存下一个节点地址,尾节点保存前一节点地址。
这样一来,用一个接口就能实现链表双向遍历,还比双链表节省空间
自定义"控制流"
假设我们要打开一个文件读写, 读写完毕把文件关闭, 并且加一点错误处理, 那么代码会像这样:
int f = open("foo.txt", O_RDWR); if (f >= 0) { ... close(f); // 忘记 close 怎么办? } else { // 错误处理 }
如果像 Ruby 一样, block 结束可以自动关闭就好啦! 利用 for 语句的执行顺序我们居然可以做到:
#define OPEN(f, ...) for (int f = open(__VA_ARGS__), _m = 1; _m; _m--, f >= 0 && close(f)) if (f >= 0) OPEN(f, "foo.txt", O_RDWR) { ... // 自动关闭, 再也不用手动调用 close 了 } else { // 错误处理 }
实现 foreach ... in
有了上面的自定义控制流,我们定义一个宏 in
,让它展开后变成 ,
,然后模拟 foreach 语法行不行?像这样:
#define in , #define foreach(e, a) for(int i = 0, elem* e = a->elems; i != a->size; i++, e++) ... foreach(e in a) { // 编译不通过 >_< }
但上面代码通不过编译,编译器在展开 foreach
时还没有展开 in
,而 foreach
是 function-like macro,校验参数个数就当成一个参数计算,然后就败了。解决此的奇技淫巧就是在宏里包括号,这样就能跳过 function-like 的校验先把 in
给展开了:
#define in , #define foreach(...) foreach_ex(foreach_in, (__VA_ARGS__)) #define foreach_ex(m, wrapped_args) m wrapped_args #define foreach_in(e, a) for(int i = 0, elem* e = a->elems; i != a->size; i++, e++) foreach(e in a) { // 好使了 }
单元测试 DSL
我们甚至可以让 C 的单元测试写起来像这样:
void your_suite() { ccut_test(foo1) { assert_true(2 == 2, "wat?"); } ccut_test(foo2) { assert_false(1 == 2, "no way!"); } ccut_test(bar) { pending; } ccut_test(simple equal) { assert_eq(expected, actual); } }
这里除了使用“自定控制流”的技巧以外,还用到了 coroutine 的技术,详见 https://github.com/luikore/ccut/blob/master/ccut.c
"类型推断"
我们知道 C++1x 可以用 auto 来省略类型声明, 而 C 的 auto 关键字意思完全不同但明显是个废话(auto 的意思是非 static, 和不写一样), 还好有 C 有 __typeof__
#define var(left, right) __typeof__(right) left = (right)
然后用起来就像这样:
var(s, 1LL); // 相当于 long long s = 1LL;
宏参数单次求值
我们知道 C 的宏会把给的参数原样拷贝到 macro 体内, 所以宏里经常要多加好多括号很麻烦, 而且如果参数在宏内出现多次的话就会被求值多次, 例如下面的宏 DOUBLE 就有这样的问题
#define DOUBLE(a) ((a) + (a)) int foo() { printf(__func__); return 3; } int main () { DOUBLE(foo()); // 调用了两次 foo() }
在 GCC/Clang 中, 利用 __typeof__ 与局部变量, 就可以让宏参数只被求值一遍
#define DOUBLE(a) ({ __typeof__(a) _x_in_DOUBLE = (a); _x_in_DOUBLE + _x_in_DOUBLE; })
细心的你会发现为什么要用 _x_in_DOUBLE 这种蹊跷的名字呢... 因为如果表达式 a 带有一个变量恰好和宏里声明的局部变量同名, 你就挂了...
把 GCC 去掉, 只用 Clang/OpenCL 的话, 利用 lambda 表达式字面量, 这个问题终于能完美简单的解决了:
#define DOUBLE(a) (^(__typeof__(a) x){ return x + x; }(a))
所以用 Clang 写 C 真的很 high...
诱导常用选择支 LIKELY
很多编译器都会提供一些 builtin 函数来帮助帮助优化, 有些是直接调用能完成复杂操作的 CPU 指令例如 popcnt, aes256, crc32, 有些是指导生成代码.
比较常见的例如 __builtin_expect 就是帮助做编译期分支预测的, 它没有执行效果, 只会帮助编译器把不常用的分支代码移离正常执行路径, 来提高执行速度.
由于某些编译器没有这个内部函数, 或者直接用 PGO (profile guided optimization) 也能达到这个效果, 所以就定义为 LIKELY 和 UNLIKELY 宏可以随时关掉好了
#ifndef __GNUC__ #define __builtin_expect(x, expected_value) (x) #endif #define LIKELY(x) __builtin_expect(!!(x),1) #define UNLIKELY(x) __builtin_expect((x)!=0,0)
使用例子: malloc 出 NULL 的情况真的很少, 那么我们可以
ptr = malloc(size); if (UNLIKELY(ptr == NULL)) { ... some dirty work ... }
其他的, 相信看这个问答更有用:
有很多啊
快速范围判断
经常要批量判断某些值在不在范围内,如果 int 检测是 [0, N) 的话:
if (x >= 0 && x < N) ...
众所周知,现代 CPU 优化,减分支是重要手段,上述两次判断可以简写为:
if (((unsigned int)x) < N) ...
减少判断次数。如果 int 检测范围是 [minx, maxx] 这种更常见的形式的话,怎么办呢?
if (x >= minx && x <= maxx) ...
可以继续用比特或操作继续减少判断次数:
if (( (x - minx) | (maxx - x) ) >= 0) ...
如果语言警察们担心有符号整数回环是未定义行为的话,可以写成这样:
if ((int32_t)(((uint32_t)x - (uint32_t)minx) | ((uint32_t)maxx - (uint32_t)x)) > = 0) ...
性能相同,但避开了有符号整数回环,改为无符号回环,合并后转为有符号判断最高位。
第一个 (x - minx) 如果 x < minx 的话,得到的结果 < 0 ,即高位为 1,第二个判断同理,如果超过范围,高位也为 1,两个条件进行比特或运算以后,只有两个高位都是 0 ,最终才为真,同理,多个变量范围判断整合:
if (( (x - minx) | (maxx - x) | (y - miny) | (maxy - y) ) >= 0) ...
这样本来需要对 [x, y] 进行四次判断的,可以完全归并为一次判断,减少分支。
补充:加了个性能评测:
性能提升 37%。快速范围判断还有第二个性能更均衡的版本:
if ((unsigned)(x - minx) <= (unsigned)(maxx - minx)) ...
快速范围判断的原理和评测详细见:《快速范围判断:再来一种新写法》。
更好的循环展开
很多人提了 duff's device ,按照 gcc 和标委会丧心病狂的程度,你们用这些 just works 的代码,不怕哪天变成未定义行为给一股脑优化掉了么?其实对于循环展开,可以有更优雅的写法:
#define CPU_LOOP_UNROLL_4X(actionx1, actionx2, actionx4, width) do { unsigned long __width = (unsigned long)(width); unsigned long __increment = __width >> 2; for (; __increment > 0; __increment--) { actionx4; } if (__width & 2) { actionx2; } if (__width & 1) { actionx1; } } while (0)
送大家个代替品,CPU_LOOP_UNROLL_4X,用于四次循环展开,用法是:
CPU_LOOP_UNROLL_4X( { *dst++ = (*src++) ^ 0x80; }, { *(uint16_t*)dst = (*(uint16_t*)src) ^ 0x8080; dst += 2; src += 2; }, { *(uint32_t*)dst = (*(uint32_t*)src) ^ 0x80808080; dst += 4; src += 4; }, w);
假设要对源内存地址内所有字节 xor 0x80 然后复制到目标地址的话,可以向上面那样进行循环展开,分别写入 actionx1, actionx2, actionx4 即:单倍工作,双倍工作,四倍工作。然后主体循环将用四倍工作的代码进行循环,剩余长度用两倍和单倍的工作拼凑出来。
现在的编译器虽然能够帮你展开一些循环,CPU 也能对短的紧凑循环有一定预测,但是做的都非常傻,大部分时候你用这样的宏明确指定循环展开循环效果更好,你还可以再优化一下,主循环里每回调用两次 actionx4,这样还能少一半循环次数,剩余的用其他拼凑。
这样比 duff's device 这种飞线的写法更规范,并且,duff's device 并不能允许你针对 “四倍工作”进行优化,比如上面 actionx4 部分直接试用 uint32_t 来进行一次性运算,在 duff's device 中并没有办法这么做。
补充:《循环展开性能评测》:
性能提升 12% 。
整数快速除以 255
整数快速除以 255 这个事情非常常见,例如图像绘制/合成,音频处理,混音计算等。网上很多比特技巧,却没有人总结过非 2^n 的快速除法方法,所以我自己研究了个版本:
#define div_255_fast(x) (((x) + (((x) + 257) >> 8)) >> 8)
当 x 属于 [0, 65536] 范围内,该方法的误差为 0。过去不少人简略的直接用 >> 8 来代替,然而这样做会有误差,连续用 >>8 代替 / 255 十次,误差就累计到 10 了。
上面的宏可以方便的处理 8-16 位整数的 /255 计算,经过测试 65536000 次计算中,使用 /255的时间是 325ms,使用div_255_fast的时间是70ms,使用 >>8 的时间是 62ms,div_255_fast 的时间代价几乎可以忽略。
进一步可以用 SIMD 写成:
// (x + ((x + 257) >> 8)) >> 8 static inline __m128i _mm_fast_div_255_epu16(__m128i x) { return _mm_srli_epi16(_mm_adds_epu16(x, _mm_srli_epi16(_mm_adds_epu16(x, _mm_set1_epi16(0x0101)), 8)), 8); }
这样可以同时对 8 对 16 bit 的整数进行 / 255 运算,照葫芦画瓢,还可以改出一个 / 65535 ,或者 / 32767 的版本来。
对于任意大于零的整数,他人总结过定点数的方法,x86 跑着一般,x64 下还行:
static inline uint32_t fast_div_255_any (uint32_t n) { uint64_t M = (((uint64_t)1) << 32) / 255; // 用 32.32 的定点数表示 1/255 return (M * n) >> 32; // 定点数乘法:n * (1/255) }
这个在所有整数范围内都有效,但是精度有些不够,所以要把 32.32 的精度换成 24.40 的精度,并做一些四舍五入和补位:
static inline uint32_t fast_div_255_accurate (uint32_t n) { uint64_t M = (((uint64_t)1) << 40) / 255 + 1; // 用 24.40 的定点数表示 1/255 return (M * n) >> 40; // 定点数乘法:n * (1/255) }
该方法能够覆盖所有 32 位的整数且没有误差,有些编译器对于常数整除,已经可以生成类似 fast_div_255_accurate 的代码了,整数除法是现代计算机最慢的一项工作,动不动就要消耗 30 个周期,常数低的除法除了二次幂的底可以直接移位外,编译器一般会用定点数乘法模拟除法。
编译器生成的常数整除代码主要是使用了 64 位整数运算,以及乘法,略显复杂,对普通 32 位程序并不是十分友好。因此如果整数范围属于 [0, 65536] 第一个版本代价最低。
且 SIMD 没有除法,如果想用 SIMD 做除法的话,可用上面的两种方法翻译成 SIMD 指令。
255 快除法的《性能评测》:
提升一倍的性能。
--
PS:大部分时候当然选择相信编译器,提高可读性,如果你只写一些增删改查,那怎么漂亮怎么写就行;但如果你想写极致性能的代码,你需要知道编译器的优化是有限的穷举,没法应对无限的代码变化,上面三个就是例子,编译器优化可以帮你,但没法什么都靠编译器,归根结底还是要了解计算机体系,这样脱开编译器,不用 C 语言,你也能写出高性能代码。
PS:不要觉得丧心病狂,你们去看看 kernel 里各处性能相关的代码,看看 pypy 如何优化 python 的哈希表的,看看 jdk 的代码,这类优化比比皆是,其实写多了你也不会觉得难解。
--
常数范围裁剪
有时候你计算一个整数数值需要控制在 0 - 255 的范围,如果小于 0 那么等于零,如果大于 255,那么等于 255,做一个裁剪工作,可以用下面的位运算:
static inline int32_t clamp_to_0(int32_t x) { return ((-x) >> 31) & x; } static inline int32_t clamp_to_255(int32_t x) { return (((255 - x) >> 31) | x) & 255; }
这个方法可以裁剪任何 2^n - 1 的常数,比如裁剪 65535:
static inline int32_t clamp_to_65535(int32_t x) { return (((65535 - x) >> 31) | x) & 65535; }
略加改变即可实现,没有任何判断,没有任何分支。本技巧在不同架构下性能表现不一,具体看实测结果。
快速位扫描
假设你在设计一个容器,里面的容量需要按 2 次幂增加,这样对内存更友好些,即不管里面存了多少个东西,容量总是:2, 4, 8, 16, 32, 64 的刻度变化,假设容量是 x ,需要找到一个二次幂的新容量,刚好大于等于 x 怎么做呢?
static inline int next_size(int x) { int y = 1; while (y < x) y *= 2; return y; }
一般会这样扫描一下,但是最坏情况上面循环需要迭代 31 次,如果是 64 位系统,类型是 size_t 的话,可能你需要迭代 63 次,假设你做个内存分配,分配器大小是二次幂增长的,那么每次分配都要一堆 for 循环来查找分配器大小的话,实在太坑爹了,于是继续位运算:
static inline uint32_t next_power_of_2(uint32_t x) { x--; x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; x++ return x; }
以及:
static inline uint32_t next_power_of_2(uint64_t x) { x--; x |= x >> 1; x |= x >> 2; x |= x >> 4; x |= x >> 8; x |= x >> 16; x |= x >> 32; x++ return x; }
在不用 gcc 内置 __builtin_clz
函数或 bsr 指令的情况下,这是 C 语言最 portable 的方案。
。。。。
待续
女王:求求题主放过我,我可不敢有什么政绩。。。
很少有人不基于框架直接写GUI界面啦,我这个回答就从GUI框架反过来推什么语言做GUI合适。(只聊桌面端GUI编程框架)
几乎是C++领域最流行的跨平台桌面端软件开发框架了,这个框架是两个挪威人在1995年创建的,发展至今可以说历史相当悠久,稳定性也很有保障。很多大公司都在用它做界面比如金山的WPS。
它内置了自绘引擎,也就是说界面上的一个按钮,一个文本框,都是Qt的引擎自己画的,这保证了基于Qt开发的软件界面在不同操作系统上看起来是一模一样的。
它提供了大量的与界面无关但与软件开发息息相关的API,比如、网络、文件系统、剪切板等,而且让这些API在不同的操作系统下都有效,这极大的节省了开发人员的时间。
但它也有一些缺点,比如在处理一些特殊需求上很不方便,比如:目前Qt有没有比较好解决高分屏下缩放显示的方案?,Qt没有真正完美的无边框解决方案吗?等,在一些组件的渲染上也会出一些隐藏的较深的问题(QListItem),一旦遇到,就很难解决。
Qt近年来不太专一,qml,qtquick等,搞了很多,而且这些新玩意儿一直不温不火,有些模块做了又废弃了,比如:qt script,搞来搞去,搞的模块繁多且复杂,用起来不是很舒服。
Qt有界面描述语言(XML描述界面),可以通过设计器拖拽空间设计界面,编译期界面描述语言被转义成C++代码,性能上没啥损失。
Qt商业授权不太友好,开发商业应用一定要谨慎,之前听说有公司为此付出了高额的版权费。个人开发者可以免费使用。Qt的免费版本不允许静态链接,会有版权上的限制,但开发者还是可以通过一些特殊的编译方法静态连接Qt的库的。
除了使用C++开发Qt应用外,开发者还可以使用其他语言开发Qt应用,最流行的就是使用Python基于PyQt做Qt应用了,其他语言的绑定不是很成熟,但PyQt仍然有版权的问题。
GTK是1997年创建的,也非常成熟稳定,是C语言开发的,但有很多语言的绑定,比如官方支持的JavaScript、Rust等,当然用C++语言操作GTK也很方便,它也有自绘引擎(Cairo),也提供了大量系统相关的API,商业授权也非常友好,基于GTK开发商业软件不用担心收到律师函的问题,虽然它是一个跨平台桌面软件,但它似乎只在Linux操作系统领域流行,有非常多的Linux桌面软件都是基于GTK开发的。
这也直接导致GTK的维护者很重视Linux领域的发展,而忽视Windows和Mac领域。这个框架提供的很多API,只在Linux下有,Windows和Mac下没有。这样的API数量众多。甚至在Windows下编译一下GTK的源码都要比Linux下难很多。而且GTK的渲染引擎在Windows下性能表现也不如在Linux下好。
GTK在Windows上也没办法静态连接,它到不是因为版权的问题,而是它依赖MSYS2的一些库,这个库用于在Windows上模拟Linux环境,这也是为什么GTK在Windows上表现不佳的原因之一。
另外,由于GTK是C语言开发的,所以开发风格也很C语言化,这对于部分开发者来说可能觉得繁琐。
wxWidgets是1992年英国的一个大学教授开创的跨平台GUI软件,也非常成熟稳定,商业授权非常友好。它没有自绘引擎,而是对不同平台下的界面API做了整合和封装,这样开发者在Windows下开发的软件看起来就是Windows窗口风格、Linux开发的软件看起来就是Linux窗口风格,这对于某些软件来说,正是他们想要的,但要想搞一些花哨的特效就没那么容易了。它同样也提供了大量的系统相关的API供开发者使用。
它是C++开发的,所以对C++开发者非常友好,除此之外它还支持静态连接,也就是说开发个应用不用分发给用户一大堆dll,当然Qt也支持静态连接,但是你得自己编译Qt的源码(不是很方便),而且Qt的授权规则也不允许普通开发者这么做。
它会有些小问题,比如我之前提的:wxEVT_NOTIFICATION_MESSAGE_DISMISSED event emit twice,但总体来说还是非常稳的。除了开发的界面比较死板外,没啥大的问题。目前使用这个框架开发软件的人越来越少了。
fltk是1998年创建的跨平台开源GUI框架,历史悠久,商业授权友好,而且C++之父也用它,它非常轻量级,支持静态连接,一个简单的应用编译后只有500K左右,非常赞,
它有自己的自绘引擎,没记错的话用的是OpenGL,但它的重绘机制是按区域重绘的,如果组件A所在的区域上存在组件B,那么A组件重绘时,会把B组件的给重回掉,开发者必须自己写代码处理这种情况。想象一下,如果你想实现一个A组件fade out的同时B组件fade in的效果,就会非常麻烦。
FLTK提供的一些组件样式都比较刻板,绘图API也比较少,你想实现一个漂亮一点的圆角按钮(它内置圆角按钮的圆角大小是不能改的),必须自己画,而且还得借助一些非常奇葩的手段才行(如果你想知道,可以联系我)
它是C++开发的,但API不够现代,用起来总体还算舒服的,它有Rust绑定:fltk-rs。它的用户比前面三个都少。它提供了一些与界面无关的操作系统API,但非常少,几乎可以忽略。
Duilib
是2010年国内一个开发者开发的GUI开发框架,因为底层基于DirectUI开发,所以只支持Windows平台,不支持跨平台,开源协议友好,商用没有任何问题(需要附加Lincence文件),国内有很多大厂基于这个技术做桌面端应用,比如网易、腾讯、百度,这个框架是基于C++开发的,对C++开发者友好。但框架本身还有一些问题,比如对高分屏支持不佳、特殊控件绘制上也有一些小问题,除了界面相关的API外,几乎没有提供系统级的API,作者纯粹是用爱发电来开发这个框架,所以更新不是很及时。
相对来说网易基于Duilib开发的分支更完善一些:NIM_Duilib_Framework,添加了高分屏支持、多国语言、整合了多线程处理的支持,但环境搭建相对比较麻烦。如果开发者要用这个框架,一定要用develop分支下的代码,master分支下的代码问题很多,这个框架看上去也是作者一个人努力的成果。
Sciter是2006年创建的跨平台闭源GUI框架,足够稳定,商业授权不友好,但个人开发者可以随便用(只能用动态链接库),一旦公司规模超过3人,就得买版权了(有权静态连接)。
它内部封了一个浏览器核心,让开发者使用HTML,CSS,JS来创建界面,但对这个浏览器核心做了大量的精简,不像Electron和NW.js动辄上百兆的体积,它只要6M就够了。当然这也意味着有些浏览器特性它是不支持的,比如CSS3的flex布局,它就不支持(但它提供了自己的flex布局实现方式)。以前它使用自研的一个脚本语言(和JavaScript很像),自从集成了Fabrice Bellard大神的QuickJs之后,就全面支持JavaScript了。它还对一些特殊的场景做了内置的支持,比如渲染大列表。
它使用C++开发,对C++开发者很友好,有Rust、go、Python等语言的绑定,但都是社区提供的,质量堪忧。有很多知名厂商都用这个库做界面,比如360、teamviewer、赛门铁克等。
RmlUi和Sciter很像,可以看成Sciter的替代框架,但RmlUi这个项目有三界作者,一个一个的弃坑不知道新任作者会不会弃坑,目前还不是很成熟,比如我正在尝试帮作者解决的CJK输入法的问题,目前还不推荐大家使用这个框架。
CEF是2008年创立的,基于Chromium的跨平台GUI框架,稳定且商业授权友好,国内很多大厂都用的CEF:比如微信桌面端、网易云音乐桌面端、QQ桌面端、微信桌面端、MATLAB、FoxMail、OBS Studio,装机量破亿。
由于它几乎封了一个完整的Chromium,所以体积非常大,但支持所有的HTMLCSSJS特性,它几乎不提供任何与操作系统相关的API,创建个托盘图标、读写个文件啥的,都要开发者自己完成,它是C/C++开发完成的,对C++用户非常友好,它有gopythonjava等语言的绑定,但都是社区提供的,质量值得担忧。
它对Chromium封装的很好,避免了开发者直接与Blink、V8、Chromium等复杂的代码打交道,很多功能都有默认实现方式,遵从约定由于配置原则,有经验的C++开发者可以很轻松的驾驭CEF框架。
由于Chromium是版本弟,所以CEF版本发布也非常频繁,很多被标记为稳定的版本,还是会出一些莫名其妙的问题,选一个好的版本非常重要。
与Electron一样,它也是分主进程和渲染进程的,所以开发者要非常娴熟的运用跨进程通信的技术,虽然CEF提供了跨进程相关的API,但复杂度还是有点高的,使用的时候要认真细心。
这是微软的跨平台GUI框架,不仅仅支持桌面端,还支持移动端,但官方并不支持Linux的桌面端(黑人问号,感觉与微软近些年向开放、开源的大方针相悖),这个框架新的狠,至今还没发布稳定版。目前还没什么人用。而且不知道将来会不会被微软放弃。
它是.NET平台下的GUI框架,有自绘引擎,对C#开发者很友好,界面依然是用XAML描述的,可能很多人一听到XAML就直接弃坑了。XAML表现力确实弱一些,我觉得WPF没火起来跟XAML有直接关系。
使用这个框架开发桌面应用得封一个.NET框架给用户,当然有了.NET框架应用程序访问一般的系统级API也就不成问题了。
这是JetBrains搞的跨平台GUI框架,也非常新,前段时间刚刚推出1.0.0版本,但这个版本还不是很稳,至少比Flutter Desktop的第一个稳定版要差很多。同样也几乎没什么人用。
它的自绘引擎用的是Google的skia,这个自绘引擎稳的很,Chrome和Flutter都是用的它,所以排版、绘制、渲染之类的工作不太会出问题。比Java生态圈里的Swing和JavaFx要好很多。
JetBrains的东西当然对Kotlin开发者友好啦,Java生态下的很多东西你都能用,访问系统级API也没啥大问题,同样也得考虑封一个JRE给用户。
这是谷歌的跨平台开发框架,开源、免费、文档齐全、投入力度大且持久,同样也新的很,Windows版本刚刚发稳定版,Mac版本还没稳定。
如果你完全没搞过移动端的flutter,想用这个框架开发桌面应用,那么意味着你要学的东西还挺多的。好在dart和flutter入门都不是很难,学习曲线比较平缓。
由于flutter在移动端积累了很多年,所以界面上的一些东西在desktop端都比较稳(skia自绘引擎),与操作系统相关的东西还不成熟,生态也不太好,比如你想订制一下窗口的标题栏,想访问一下注册表这类工作可能得自己想办法。不过它有类似FFI的支持,跟C/C++语言打交道很方便。
开发者直接使用Dart语言描述界面,这会导致众多大括号嵌套在一起的问题,可能很多开发者不习惯。
这是微软Edge浏览器团队推出的跨平台GUI引擎,是闭源的,目前只支持Windows,对C#和C++开发者友好,如果使用C#开发,就得考虑把.NET运行时分发给用户,如果使用C++开发,就得自己处理系统级API的操作,webview2本身是不对系统级API做封装的。
这个框架推出也没多久,很多API也还不稳定,更值得担忧的是这个团队,他们前不久刚刚放弃了自己的浏览器核心转而使用Chromium浏览器核心,不知道他们会不会放弃webview2这个框架。
它的优势是可以复用系统当中已存在的webview2二进制资源,也就是说它虽然封了一个Chromium浏览器核心,但如果你可以确定客户电脑已经存在了基于webview2开发的应用,你的安装包体积可以足够小。
它也是多进程架构,甚至比Electron还要多一个进程(为了复用二进制资源),资源占用比较多。
这个库使用操作系统的浏览器引擎来达到减小安装包体积的问题,Mac上使用Cocoa/WebKit,Linux上使用gtk-webkit2,Windows 10上使用Edge(也就是上一个小节里提到的webview2),它应该是不支持Win7的。开发者要考虑前端代码浏览器兼容的问题。
开源且免费(MIT)有go、Rust、Python等语言的绑定,不过官方支持的是go语言,C和C++,操作浏览器的API非常少,不支持自定义scheme,更别提系统级API了。
采用的技术方案与webview类似,所以安装包也足够小,非常新,还没发布稳定版,开源免费。webview框架碰到的问题TAURI都有,
使用Rust开发,将来会支持Deno,作者说将来会直接使用webview的技术来支持多平台,
NW.js最早把Chromium和Node绑定到一起,用前端知识做界面,用Node技术访问操作系统,最早叫node-webkit,在2012年创建。NW.js基于MIT开源,可以无忧使用。没记错的话,微信小程序开发工具是用NW.js开发的。作者是英特尔的员工,英特尔的一些工具也是用NW.js开发的。
除了Chromium和Node的能力外,NW.js自己也封装了一些系统级API,类似托盘图标、剪切板、系统菜单这种,但数量明显比Electron要少。
NW.js可以在多个窗口间共享同一个Node.js上下文,而且还可以通过配置让Node的上下文和Dom上下文混合,这给开发者带来了很多便利。心智负担减少很多。不像Electron要时刻想着进程间通信,哪些模块当前进程不能用这类问题。
NW.js虽然起步早,但奈何没有杀手级应用,周边的生态和工具链没发展起来。用的人越来越少,维护的投入也不如Electron大,再加上Chromium更新非常频繁,导致NW.js的有些API也不是很稳,恶性循环加剧。
Electron的作者曾经在NW.js团队工作过(NW.js项目贡献第二多的人就是Electron的作者),后来辗转到了github公司,于2013年在创建了Electron,也是个开源免费的产品。由于VSCode、slak等国际型产品都选择了Electron,所以从者甚众,生态和周边工具链也完善的多。虽然开发方式上有点蹩脚的地方(多进程架构及模块归属进程),但瑕不掩瑜。
Electron每创建一个窗口都会多一个进程,这使Electron创建窗口的效率不高(秒级),NW.js有复用进程的机制,即使新窗口加载完全不同域的页面也不会创建新的进程(毫秒级)。这也是为什么很多基于Electron开发的应用都使用Dom模拟弹窗的原因。
无论是浏览器相关的API,还是系统级API,Electron提供的都比NW.js多。
--------2022-02-25更新--------
这些框架除了对开发者使用的编程语言有要求外,还有一个重要的差异就是有没有独立的界面描述语言(也就是UI DSL),这非常重要,涉及到一个框架表达业务的重要能力。
类似XAML、qt的ui文件、HTML+CSS都是界面描述语言,下面这种也可以算界面描述语言,但我感觉它不够纯粹(flutter、qml和Compose Multiplatform都是类似这样的):
panel { row { checkBox(...) row { textField(...) // indented relatively to the checkbox above } } }
但无论如何,显而易见的是,没有任何一个界面描述语言能比的上HTML+CSS组合。想想看:HTML里各种花里胡哨的语义化标签和Dom操作技巧,CSS里的布局方式、伪元素、动画描述...,对比之下你就会觉得XAML、qml直流都是弟弟。
除此之外,一个优秀的GUI框架还有两个重要的需求,这里我简单聊聊:
强大的事件处理机制必不可少。
想想这些:鼠标事件、键盘事件、触屏事件...界面加载完成、媒体播放结束、元素大小改变...网络状态变更、数据段传输完成...另外,还得处理事件冒泡、事件捕获、事件分发吧...
qt的开发者曾经说过qt的SIGNAL和SLOT机制是有性能问题的(但影响很小)
强大的异步处理机制必不可少
你不能在用户处理业务逻辑的时候,让界面渲染工作阻塞,这就需要一个强大的异步处理机制,让开发者自己去开线程去完成业务处理,无疑是又麻烦又会增加开发者的心智负担。
我记得很早之前在C# WinForm应用中,点击一个按钮,如果不用Invoke执行逻辑处理的话,界面就会卡死。
这么看来,在你的GUI应用里包一个浏览器核心还是挺有必要的,这样你就可以用HTML+CSS强大的能力来描述你的界面,用JavaScript强大的事件处理机制和异步处理机制来完成用户交互。
可能有人会想,这会带来很多问题呀,比如应用体积会增大的100M以上、会占用更多的CPU和内存资源,还会更耗电等等。
确实,目前来看这些都是问题,但仔细想想,这些问题应该不会持续太久,网络会变的更快,用户的磁盘和内存会变得更大,CPU处理能力也会更好,耗电的问题当然会持续存在,甚至会愈发耗电,但电的供应会持续增长呀。
web相关的技术之所以胜出,并不是这些技术的设计者有多厉害,而是这20多年间,有大量的人涌入了这个领域,前赴后继的推动着它前进。其他任何一个领域都没有这么热火朝天的景象。推荐大家看看我的另一个回答:
------------2022-02-27更新----------
用Web相关的技术做GUI应用的优势是,让开发者可以把大部分精力投注在业务本身上,而不是处理与GUI相关的技术细节。
实际上所有的框架,都应该是这个目的,比如ORM框架,目的应该是让开发者把大部分精力投注在业务与数据之间的关系上,而不是管理关系型数据的技术细节。
当然这肯定是有损耗的,在性能、稳定性、资源消耗上,都会有所削减。而且,因为有框架的存在,开发者很难深入到框架内部做一些特殊的事情。比如,我们该如何修改HTML的排版渲染机制呢?
所以,有些框架注重性能,有些框架注重开发效率,开发者做选择题的时候也应该衡量这两个问题,你的应用对哪些方面要求多一些呢?
你如果要开发一个视频监控系统,没多少业务功能,但要24小时不间断的记录视频数据,随时调取某一段时间的视频数据,这种应用可能Qt是最好的选择。
你如果要开发一个类似飞书的团队协作应用,业务逻辑复杂的一塌糊涂,而且要在短时间内满足更多用户的需求,占领更多的市场,那么Electron可能是更好的选择(目前飞书已经不再用Electron了,他们自己编译了Chromium核心,自己封了一个类似CEF的框架)
目前微软、谷歌、JetBrains等公司都非常重视桌面端开发框架,也在推各自的框架产品,说明桌面应用领域并没有没落,反而应该更加受到重视。
虽然移动端应用大行其道,但我认为,只有生活、社交、轻娱乐等方向上的应用在移动端有较好的发展。文档协作、大型游戏、开发工具、专业管控软件等应用还是在PC端发展的更好一些,毕竟PC端有更多样的输入输出设备、更广阔的显示和交互的空间,更强的存储和计算能力。
希望桌面软件开发领域的从业者都能获得幸福。
满屏荒唐言,一把辛酸泪,一把辛酸泪,一把辛酸泪...