乐于分享
好东西不私藏

大学毕业后,我选择继续做自己的软件

大学毕业后,我选择继续做自己的软件
2024 年 4 月 23 日晚上,我坐在宿舍里,手里转着一支笔,把以前做过的项目一个个点开。

距离毕业已经不远了。

那时我已经做了三年软件,作品累计下载量超过千万,每个月有数十万人在使用。这个数字足以让我在自我介绍时显得体面,却不足以回答一个最现实的问题:

毕业以后,我该去找一份工作,还是继续做自己的软件?

大学快结束时,我很容易把人生看成一道必须立刻作答的选择题。考研、就业、考公、创业,每个选项旁边似乎都站着一群已经抵达终点的人,向我证明那条路更加正确。没有人告诉我哪一步得分,哪一步失误,也没有一份标准答案能保证我从此不再后悔。

那天晚上,我没有想明白自己的未来。

第二天早上,我只是从床上爬起来,打开电脑,新建了一个项目。

项目里什么都没有,只有一个空白窗口。我在上面写下:

新建文件夹,预计几年后

Salt Player for Windows™

我截了一张图,配上一句“又来画饼了”,发到了频道里。

现在回头看,那个窗口真正重要的地方,不是它后来变成了一款软件,而是从那天开始,我第一次认真地把一段尚未确定的未来,交给了自己。

一、我没有选择更勇敢的路

大家好,我是不要糖醋放椒盐

我是一名 00 后独立开发者,软件工程专业,也是椒盐音乐 Salt Player、椒盐笔记、青盐云听、Hearusy Spectrum、心事卷轴等软件的开发者。

有人会把独立开发想象成一种很浪漫的生活:没有领导,不用打卡,可以自由决定做什么产品、使用什么技术。

真正身处其中以后,我很少觉得自己是在追求浪漫。

我也考虑过求职,只是心思始终不在那上面。我不愿意准备面试,不愿意把接下来的几年投入一个自己并不期待的方向。市场上当然存在更容易变现的软件生意,但我知道,如果只是为了尽快赚到钱,去做一些自己根本不认同的东西,几年后的我大概会非常讨厌当时的自己。

这并不意味着独立开发比就业更加勇敢,也不意味着坚持热爱就一定能够得到回报。

我只是恰好更愿意承担这一种代价。

所谓选择,不是终于找到一条没有风险、不会后悔的道路。它真正发生的时刻,是我已经看见了代价,仍然决定把自己的时间放进去。
毕业时的选择也没有在那个早晨结束。
它很快变成了另一系列更加具体的问题:该用什么框架,怎样设计软件,又该在什么时候承认,自己最初的判断并不完全正确。

二、一年计划的开始

Windows 桌面端有很多开发方案。我早年接触过 VB,后来也尝试过 WPF、UWP 和 WinUI 3,最终选择了 Compose for Desktop。

这并不是一个足够稳妥的选择。

我从 2021 年开始关注 Compose,也清楚它并不成熟。对于 Windows 软件来说,选择 JVM 技术栈,在一些人眼里几乎等于主动走进桌面开发的鄙视链:

Java 写的软件,吃内存、启动慢、运行卡,内存占用差不多可以和 Electron 坐一桌。

但独立开发者做技术选型,不能只看一项技术在论坛上是否体面。大公司可以让不同团队分别维护不同平台,我只有有限的时间、资金和精力。跨平台复用对我而言不是漂亮的技术愿景,而是非常具体的成本问题。

也许我很容易迷恋“正确答案”。

我比较框架、语言和架构,希望从中选出一条最先进、最优雅、最不会后悔的路线。但真正的软件工程很少提供这种答案。每一项技术都在用某种优势交换另一种缺陷:性能换效率,灵活换复杂,统一换妥协。技术选型不是找到没有缺点的方案,而是判断哪一种缺点,自己有能力长期承担。

我后来认真了解了 JVM,也重新认识了 HotSpot。它当然存在局限,但它本身仍然是一项软件工程上的奇迹

我的态度逐渐变成了:

先相信,再质疑。

先真正用它做出一些东西,再判断它究竟行不行。因为开发者最容易高估的,是自己对一项从未真正使用过的技术的理解。

三、一个人做软件的进度失控

项目刚开始时,我给自己安排了一条看起来很完整的路线。

先完成基础 UI,再设计数据库和音频引擎,之后处理播放队列、歌词和设置,最后集中测试。我计划用一年时间把它做完。

当时我已经做了很多年移动端本地播放器,自认为足够熟悉播放器的功能和结构。我以为 Windows 版本只是工作量更大,不会困难到失控。

但拥有移动端经验,并不意味着能够跨过桌面端的坑;熟悉播放器,也不意味着熟悉 Windows。

