从开源 Windows 应用程序 SumatraPDF 15 年的经验教训

2021-07-27 06:09:53

我在 2006 年发布了 SumatraPDF 的第一个版本。那是 15 年前,这似乎是回顾的好时机。 SumatraPDF 是适用于 Windows 的多格式(PDF、ePub、Mobi、comic booc、DjVu、XPS、CHM)查看器,目前如下所示:SumatraPDF 是适用于 Windows 的开源文档阅读器。它最初是一个 PDF 阅读器,因此得名,但随着时间的推移,我添加了电子书格式(epub、mobi)、漫画书(cbz、cbr)、DjVu、XPS、图像格式等。它是针对 Win32 API 编写的,不使用像 Qt 这样的 GUI 抽象库。这有助于使其尽可能小和快速。编写的代码量实际上更高。代码被编写和重新编写是长时间运行的代码库的性质。我们删除、添加、更改。这是一个业余项目,在下班后完成,而不是全职工作。每天在应用程序上工作的感觉如何?你也可以看看我的开发日志。我一年前才开始使用,所以只涵盖了 15 年中的 1 年。

2006 年,我在 Palm 工作,我的工作职责之一是为 Foleo(一款 ARM 和 Linux 驱动的迷你笔记本电脑)编写 PDF 阅读器。你从未听说过 Foleo,因为它在发布前几周被取消,原因我不知道。当时我不知道 PDF 很流行,但 Palm 管理人员知道,这就是为什么他们决定 PDF 阅读器是必备应用程序的原因。我最终成为该项目的(唯一)开发人员。编写 PDF 渲染库需要多年的努力。我们没有几年,所以我使用了 Poppler 开源库。我的工作是编写一个基本的 PDF 查看器,它使用 Poppler 将 PDF 页面渲染成内存中的位图,并在屏幕上对这些位图进行 blit。 PDF 是一种复杂的格式,某些 PDF 的渲染速度很慢。我想提高速度,因为 Jeff Bezos 告诉我,速度是客户永远关心的事情。提高速度的方法是分析代码并查看结果。不幸的是,未发布的 ARM 硬件的工具链不是很好。忘记分析器吧,孩子,感谢你有一个 C++ 编译器,不必像 Steve Wozniak 那样通过输入十六进制来输入汇编。

一旦我让库在 Windows 上运行,我就编写了最简单的 GUI 应用程序,它可以显示页面并允许在页面之间导航。我在我的网站上发布了它。它无能为力,所以我将其标记为 0.1 版。如果您不为您的应用程序感到尴尬,那么您已经等了太久才发布它 获得早期用户,了解他们最想要的功能比辛勤工作数月或数年,并在您知道有人关心之前实现了许多功能。我对渲染时间最长的文档进行了概要分析,并进行了一些非常简单且非常有效的优化。优化字符串类以使用所谓的“小字符串优化”,即在字符串类中添加一个小缓冲区来内联保存小字符串(而不是总是为字符串分配内存)。字符串被频繁使用,其中大多数是通过将其转换为批量读取来小型固定一次字节的 i/o。代码在某些代码路径中的结构方式它将为每个字节执行虚拟 C++ 调用和对 C read() 函数的调用。这些非常便宜,但当你做 500 万次时就不会了

正如我为开源项目做出贡献的经验一样,这与其说是成功,不如说是一次失误。是的,我收到了 13 次提交,但该项目不是很活跃,维护人员并不急于接受小改动之外的任何事情。忘记任何重大的重构。我不是一个自愿用头撞墙的人,所以我停止了尝试。如何在大多数情况下独自工作、没有人进行代码审查、没有专门的 QA 团队的情况下保持高代码质量?自己测试代码。在调试器中逐步执行新添加的代码,验证新添加的功能是否按预期工作,并且通常使用该应用程序大量自动崩溃报告。不幸的是,构建起来很痛苦,但这是您可以做的最重要的事情来提高软件的质量。简而言之:设置异常处理程序以捕获应用程序中的崩溃,在崩溃处理程序中从服务器下载符号以获得可读的调用堆栈,创建包含所有线程、程序和操作系统信息的调用堆栈的崩溃报告,记录并将其提交给服务器。在服务器上,处理这些文件并生成网页以便于查看崩溃情况。就像我说的:构建起来很痛苦。一旦发生崩溃,偶尔查看它们并尝试找出问题所在并修复它 assert()。断言是 C++ 代码中公认的做法:仅在调试版本中执行的附加代码,用于验证某些条件是否为真。如果不是,则说明出了点问题,您应该进行调查。我编写了自己的类断言函数,我在非调试预发布版本中启用该函数,以便我自动从满足这些条件的人那里获取错误报告。相信我:没有多少测试可以与一千个人仅通过使用该应用程序所做的所有不同事情相匹配。

