跳转至

PM重写开发经验分享

PM从2.0开始,全面从Python转向.NET,将重写开发时的一些经验总结一下分享出来

为什么要重写

出于以下几个原因,不得不考虑重写

长期没有熟悉Python项目代码

因个人原因有一年多没怎么碰过PM的源码,等到因开发需求需要重新修改PM时,对已写的代码感觉十分陌生,继续维护一段自己觉着陌生的代码,难度不亚于重写一份

Python开发环境问题

熟悉Python/PyQt开发的人应该对这个点比较赞同,就是Python的设计决定它不是一个特别适合编译发布的语言,虽然社区有提供很多工具包,方便打包编译成可执行文件供用户使用,但是终究不是第一方方案,还是会觉着有些别扭。而且由于Python本身是一门偏向脚本语言,强行打包编译成可执行文件,调试开发等都会带来很多问题,没有什么可靠的工具可以定位问题,除了一遍遍手工编译加日志排错

而C#.NET本身是以一个发布可执行文件为核心设计点的语言,所以围绕可执行文件调试开发的工具也相当丰富,而且关于桌面开发的解决方案,也有很多比较丰富的选择,体验也会好很多

跨平台开发问题

虽然Python确实是一门跨平台的编程语言,但落实到GUI开发上,还是会有很多限制,我很难为了跨平台开发,专门配备两个甚至更多不同系统的开发环境用于做本地调试,更多还是在必要的情况下配置一个不同系统的环境,做简单调试确认功能完整性。

这个问题又回到了问题2上,无论是pyinstaller还是nuitka,都需要在指定系统上编译,前者编译速度快点,后者编译速度很慢,无论是指定系统,还是编译速度,都有些令人不太舒服。现在.NET支持了跨平台交叉编译,至少我不用单独为了Linux系统,专门配置一个Linux专用开发环境并做编译了,这一点和Go做交叉编译差不多。虽然当前主要发布的是x86架构的可执行文件,但如果有用户需要ARM架构的可执行文件,我依然可以在几乎不怎么修改代码的前提下编译发布,并做简单测试验证,这样在配置环境上节省大量时间和精力了

当然还有其他很多细节性的小问题,比如PyQt的资源系统设计,使用的第三方库的版权问题等,都或多或少影响到最终的决策。对于需要做跨平台开发的独立开发者,使用.NET解决方案对我来说在很多细节上节省很多精力

PM项目设计

从2.0版本开始,在起初项目设计上,就已经充分考虑跨平台开发,而1.0版本,在设计之处更多以Windows系统为主,这就导致一些问题,2.0在处理不同系统上的差异问题时,因为项目设计,在开发时可以分模块处理,可以处理好不同系统的问题,但是1.0的设计,整个项目设计都是围绕Windows的,这就导致如果想支持Linux等系统,需要做大量的修改才能勉强适配

借由.NET的解决方案和项目文件的设计,2.0的项目结构大体如下

  • PM.Core(跨平台核心功能)
  • PM.Desktop(项目启动器)
  • PM.UI(跨平台UI逻辑)
  • PM.Resources(PM资源文件)
  • PM.Windows(Windows版本专属功能模块)
  • PM.Linux(Linux版本专属功能模块)
  • PM.Updater(PM更新工具)
  • Others

对于不同操作系统的专属功能模块,采用接口+依赖注入的设计思路去实现,对程序主体,只需要知道并调用接口,具体实现会根据不同操作系统进行切换,这样后续再做扩展开发,比如支持MacOS等,也可以较为轻松的实现。 另外,考虑到后续PM可能不会有大的功能改进,但依然需要跟随Firefox主版本更新Firefox配置,因此,将所有资源文件单独抽离成一个项目文件,后续用户只需单独更新PM.Resources.dll文件,即可快速适配最新版本的Firefox配置

项目难点总结

项目主体功能其实并没有太多难点,因为核心功能在1.x均实现完成,更多是换一种语言将功能进行重写。所以项目难点并不完全在重写功能上,而是在非语言本身的问题上

对1.x兼容问题

1.x的代码虽然不算庞大,只有10k行左右代码量,但依然需要仔细理解回想1.x的设计,在保证功能几乎一致的前提下尽可能兼容1.x的所有配置和操作逻辑,但可惜的是,在1.x设计时并没有使用单元测试做功能验证,所以只有根据1.x代码做功能重写并后续反复测试验证才能尽可能适配

