chromium与markdown极简笔记多线程文本渲染

2021-06-16 16:25:23 浏览数 (1)

最近我的markdown笔记软件做了一次升级,升级内容主要是将单线程的文本渲染做成了多线程的,这样避免了笔记打开时候卡顿的情况。本篇聊一下如何做多线程的文本渲染,以及如何使用chromium的基础模块进行跨平台开发,对于做App客户端、游戏客户端的同学还是比较有实际意义的。

对于一个App来说,用户操作优先级是最高的,也就是说,理想状态下,用户的任何操作都需要立即得到反馈,特别是对于耗时比较久的操作,比如下载文件、渲染大型场景,一般会增加一个loading动画或者进度条之类的元素。

如果这种耗时操作在主线程(一般是UI线程)执行,程序会发生假死的情况,任何点击都不会响应,对于用户来说这种体验非常糟糕,这是最严重的情况,其次是卡顿现象,比如我的笔记软件,在加载一篇一万字左右的笔记的时候会有几百毫秒的卡顿现象,这种体验虽然能够接受,但是总给人一种卡卡的感觉。这个并不是程序性能慢,而是没有用多线程,没有将加载和显示进行拆解。

单线程渲染

单线程渲染是指从加载文本开始,一直到文本显示在屏幕上,都是主线程来处理所有逻辑。

这个过程中可能耗时较长的操作主要是

  • 加载大型文本
  • 解析文本为树形结构
  • 插入到底层富文本,根据字体大小等样式计算文本宽高
  • 渲染引擎根据layout进行文本图片表格的绘制

对于单线程的富文本的渲染,一般分为3个步骤:

  1. 解析markdown,生成一棵dom节点树;
  2. 通过dom节点,依次插入到富文本接口中,比如文本调用insertText方法,图片和公式调用insertImage方法,表格调用insertTable方法。
  3. 渲染文本。

显然,一旦文本结构复杂且很大,文本的渲染就会卡顿。

多线程渲染

多线程的思路就是将可能卡顿的地方放到其他线程中处理。

对于多线程的流程,可以分为如下几步

  • 主线程准备进行文本加载,将文本内容和发送给另外一个线程b
  • 线程b开始解析文本
  • b线程中生成一个文档对象d,插入解析后的数据结构
  • b线程将生成好的文档对象d传给主线程,主线程通过这个对象进行渲染工作

这个过程可以细化,做成文本分段传给主线程,这样主线程能够即时渲染开头部分的文本内容,即使几百mb的文本也不会体验很差。

通过异步操作,原来单线程中需要一秒钟加载完的笔记,现在只会卡顿20多毫秒。另外这种做法还使得逻辑解耦,因为每一步的数据都是独立的相互之间没有影响。另外单线程文本插入过程中会产生大量的layout重算和UI回调以及渲染节点的修改,导致性能非常差,就相当修改一个已经在线产品,会影响很多用户一样,而多线程是在独立线程进行文本插入,这种操作不涉及UI回调,渲染进程不需要更新还没计算完的layout,因此性能会好很多。

多线程看起来是复杂了,但是对于整个流程的控制却相当清晰,而且性能提高了20到30倍。

chromium多线程模型讨论

上面说的多线程处理是使用chromium的base库做的,chromium用重锤砸核桃的方式写了一个多线程模型,这个模型的主要功能是线程间通信,每一个线程交互都是一个task,这个task是一个对象,可以带参数,传递到别的线程队列中,执行的时候可以带参数。

base库比较高效的原因主要是使用了系统接口作为队列,比如Windows下使用纯消息窗口进行消息循环(HWND_MESSAGE不需要UI显示),

在mac、安卓、ios都是使用类似的方式创建消息循环,这种方式作为事件驱动有一个好处是由操作系统控制队列的性能,这样对于系统更加友好,也会更加高效。如果自己在线程内部写一个死循环,看起来不费性能,但是这就像操作系统是一个管家,每个进程的线程都是一群孩子,如果每个孩子都一起向管家要糖吃,管家就不知道要给哪个孩子糖吃,但是这群孩子如果排队,那么系统运行就会很顺畅。

base库另外一个出彩的地方是task的封装,这个封装简直完美,用起来非常方便。

如下图所示:

代码语言:javascript复制
// 插入图片的事情交给底层统一处理
auto task = base::Bind(&MRendererImageNode::QueueTaskInsertImage, base::Unretained(this), tree->getTextEdit(), src);

if(tree->IsRenderWithThread()){
    addDocumentQueueTask(tree->getTextId(), task);
}else{
    task.Run();
}

base::Bind函数的第一个参数是类成员函数,第二个参数是对象,后面2个参数是函数参数。函数创建一个对象task,我们可以在别的线程中调用task.Run()方法,Run方法可以带要运行的函数参数。

这个实现是使用C 的模板来实现的,实现细节非常复杂,需要对模板技术非常熟悉才能写得好这样的接口。

结语

本篇是极简笔记多线程文本渲染的开发总结,如果你也对富文本编辑器感兴趣,可以持续关注ACM算法日常,我打算把富文本的开发细节做成一个系列,以便后来人能够非常轻松的解决富文本编辑器问题。

后续引子

App程序的开发可以复杂如chrome浏览器,富文本是其中比较复杂的一种App,对于我这种强迫症患者来说,开发一个笔记App必须跨平台、秒速启动、运行流畅、用户操作符合系统习惯,目前极简笔记还是差了很多,也因此我一直在探索如何让程序更好的运行。当前版本的极简笔记采用QT框架开发,然而QT的技术很难做到极致,也因此我产生了一个新的思路:

富文本的核心部分可以采用QT现有的数据结构,然而渲染层最好能够嵌入到各个平台的本地接口中,比如Windows下面可以使用duilib作为窗口和控件管理,自定义一个文本渲染层,对接到duilib控件中,IOS和安卓用系统本地语言开发界面,自定义渲染层对接到view中,这样能够做到App本地高效运行,又能跨平台使用同一套富文本底层框架。

这就像一套组合拳,组合了chromium的base库、QT的富文本数据结构和layout接口、duilib的窗口管理、各平台本地开发接口等,渲染层抽象出来可以使用各平台本地渲染,也可以切换为跨平台的skia图形引擎。

让我们一起拭目以待吧。

ps:公众号输入note获取下载地址哦。

0 人点赞