日志记录。在调查问题时,了解导致崩溃的事件顺序会有所帮助。我的微型日志模块记录到一块内存中。这与崩溃报告一起发送。我还可以选择登录到文件,最近我通过命名管道将日志记录添加到了单独的日志记录应用程序。这是完美的,因为大多数时候我不关心日志,但是当我这样做时,我不想重新启动应用程序以启用日志记录。使用单独的日志记录应用程序,SumatraPDF 一直在记录日志,当它检测到日志记录应用程序正在运行时,它也会记录到它。实现是微不足道的:日志应用程序创建一个命名管道,记录器打开管道(就像一个文件),如果打开成功,这意味着记录器应用程序正在运行,它读取我们写入管道的日志静态代码分析:警告的最大级别在 C++ 编译器中,将警告转化为错误,Visual Studio 的 `/analyze' 选项,cppcheck,clang-tidy,GitHub 的 CodeQL。偶尔运行这些并修复错误和警告 ASAN(地址消毒剂),太棒了。在 Visual Studio 2019 的某个时间点版本中添加。它可以以非常小的性能成本检测您是否覆盖内存或尝试读取未初始化的内存。我有一个启用了 ASAN 的配置。它足够快,可以用作常规构建。压力测试。 Sumatra 的工作主要是渲染复杂的文档格式。由于格式的复杂性,特定文件中经常会出现崩溃。为了确保没有崩溃,我编写了一个压力测试代码,用于读取和呈现目录中的所有文件。我通常在发布之前对我多年来积累的大量测试文件进行单元测试时运行它。我没有很多,它们主要用于测试字符串格式等低级功能的边缘情况。他们偶尔会发现错误。内存泄漏。很难找到一个易于使用的内存泄漏检测工具。我正在研究一个非常简单的内置检漏仪。与此同时,我正在使用记忆博士。它有效,但速度超级慢。当您没有很多功能时,改进应用程序既快速又容易。实现“转到”对话框(在 v 0.2 中实现)并不需要太多努力。

一方面,我不想过于频繁地发布,但我也确实希望用户尽快获得新功能。我的新版本政策是:当至少有一个显着的、用户可见的改进时发布。 Web 应用程序将其发挥到极致(有些公司一天多次部署到生产环境中)。在桌面软件中,它涉及更多,我必须构建功能以使其更容易,即添加新版本检查,编写可以更新程序的安装程序。顺便说一句:我的意思是“频率与编写的新代码数量成正比”。 SumatraPDF 的发布绝对不是频繁的,但如果您认为它是兼职的下班后项目,则频繁发布。大多数开源项目可能不属于这一类,但如果您希望您的开源尽可能成功,请把它当作软件公司的商业产品。从第一天起,我就为该应用程序创建了一个网站。它有截图,有文档,很容易下载和安装。诚然,Reddit 上的一个好心人称其为“一个 6 岁孩子制作的网站”。这里的教训有两个方面:

一个 6 岁的孩子建立的网站总比没有网站好。它不一定是漂亮的,它必须是功能性的,我做了基本的 SEO。除了 Google 的“SEO 101”文档之外,别无他物:只需注意 URL,放置正确的元数据,使用正确的关键字。我有一个论坛供用户提问、提交功能请求并偶尔互相支持。凡是推广商业软件的好主意,也是开源项目的好主意。在某些时候,我决定从 Poppler 切换到 mupdf,因为 mupdf 更好并且维护得很好。更改应用程序以使用完全不同的库不是您可以在一个下午完成的事情。为了在支持替代渲染引擎的同时保持编译,我为渲染引擎开发了一个抽象。

引擎将提供 UI 所需的功能:获取文档中的页数、每页的大小(以计算布局)、将页面渲染为位图等。我对抽象的热情远低于大多数程序员(至少那些喜欢在 Hacker News 上发表意见的人)但在这种情况下,它对我很有帮助。我能够将使用 Poppler API 的程序形式逐步转换为通过引擎抽象使用 Poppler 到通过引擎抽象使用 mupdf。有一段时间我同时支持这两个引擎,但最终我切换到只使用 mupdf,以保持应用程序小。在 Firefox 之前有 Netscape Navigator。这是一个应用程序的野兽,结合了网络浏览器和电子邮件客户端。 Netscape 无法自拔,在功能上添加功能,导致 UI 非常复杂。 Mozilla 内部的一小群叛徒分叉了代码并专注于简单的 UI。

简单的 Firefox 比复杂的 Navigator 更受欢迎,并最终完全吞噬了它。从一开始,我的目标就是让 SumatraPDF 的 UI 尽可能简单。 80/20 应用程序:80% 的功能和 20% 的 UI。这需要决心。我不断收到向工具栏添加更多图标的请求,我经常不得不说“不”,因为向工具栏添加另外 2 个图标以满足 10% 的用户会使应用程序对 100% 的用户来说稍微差一些。另一个陷阱是附加设置的警报声。有时人们建议程序应该做Y而不是做X。不愿意删除X,他们建议添加一个新的UI设置“[ ]做Y而不是X”。设置对话框包含 100 个设置并不是一个好的解决方案。它使每个人的应用程序变得更糟,因为他们有很多选择,并将重要的选项隐藏在一大堆不重要的选项中。更不用说每个条件行为都需要更多的代码、更多的潜在错误和更多的测试。话虽如此,我也相信可定制性很重要。我相信 Winamp 成为(当时)如此占主导地位的音乐播放器的一个重要原因是它能够为整个 UI 设置皮肤。

某些高级功能可能仅被 20% 的用户使用,但这些用户很可能是高级用户,他们会比其他 80% 的用户更多地宣传应用程序。我为高级设置设计了一个简单的、人类可读(和人类可写)的文本格式。想想JSON,但更好。我没有费心编写 UI 来更改这些高级设置。我只是用该文件启动 notepad.exe。当用户更改设置并保存文件时,我重新加载它并应用更改。我不敢相信有多少流行的项目仍然使用 craptastic Sourceforge 作为源存储库或邮件列表。其实我可以相信:改变是需要努力的,阻力最小的路是什么都不做。我添加了一个浏览器插件,然后在浏览器停止支持此类插件时将其删除。 Windows XP 从大多数用户使用的操作系统变为不再受支持(在 Microsoft 停止支持之后很久)。

起初我只有 32 位版本,现在我有两个但强调 64 位版本。事后看来,支持尽可能多的文档格式是一个显而易见的想法,但我花了 5 年时间才意识到这一点。大多数读者仍然使用单一格式,我相信多格式帮助 SumatraPDF 变得流行。我不能说这是完全独特的想法。早在 SumatraPDF 之前就有了多格式图像查看器,我可能受到了它们的启发。按照今天的标准,SumatraPDF 很小(安装程序小于 10 MB)并且可以立即启动。这又回到了 Jeff Bezos 的智慧:永远不会有用户想要臃肿和缓慢的应用程序的时候,所以小而快是一个永久的优势。我避免不必要的抽象。 Window 的控制系统对于编程来说是一个巨大的痛苦。我可以使用像 Qt、WxWindows 或 Gtk 这样的包装器。它们更易于使用,但会导致瞬间的巨大膨胀。

我不怕写我自己的东西实现。我有自己的 JSON、HTML/XML 解析器,它们只是这些任务的流行库的一小部分。假设我需要做一个网络请求。我可以包含像 curl 这样的怪物库,或者我可以使用 win32 API 编写 300 行代码。我写了 300 行代码。当我在 Palm 工作时,我正在参加一个手机自动更新系统的设计会议。其中一部分是在图像中存储有关当前版本的信息,下载有关最新版本的信息并进行比较。开发人员决定使用 XML 来存储该信息。对于存储像版本号这样的简单信息来说,这似乎是一个很大的负担。一个兼容的 XML 解析器本身就是很多代码。我建议使用简单的二进制格式当然更容易实现,但被忽略了。为了存储高级设置,我设计并实现了一种比 XML 更小的、人类可读和可写的文件格式,并且可以用几百行代码实现。它与 JSON 一样强大,而且更具可读性。它非常简单,在实现之后我有时间为 C++ 对象和 Go 代码生成器实现一个序列化系统。要添加更多设置,我不必编写更多 C++ 代码。我只是将数据定义添加到 Go 生成器,重新运行它并自动生成数据驱动的 C++ 解析。当有人付钱给你写代码时,你必须按照他们喜欢的方式去做。

编写无需付费的代码的一大吸引力在于,没有人可以告诉您该做什么或如何做。我的代码不会通过谷歌的代码审查,不是因为它很糟糕,而是因为它通常是非正统的。在公认的教条之外。通过不使用 STL 来最小化代码大小?这太疯狂了,但我做到了。诚然,2006 年的 STL 并不是很好。我了解了 Plan 9 C 代码如何使用非传统的 #include 文件方案,它们不会在每个 .h 文件中放置 #ifdef 包装器以允许多个包含,并且 .h 文件不包含其他 .h 文件。因此,.c 文件必须以正确的顺序包含他们需要的每个 .h 文件。这有点痛苦,我所知道的其他现代 C++ 代码库都没有维护这样的纪律。但这是我的项目,所以我做到了,我一直在做。它可以防止 .h 文件之间的循环依赖,并且不会因为粗心一遍又一遍地包含相同的文件而延长 C++ 构建时间。我实现了一个受 CSS 启发的 UI 系统。不是很好,但我的。我打算换一个不同的。支持其他平台(Linux、Mac、Android)是最常见的请求之一。我不得不拒绝的请求。

首先,有一个务实的原因:我只是没有足够的带宽来为 3 个平台编写代码。其次,我相信一个平台的优秀应用程序可以比三个平台的平庸应用程序更受欢迎。回到第一个原因:我没有足够的带宽来编写 3 个优秀的应用程序。 SumatraPDF 很小的部分原因是我在 UI 中使用了 win32 API。一个人甚至尝试跨平台应用程序的唯一方法是使用像 Qt、WxWidgets 或 Gtk 这样的 UI 抽象层。问题是 Gtk 很丑,Qt 非常臃肿,WxWidgets 几乎不能工作。我并不是说测试不好,或者你不应该编写测试或进行代码审查。教条是强大的。有时在我的公司生活中,我觉得编写测试只是在进行。也许我们应该花更多的时间来编写代码,但我?

但是尝试向您的开发人员同事提出更多测试与更多代码的细微差别,你会被烧死,你闷烧的尸体会被扔给野狗。村里的孩子会用你的头颅踢足球。然而我确实知道你可以在没有测试的情况下编写复杂的、相对没有错误的代码,因为我做到了。我确实知道您可以编写复杂的、相对没有错误的代码,而无需任何人查看您的代码,因为我做到了。如果很多人使用你的应用程序并且它崩溃了,他们会告诉你然后你会修复它。 SumatraPDF 相对流行。不是 Facebook 流行或 DOOM 流行,但比大多数应用程序更流行。受欢迎程度可观。这一切都始于 v 0.1 和少量下载。很多很多个月,它仍然是涓涓细流。不幸的是,在那个阶段,它与(最终的)失败无法区分,所以如果你正在从事一个尚未成功的项目并争论是否应该继续或放弃,那么这种智慧对你没有帮助

如果你想赚钱,你可以做任何其他事情:尝试销售软件、做咨询、建立一个 SAAS 并按月收费、抢银行。曾经有一段时间 AdSense 会支付不错的每千次展示费用,所以我在网站上投放了 AdSense 广告并赚了一些钱。我不再这样做,因为费率确实下降了,而且不值得惹恼人们。我的灵魂是有代价的,而 AdSense 再也负担不起了。现在我正在尝试使用 Patreon 和 Paypal 捐款。它每月赚 100 多美元,但仅此而已。你很少能同时拥有:做任何你想做的事情的自由和高薪,所以选择对你更重要的东西。开源给你自由而不是金钱。多年来,我一直拒绝添加编辑功能。 “它只是一个读者”我说。但是为什么不添加编辑呢?如果人们想要它,就给他们。所有软件的未来都是作为一个网络应用程序。为什么不把 SumatraPDF 的精神带到网络上呢?像水一样意味着在 5 年内我会有其他想法,根据当时发生的情况。