与此同时,在重写期间,也发现1.x的一些bug,比如在处理用户自定义iv问题上,经检查发现1.x存在问题,但由于1.x被大量使用,如何平衡这个问题上需要做很多取舍评估,如果完全修复该问题,则会引发很多不兼容行为,从而影响用户体验,但完全按照1.x的错误设计,这个问题有可能会被利用,因此需要做好充分评估确定才行

1.x其实还有很多以现在的角度看不太合理的设计,其中有些设计因兼容问题不得不继续兼容支持,以保证用户可几乎无缝切换

代理节点配置问题

1.x并没有对很多主流代理配置做兼容支持,仅支持了有限几种,比如vless+tls+ws/vmess+tls+ws等,但近两年新增更多代理协议,按照以往设计思路会很难维持开发,因此得在设计阶段设计一种可扩展的配置模版,这样即使后续再新增一些协议,只要可放入链式代理,依然可快速支持。

后续因个人能力问题,在2.x初期采用v2rayN的配置模版,并将sqlite表结构改成json结构。但在最近发现,v2rayN对表结构做了相当幅度调整,且原有的表结构设计确实谈不上很合理,因此,后续可能采用sing-box/xray的json模版配置,但因此可能产生不兼容行为,所以需要做充分验证评估以确定是否加入支持

在线更新问题

在线更新功能本身并不很复杂,本质上其实就是在线下载+本地文件替换。但这期间会出现一个问题,即更新期间,主程序不能运行,这个问题该如何解决?很容易想到的解决办法,就是单独写个用于本地更新的程序,从而可更新替换本地现有文件即可。这个思路本身没问题,但更多还是非语言层面的设计问题。

到底用什么技术方案编写该本地更新程序呢?如果是采用各平台的专属语言脚本,即Windows的bat,Linux的shell,这对不同系统的维护支持难度较高,且这些语言本身不支持代码调试功能,因此出现一些问题只能逐行排错,且部分脚本语言功能有限,不满足需求。如果是使用C#编写统一的功能呢?但由于.NET默认编译的程序需要runtime,而打包成SelfContained程序则导致体积大幅增加,且会产生一堆依赖库,但是这个功能模块又需要足够小巧,且相对独立。那这个问题该如何解决?近期.NET新增一个功能叫AOT,即可以将c#代码借由AOT编译成对应平台的本机代码,经过测试,使用AOT编译的程序,再加上合理的设计,可将程序体积压缩至4MB左右,对整体程序体积影响较小。但使用AOT会带来其他问题,第一,采用AOT编写的代码,需要尽可能保证不会引入影响AOT编译的代码。第二,需要在指定系统上编译,而非交叉编译。但由于该功能模块相对独立,且不经常更新编译,因此这些问题相对可控。

待优化的方向

  • 高权限操作问题

目前这个项目还有一些点可以优化,比如在Windows系统中,为了能调用执行一些管理员权限的操作,且不能影响主程序,目前采用的方案是调用可提权的命令行程序,这个方案有两个问题,第一,无法调试,只能手工测试验证。第二,频繁弹出UAC弹窗,体验并不是很好,后续的解决方案可能会借助Windows服务/systemd服务,让需要高权限运行的功能放在系统服务中,在必要时调用执行,从而可以平衡使用体验和开发难度

  • Linux打包问题

对于Windows系统,目前有不少相对成熟的安装包打包方案,但Linux系统中,相对可用的方案主要有三个,第一是打包成deb文件,但需要用户手敲命令安装。第二是打包成appimage,但要求程序目录中不能有新增、修改文件,否则体验较差。第三是打包成压缩包,通过脚本安装,但需要开发者熟悉学习shell语言。目前的思路是,参考Windows安装包思路,借由avalonia+AOT,打包编译一个体积相对可控的UI程序,通过UI程序引导步骤进行安装。

  • UI测试问题

还有一点是关于UI测试,目前的功能测试,都需要通过手工测试验证,是否无意间引入bug,纯靠开发者手工测试是否覆盖率足够高,如果项目结构足够复杂,这个方案并不是很可靠,后续需要围绕UI测试寻找一套完整解决方案以做自动化测试,降低无意间引入bug的可能性。

  • 项目编译问题

关于Windows程序打包,由于引入了COM组件,导致无法直接使用dotnet publish进行编译发布,而是使用MSbuild进行编译发布,对于没有安装过MSbuild环境的用户来说,想自主编译PM程序,有一定上手门槛,后续也会考虑逐步移除COM组件,换用其他方案实现,从而可全流程使用dotnet publish进行编译,提高编译效率