最先拖慢项目进度的,甚至不是什么复杂的音频问题,而是 UI。

我对界面细节非常在意。一个间距不统一、一个按钮状态不自然、一个动画不够连贯,我都可能花很长时间处理。我希望软件符合 Windows 11 的视觉语言,也不希望它只是一个被放大到电脑上的移动端应用。

可 Windows 是一个背负着漫长历史的系统。不同年代的组件同时存在,系统软件自身的设计并不完全统一,文档中的规则与实际表现也可能存在差异。有时我只是想实现一个看起来理所当然的效果,却要在框架、系统版本和自定义实现之间反复试验。

独立开发者也不只是一名程序员。

还要同时处理产品、设计、测试、排期、运营、宣发和用户反馈。写出一个功能或许只需要几天,但决定它是否值得被加入,需要考虑它会影响多少用户、增加多少复杂度,以及未来由谁维护。

大一上软件工程课时,老师问我们:“什么是软件工程?应该怎样应对用户需求?”

我当时回答,软件工程就是尽可能完成用户提出的所有需求。

老师笑着说,随着课程继续学下去,我可能不会再这样认为。

后来我确实不再这样认为了。

每增加一个开关,我都要考虑它与其他设置怎样组合;每加入一种特殊行为,都要在之后的版本里继续兼容;今天为了一个场景随手写下的分支,两年后可能会变成谁也不敢碰的逻辑。

功能写完,并不代表这项工作结束了。

很多时候,维护才刚刚开始。

一年计划就是这样一点点失去边界。不是因为我遇到了某个完全无法解决的问题,而是每个问题都能够解决,只是所有问题加在一起,远远超过了一个人能够控制的速度。

四、我曾把完美,误认成认真

项目进行到一段时间后,我申请了 Steam 商店页面。

审核通过的消息,是我下宿舍楼丢垃圾时收到的。我站在垃圾站门口看着手机,高兴得差点滑了一跤。

那些真正改变一件事情的时刻,未必总是庄重的。它可能只是宿舍里一个空白窗口,也可能是一条在垃圾站门口收到的通知。可从那一刻起,原本只存在于电脑和聊天频道里的想法,开始拥有了一个真实的商店页面。

与此同时,项目进度仍然在不断延后。

当时我认为,自己只是对品质要求比较高。后来我才发现,精益求精和迟迟不愿交付,有时只有一线之隔。

我往往会在自己能够控制的地方表现得格外勤奋。调整间距、修改动画、重构代码,这些事情都有清晰的反馈:投入时间,很快就能看到结果。但让用户真正开始使用、接受反馈、承认自己的判断可能错误,意味着作品会离开你的控制。

于是,“还不够完美”就成了一个非常合理的理由。

它让你看起来始终在认真工作,也让你暂时不必面对真实世界。

到了 2025 年上半年,我又花了一个多月,把音频引擎推倒重来。

我希望统一 Windows 和安卓端的音频能力,用一套更漂亮的架构减少重复维护。

真正开始以后,我发现两个平台在音频设备、输出链路、应用生命周期和异常恢复上,都有各自的规则。

接口当然可以起成相同的名字,运行时的行为却不会因此变得一致。

为了维持表面上的统一,我开始在公共层中加入越来越多的平台判断。复杂度没有消失,只是从两个相对独立的模块,被搬进了一个更难理解的抽象层。

一个多月过去,代码看起来更整齐了,发布日期却更远了。

现在回头看,这次重构不能算成功。

我高估了抽象能够解决的问题,也低估了两个平台之间真实存在的边界。有些重复来自设计不足,但另一些差异本来就无法消除。

不过,这段时间也没有完全浪费。

PC 和安卓端的歌词组件、播放队列以及部分基础 UI,在这个过程中逐渐完成了统一。我没有得到最初设想的结果,却更清楚地知道了哪些东西可以抽象,哪些边界必须尊重。

真正让我不舒服的,不是这次重构没有达到预期。

而是我开始承认,我有一部分所谓的认真,其实是在逃避交付。

五、把软件交给真实世界

PC 版本最初,我不愿意太早把一个不完整的软件交给用户。因为我觉得我做 Android 版本 3 年多了。

我以为自己可以先在内部解决大部分问题,然后直接发布一个足够成熟的版本。

很快,我发现我错了。

我的电脑运行正常,并不代表软件真的正常。不同显卡、驱动、系统版本、缩放比例、输入法和第三方软件,都可能改变程序的行为。用户看到的也许只是一个按钮没有反应,开发者却可能要从 UI 框架一路追查到系统接口,最后发现问题来自某个显卡驱动。

