- 桌面应用
- 系统编程
【免费下载链接】mac-mouse-fix
Mac Mouse Fix - Make Your $10 Mouse Better Than an Apple Trackpad!
本指南基于 mac-mouse-fix 仓库中的 CodeStyle.md 整理而成,并辅以 SharedMacros.h、MarkdownParser.m 等源码佐证。mac-mouse-fix 是一个通过模拟触控板手势来让普通鼠标"变得更好用"的 macOS 应用,其代码库由大量 Objective-C 与少量 Swift 混合构成,并深度依赖私有 API 与底层 IOKit / CoreGraphics 交互。在这种体量和复杂度下,一份明确的代码风格约定直接决定了长线可维护性。读完本文,你将掌握该仓库在头文件组织、可空性(nullability)、方法调用空格风格、枚举命名、switch 替代方案与临时宏六个维度上的具体取舍,以及背后的编译器与工程考量。
这份代码风格文档在项目中的定位
CodeStyle.md不是一份对外发布的开发规范,而是项目作者持续维护的"内部决策记录":每一条风格约定都带有日期标注(如[Feb 2025]、[Jul 2025]、[Nov 2025]),记录当时的动机、尝试、以及后来推翻结论的过程。因此它更像一份"踩坑备忘录 + 约定成文",其价值在于:后续维护者(包括未来的作者自己)能快速理解"为什么代码长这样",而不是仅靠猜测。
该文档并不追求覆盖所有风格问题,而是聚焦于 Objective-C 中最容易引起分歧、也最影响可读性的几个点:头文件防重包含、可空性标注、方法调用空格、枚举定义、基于 block 的 switch 替代方案,以及临时局部宏。
头文件防重包含:#pragma once被弃用的原因
传统上,C/Objective-C 项目会在头文件顶部用#pragma once(或#ifndef守卫)防止同一头文件被重复包含。mac-mouse-fix 的结论却是:
[Feb 2025] 我们其实不需要
#pragma once,因为全项目统一使用#import,它已经解决了重复包含头文件的问题。
#import是 Objective-C 对#include的增强:同一文件只会被导入一次,编译器自动去重。既然全仓库都坚持#import,#pragma once就成了冗余代码。
有趣的是,从仓库现状看,这一约定并未被严格回填:仍有少量旧头文件保留着#pragma once,例如 ListOperations.h、MFBenchmark.h、MFLoop.h、PrivateFunctions.h。这印证了风格迁移的渐进性:新代码不再书写#pragma once,旧文件则按需逐步清理。
NS_ASSUME_NONNULL:刻意不用,以及背后的可空性警告体系
NS_ASSUME_NONNULL是 Apple 提供的一个区域标注宏:包裹在它之间的声明默认按_Nonnull导入 Swift,从而减少 Swift 侧的 Optional 噪音。mac-mouse-fix 明确弃用它,理由很直接:
我们通常希望 ObjC 方法是可空 / null 安全的。当方法返回
nil时,如果 Swift 把它当作非 Optional 导入,那反而是危险的。
作者进一步指出,NS_ASSUME_NONNULL的问题是"只有开启键、没有关闭键"——Apple 没有提供对应的NS_ASSUME_NULLABLE,因此无法按需声明默认可空区域。
仓库里对这一问题有更深的工程化思考,详见 Xcode Nullability Settings。该文档整理了 7 个与空指针相关的 Xcode 构建设置,其中关键的一条是CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION(clang 标志-Wnullable-to-nonnull-conversion):
- 它会在"将可空表达式传给
_Nonnull参数"时告警; - 也会在"从返回类型为
_Nonnull的函数返回可空表达式"时告警; - 但它不对"字面量
nil"产生告警,这需要配合-Wnonnull(默认开启,只对传给_Nonnull参数的字面量nil告警)以及"重定义 nil"的 hack(在OTHER_CFLAGS中加入-Dnil=((id _Nullable)__DARWIN_NULL))来补齐; - 值得注意的是,即使启用了该告警,
NS_ASSUME_NONNULL区域内的代码仍不会生效——警告只在显式书写_Nonnull时才出现。
这套"显式_Nullable/_Nonnull+ 编译器告警"的策略,正是对 CodeStyle.md 中"不用NS_ASSUME_NONNULL"结论的落地支撑:与其依赖区域默认值,不如在函数签名上显式标注,让编译器替你把关 Swift 互操作的安全性。
ObjC 方法调用空格风格:GNU 风格的一次尝试与放弃
CodeStyle.md记录了作者对 Objective-C 方法调用排版的一次探索。GNUstep 风格的写法是在冒号后加空格:
// Gnu 风格 [aCoder encodeObject: [self objectForKey: key] forKey: s]; // Apple 风格 [aCoder encodeObject:[self objectForKey:key] forKey:s];作者认为 GNU 风格在嵌套方法调用时更易视觉扫描:"能更轻松地看清哪些部分属于某个方法签名,以及嵌套调用从哪里开始、到哪里结束"(原文引用了 GNUstep 的NSDictionary.m实现作为参考)。
然而到 [Apr 2025] 这条约定被正式放弃,理由非常现实:
如果坚持这种写法,你会不断和 Xcode 的自动补全作斗争,所以我们放弃了。
这是一个很好的"风格必须服从工具链"的案例:当 IDE 的自动补全默认产出 Apple 风格、且格式化器不会自动纠正时,手工维持 GNU 空格风格的成本远高于其可读性收益。最终项目整体回归 Apple 风格。
枚举定义:kMF前缀命名 +_ToString调试函数
CodeStyle.md提出了一个完整的枚举定义模板,这是全文档中最具实操价值的部分之一:
typedef enum : int { kMFMyEnum_First = 0, kMFMyEnum_Second = 1, kMFMyEnum_Third = 2, } MFMyEnum; NSString *MFMyEnum_ToString(MFMyEnum case) { static const NString *map[] = { [kMFMyEnum_First] = @"First", [kMFMyEnum_Second] = @"Second", [kMFMyEnum_Third] = @"Third", }; NSString *result = safeindex(map, arrcount(map), case, nil); return result ?: stringf(@"%d", case); }这套模板的设计动机(文档明确列出):
- 以枚举名为枚举值统一前缀(
kMFMyEnum_First),让自动补全体验一致; - 用下划线分隔枚举名与枚举值名,避免驼峰粘连;
- 使用
k前缀,与 Apple SDK 惯例一致,也利于自动补全命中; MF前缀标识项目私有符号,与系统符号区分,避免命名冲突;- 配套
_ToString函数,对调试极为友好;虽然略显样板化,但配合多光标编辑很容易批量生成。作者也考虑过用 X macros / foreach 宏消除重复,但认为"过于复杂,不值得"。
其中safeindex与arrcount正是仓库中 SharedMacros.h 定义的通用宏:arrcount(x)基于sizeof计算 C 数组元素个数(并对对象类型做static_assert拦截),safeindex(list, count, i, fallback)提供带边界检查、越界返回 fallback 的数组访问,同时用nowarn_push/nowarn_pop屏蔽-Wsign-compare与-Wnullable-to-nonnull-conversion噪音(这些细节正是"告警噪音太多"的实证)。
作者在 [Nov 2025] 更新中自我评价"这有点蠢,直接写个 switch 就行",但模板本身仍在仓库中留下大量痕迹。典型的实例如 CaptureToasts.m 中的:
typedef enum { kMFCapturedInputTypeButtons, kMFCapturedInputTypeScroll, kMFCapturedInputTypeHorizontalScroll, kMFCapturedInputTypeVerticalScroll, kMFCapturedInputTypeHorizontalAndVerticalScroll, } ...以及 SymbolicHotKeys.m 中为"虚拟键码"定义的哨兵值(kMFVK_Null、kMFVK_FirstAppleKey、kMFVK_OutOfReach),同样遵循"kMF前缀 + 枚举名 + 下划线"的命名纪律。
_ToString模式也在仓库中得到实际应用,例如 AXUIElement_Utils.m 的AXError_ToString、XCUITest_Utils.m 的XCUIApplicationState_ToString,甚至在断言消息里直接拼接枚举名,显著降低排障成本。
替代方案:NSString 常量集合
文档同时给出了另一种思路——用typedef NSString *加一组常量替代 int 枚举:
typedef NSString * MFMyEnum; MFMyEnum static const kMFMyEnum_First = @"First"; MFMyEnum static const kMFMyEnum_Second = @"Second"; MFMyEnum static const kMFMyEnum_Third = @"Third";作者列出的优缺点对比:
| 维度 | 说明 |
|---|---|
| 优点 | 比单独的_ToString函数更简洁;枚举值可直接放进 ObjC 集合而无需装箱(boxing) |
| 缺点 1 | 若放进.h,每个编译单元各自持有一份static变量副本,略不高效(作者认为影响不大;也可声明extern并在.m中定义) |
| 缺点 2 | 无法获得编译器对 switch 穷尽性的检查(作者自认从未用到该检查) |
| 缺点 3 | 序列化后字符串常量不可再修改,否则破坏持久化数据;int 枚举则需保证序列化后不调整顺序(除非显式编号) |
这一方案在仓库中的落地代表是 Links.h:typedef NSString * MFLinkID;配合#define kMFLinkID_CapturedButtonsGuide @"CapturedButtonsGuide"等一系列链接 ID 常量——注意它最终选择了#define而非static const,从源码结构看,这比文档中的示例更进一步规避了"每编译单元一份副本"的问题,也让每个 ID 在代码库中可被grep精确检索。
为什么不用 Apple 的NS_ENUM/NS_TYPED_ENUM
文档明确给出结论-> Don't use。原因在于这些宏会"给 Swift 导入添加魔法":它们会把枚举值在 Swift 侧重命名并命名空间化(例如kMFLinkIDCapturedButtonsGuide变成MFLinkID.capturedButtonsGuide),导致:
- 在代码库中更难搜索这些枚举值的使用点;
- 部分 case 的重命名会出错(文档提到
MMFLActivate就是反面案例)。
这一点在 SharedMacros.h 的MFStringEnum宏注释中得到了完整印证:该宏是作者为"简化NS_TYPED_ENUM使用"而写的,但最终结论是"Unused as of now"(截至 [Apr 2025] 未使用),且注释详细分析了NS_TYPED_ENUM的唯一实际作用就是触发 Swift 重命名,而作者并不想要这种重命名(部分原因是担心 Swift 编译期变慢)。最终推荐做法退化为朴素的typedef NSString *+#define。
Dict-of-blocks:用NSDictionary模拟任意对象的 switch
CodeStyle.md记录了 [Jul 2025] 的一个实验性技巧:对"以任意 ObjC 对象为分支条件"的场景,可以用"装满了 block 的 NSDictionary"来模拟 switch:
NSDictionary <NSString *, void (^)(void)> *dictswitch = @{ @"A": ^{ printf("Case A!\n"); }, @"B": ^{ printf("Case B!\n"); }, }; if (dictswitch[value]) dictswitch[value](); else assert(false);作者在 MarkdownParser.m 内对该模式做了基准测试,结论是:它并不比原生 C 的 switch 慢——"一定有某些疯狂的 clang 优化让它这么快"。
但作者随后给出了实践层面的否定意见:还是优先用带宏的 if-else 链。理由是不必依赖"神奇的 clang 优化"来保证性能,而且写法同样简洁:
#define xxx(value_) else if ([value_ isEqual: value]) if ((0)) ; xxx(@"A") printf(@"Case A!\n"); xxx(@"B") printf(@"Case B!\n"); else assert(false); #undef xxx这段代码也是下一条"临时局部宏"约定的实际示例(宏名统一叫xxx,用完立即#undef)。
值得说明的是,从 MarkdownParser.m 的注释可以看到这段历史的后续:MarkdownParser 现在使用原生 C switch(配合项目自己的bcase宏),注释明确写着"我们过去用装 block 的 NSDictionary,基准测试显示与 C switch 的差距远小于运行本身的随机波动,但既然代码用bcase宏写起来更干净,就保留了 switch"——这正是 CodeStyle.md 中"if-else + 宏 / 原生 switch"结论在真实代码中的落实。
bcase/fcase:原生 switch 的语法糖
对于整数 switch,项目在 SharedMacros.h 中定义了bcase()与fcase()宏,它们是原生 C switch 的薄封装:
bcase(...)在每个 case 后自动插入break(默认行为,b= break);fcase(...)用于需要**贯穿(fallthrough)**的分支(f= fallthrough);- 参数为空时展开为
default分支; - 支持逗号分隔多值匹配。
示例与等价展开:
switch (x) { bcase (A): doA(); bcase (B, C): doBC(); fcase (D): doBCD(); bcase (): doDefault(); }等价于传统写法:
switch (x) { case A: doA(); break; case B: case C: doBC(); case D: doBCD(); break; default: doDefault(); }bcase的实现极其简单:#define bcase(values...) break; fcase(values),而fcase通过参数个数选择器(_fcase_0到_fcase_9)递归展开出case x: case y: ...链。作者自评这套宏"对它们所做的事来说有点复杂"——95% 的用例其实#define bcase break; case就够了——但坚持保留复杂实现,因为这样能让"多值匹配"、"贯穿"与"break"三件事的语义彻底统一,永远不必回到裸case关键字。
真实使用例见 CaptureToasts.m:
switch (inputType) { bcase(kMFCapturedInputTypeButtons): { linkURL = [Links link: kMFLinkID_CapturedButtonsGuide]; ... } bcase(kMFCapturedInputTypeScroll): { linkURL = [Links link: kMFLinkID_CapturedScrollWheelsGuide]; ... } bcase(): { ... } }此外 MFCoding.m、NSCoderErrors.m 等序列化模块也大量使用bcase/fcase,说明该约定已渗透到项目核心的数据编解码路径。
临时局部宏:用xxx压缩样板代码
最后一条约定([Jul 2025]):当某段代码需要用宏压缩样板时,可以在函数/文件局部定义宏,使用后立刻#undef。作者的习惯是这类临时宏统一命名为xxx,并自嘲"不知道为什么,这比给它一个更有描述性的长名字效果更好"——大概因为xxx本身就是"临时占位"的语义信号,且极易全局搜索清理。
前述 if-else 链示例就是标准范式:
#define xxx(value_) else if ([value_ isEqual: value]) if ((0)) ; xxx(@"A") printf(@"Case A!\n"); xxx(@"B") printf(@"Case B!\n"); else assert(false); #undef xxx这种"定义即用、用完即弃"的模式与 SharedMacros.h 中大量长期共享宏(如bcase、safeindex、stringf、mfonce、isclass等)形成互补:一次性逻辑用局部宏,跨文件复用的语义用共享宏。共享宏的命名也有明确纪律(见 SharedMacros.h 头部注释):短且全小写、便于输入;唯一、便于 grep;不唯一时加mf前缀消歧。
小结:这套风格约定的内核
回看CodeStyle.md的全部条目,可以提炼出几条一以贯之的原则,它们共同构成了 mac-mouse-fix 的 ObjC 工程观:
- 能用语言/编译器机制解决就不写冗余代码:
#import已防重包含,就不写#pragma once;显式_Nonnull加告警能防错,就不用NS_ASSUME_NONNULL区域。 - 风格必须服从工具链与可检索性:GNU 空格风格因 Xcode 自动补全而放弃;
NS_TYPED_ENUM因 Swift 重命名破坏 grep 而被弃用。 - 调试友好优先于极致简洁:
_ToString函数、kMF前缀的哨兵值、vardesc式调试宏,都在为排障铺路。 - 宏是双刃剑,明确边界:共享宏(SharedMacros.h)承担复用语义,临时宏(
xxx)负责局部压缩,且都强调"用完#undef"与"命名可 grep"。
对于正在维护大型 ObjC/Swift 混合代码库的开发者,这份文档最有价值的不是某一条具体规则,而是它展示的决策方法:每条约定都记录动机、写下实验数据、允许自己推翻自己(如枚举模板的 [Nov 2025] 自我否定)。风格文档若能做到这种"活文档"状态,才能真正成为团队共识而非一纸空文。
如果你希望将这套约定引入自己的项目,最直接的起点是复用其核心工具层:阅读 SharedMacros.h 中的bcase/fcase/safeindex/arrcount/stringf等宏(注意其中部分宏依赖项目私有的nowarn_push/nowarn_pop与stringf,迁移时需一并携带),并对照 Xcode Nullability Settings 中整理的 7 个空指针相关构建设置,评估是否开启CLANG_WARN_NULLABLE_TO_NONNULL_CONVERSION来强化 Swift 互操作安全性。
- 桌面应用
- 系统编程
【免费下载链接】mac-mouse-fix
Mac Mouse Fix - Make Your $10 Mouse Better Than an Apple Trackpad!
相关推荐
3步解锁Mac高效窗口切换:告别Command+Tab的烦恼
3步解锁Mac高效窗口切换:告别Command+Tab的烦恼 你是否曾在Mac上频繁切换窗口时感到效率低下?面对十几个打开的应用程序,Command+Tab只能
桌面应用Soundflower代码风格指南:Objective-C编码规范与最佳实践
Soundflower代码风格指南:Objective C编码规范与最佳实践 1. 概述 Soundflower作为MacOS系统扩展,允许应用程序之间传递音频
驱动开发音视频Mac Mouse Fix系统升级兼容性维护指南
每次macOS系统更新都像是一场小冒险🎯,你可能期待着新功能,但同时也担心鼠标增强工具Mac Mouse Fix会不会出现兼容性问题。别担心,这篇文章将带你轻
桌面应用系统编程
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考