用户只看见水面上的一个气泡,开发者必须一路潜下去,找到究竟是哪一层正在漏气。

后来,我建立了测试群,开始定期发布测试版本。四千多位测试用户使用不同的设备和环境,让我见到了大量靠自己永远无法发现的问题。

测试不是发布前寻找几个 Bug。

真实世界永远比开发者脑海里的模型复杂。一个人当然可以写出程序,但只有当程序能够在无数个不由你控制的环境里继续工作时,它才真正开始接近软件工程。

我也逐渐理解了宣发与期待之间的关系。

独立开发者没有大型发布会,也很难在产品上线当天突然获得巨大的关注。持续分享开发进度,能够让用户提前认识产品,看着它从空白窗口逐渐拥有播放界面、歌词、数据库和音频能力。

但每分享一次,期待也会跟着高一点。

用户看了一年的精美截图,自然会认为,正式版本已经足够成熟。

有时我发出一张完成度很高的播放页,别人看见的是一款快要做完的软件;而我知道,截图以外还有很多页面没有适配,很多设备没有测试,还有一些问题,甚至只是暂时没有发生。

后来我不再只想展示最好看的部分。

什么已经能用,什么仍然不稳定,什么暂时做不到,都应该尽量说清楚。

不然项目越受关注,真正发布时的落差就越大。

六、软件上线以后

后来,因为审核机制上的一些阴差阳错,软件在一个没有预料到的时间点上线了。随后,双平台也陆续通过审核。

我曾经想象过发布当天的样子。

也许我会很激动,也许会反复刷新后台,觉得长时间的投入终于有了结果。

真正发生时,却没有什么狂欢。

我只是坐在电脑前,看着后台的数据开始变化。

有人下载,有人打开软件,也有人第一次把自己的音乐文件拖进去。之前所有只发生在测试群里的问题,从这一天开始,可能出现在更多我完全不认识的人面前。

发布以前,我总能告诉自己,软件仍在开发,界面还可以修改,架构也还有机会重来。

发布以后,这些解释突然变得没有那么重要。

用户点击播放,音乐有没有响起来,才是他真正遇见的软件。

后来我看到有用户说:

“这个软件界面很干净,用起来很舒服,没有那些乱七八糟的打扰。”

我不知道他说这句话时正在听什么。

也许电脑放在大学宿舍的书桌上,也许是在出租屋里,也许只是某个很普通的晚上。

上线以后,我花在维护上的时间越来越多。

新功能可以截图,也容易写进更新日志。稳定却很难展示。播放没有中断,程序没有崩溃,系统更新以后仍然可以正常工作,这些事情发生时,通常不会有人特意提起。

因为它们本来就应该发生。

可要让这些“本来就应该发生”的事情持续发生,背后需要处理大量不漂亮的工作。稳定不是保守,它是开发者对用户作出的一种长期回答。

重复测试,修改兼容逻辑,分析偶发错误,为很少见的设备增加判断,然后在下一个版本里重新检查一遍。

以前我更容易被“做出一个新东西”吸引。

后来我发现,能够把一个已经做出来的东西长期维护下去,也不比创造它更容易。

虽然我肯定也会遇到些问题:

25岁,独立开发两年,我在凌晨2:30按下了撤销键

从一个窗口,到另一个窗口

后来,我受邀来到上海,参加华为鸿蒙系统开发培训。

住在酒店时,我站在落地窗前,看着城市里的车流和灯光,突然想起了 2024 年 4 月 23 日的那个空白窗口。

一个是电脑屏幕里刚刚创建的窗口,一个是真正望向城市的窗。

它们之间隔着数据库、音频引擎、失败的重构、四千多名测试用户、平台审核、好评与差评,也隔着很多个我怀疑自己究竟能不能把这件事做完的晚上。

在大学里,很多事情都有明确的结束时间。
课程会结课,考试会出成绩,比赛会公布名次。就算失败,大多也能在一个学期内得到结果。
做自己的软件不是这样。
它没有一个真正意义上的完成时刻。版本发布以后,还有下一个版本;问题解决以后,还会遇到新的问题。
有些付出很快就能看到结果,有些则要过很久,才知道当初的决定有没有意义。

我也没有因为做出了这款软件,就突然找到了毕业以后所有问题的答案。

我仍然会怀疑自己的判断,也会担心收入和未来。看到别人走上更加稳定的道路时,有时也会想,另一种生活会不会轻松一些。

只是那个写着“预计几年后”的空白窗口,最终没有一直停留在一张截图里。

它已经出现在了很多人的电脑上,在某个普通的晚上,为一个我不认识的人播放一首歌。

这件事没有替我回答未来。

但至少,它回答了当年的那个晚上。
这就够了。