
我的 iOS 系统语言是英语,地图 App 语言强制跟随系统语言,无法单独设置。
在国内使用时,地名会显示为英文拼音,难以辨识,如下图。

于是我开发了 MapsLingo,给 Apple 地图增加了独立语言设置功能。
- 下载地址:https://github.com/Druadach/MapsLingo/releases/download/v0.1.0/MapsLingo-0.1.0.dylib ↗
- 仓库:https://github.com/Druadach/MapsLingo ↗
- 环境:TrollStore + iOS 15 及以上
- 兼容性:目前只在 iOS 15.4.1 实机测试通过
一、使用方式#
安装注入#
- 在多任务界面把 Apple 地图向上划掉,彻底退出;
- 打开 TrollFools → Advanced Settings,开启 Prefer Main Executable,其余保持默认;
- 只注入
MapsLingo-0.1.0.dylib,不要和应急恢复库MapsLingoRestore-0.1.0.dylib同时注入。
切换语言#
- 打开地图,首次会自动弹出语言面板(之后在地图内双指按住不动 1.2 秒唤出);
- 选择语言并确认,面板上的勾选表示”已保存”,当前界面不会立即变化;
- 关键一步:进多任务界面把地图划掉彻底退出,再重新打开。只回桌面不算退出——语言偏好只在下一次冷启动时加载。
二、原理#
iOS 的语言选择落在偏好系统里。每个 App 的沙盒偏好域(这里是 com.apple.Maps)都有自己的 AppleLanguages 数组,App 冷启动时读自己域里的这份值来决定界面语言——不写这份值时才会回落到系统首选语言。
所以”独立语言”不需要改系统设置,只需要往地图自己的偏好域里写一份数组,等于替地图做了一个 per-App 语言覆盖:
// language 是地图自带本地化里的语言标识符,如 "en" / "zh-Hans"
const void *values[] = {language};
CFArrayRef requested = CFArrayCreate(kCFAllocatorDefault, values, 1, &kCFTypeArrayCallBacks);
CFPreferencesSetValue(CFSTR("AppleLanguages"), requested, CFSTR("com.apple.Maps"),
kCFPreferencesCurrentUser, kCFPreferencesAnyHost);
CFPreferencesSynchronize(CFSTR("com.apple.Maps"), kCFPreferencesCurrentUser, kCFPreferencesAnyHost);c真实代码里这些字符串都是运行时构造的(CFStringCreateWithCString),不落静态常量,见下节。
可选项直接取自地图包自身的本地化资源,不做任何猜测:
CFArrayRef localizations = CFBundleCopyBundleLocalizations(CFBundleGetMainBundle()); // 跳过 "Base" 并去重c所以面板里只会出现当前地图版本真正带了的语言;选一个地图没有的语言会被拒绝并提示,不会写脏配置。列表按语言名排序,zh-Hans / zh-Hant / en 置顶。
三、备份与恢复#
只写不备份的插件是不敢用的。MapsLingo 在地图偏好域里另存一份 MapsLingo.SafeBackup.v1,用三个状态描述”谁改的、改到哪了”:
| 字段 | 含义 |
|---|---|
originalValue | 首次修改前的原始值(只在第一次修改时写入,之后不再覆盖) |
lastAppliedValue | 上一次由本插件写入的值 |
pendingValue | 正在写入、尚未确认完成的值 |
写入顺序是:先记备份 → 再写语言 → 回读校验(CFPreferencesSynchronize + 读回比对)→ 清掉 pending。即使中途断电或被杀进程,pending 也能让下一次操作识别出”上次没写完”。
「恢复原设置」同样保守:只有当偏好域当前值仍然等于插件写进去的值(或等于 pending 值)时才会回滚;如果期间用别的方式换过语言,插件会保留你后来的改动并清除旧备份,而不是硬拽回去。
四、为什么不做 Hook#
- 不替换任何 Objective-C 方法,不用 Substrate / ElleKit,不注入系统框架。
- 回调类在运行时创建,字符串也运行时构造,二进制里没有静态 ObjC 类表、category、protocol 列表,也没有
__cfstring段;-no_fixup_chains+ ad-hoc 签名,对 arm64e 的 PAC 与签名校验更友好。 - 插件只在
getprogname() == "Maps"且 bundle id 为com.apple.Maps时启动,误注入到别的 App 里直接不工作。
Class callbackClass = objc_allocateClassPair(objc_getClass("NSObject"), "ML031LanguageCallbacks", 0);
class_addProtocol(callbackClass, objc_getProtocol("UITableViewDataSource"));
// 方法签名从协议里取,避免手写 type encoding 出错
struct objc_method_description method =
protocol_getMethodDescription(protocol, selector, YES, YES);
class_addMethod(callbackClass, selector, implementation, method.types);
objc_registerClassPair(callbackClass);c面板本身是标准 UIKit:UITableViewController(Inset Grouped)+ UINavigationController(Form Sheet),走系统控件、动态字体与深色模式,没有自绘 UI。
唤出用的是双指长按手势,参数都调到”不干扰地图”:
UILongPressGestureRecognizer *gesture = [[UILongPressGestureRecognizer alloc] initWithTarget:...];
gesture.numberOfTouchesRequired = 2; // 双指
gesture.minimumPressDuration = 1.2; // 1.2 秒
gesture.cancelsTouchesInView = NO; // 不吞地图自己的触摸
gesture.delaysTouchesBegan = NO;
gesture.delaysTouchesEnded = NO;objc再实现 gestureRecognizer:shouldRecognizeSimultaneouslyWithGestureRecognizer: 返回 YES,保证拖动、缩放、搜索、导航手势照常工作。手势按窗口安装并用弱引用表去重,前后台切换不会重复叠加。
五、构建#
bash build.sh # 编译 arm64 与 arm64e 两个切片并 lipo 成通用 dylib
node scripts/verify.mjs # 校验产物
node scripts/package.mjs # 打包bashmacOS 走 Xcode 的 iPhoneOS SDK;Linux / WSL 指定交叉工具链即可:
TOOLCHAIN_BIN=/path/to/iphone/bin \
SDKROOT=/path/to/iPhoneOS.sdk \
bash build.shbash官方 0.1.0 的 release 二进制就是在 Linux 上编的(BUILD-INFO 里能看到 clang version 19.0.0git、target x86_64-unknown-linux-gnu、iOS 15.0 最低版本)。
verify.mjs 不是走个过场,它按 Mach-O 结构逐项断言:
- fat 头 + 两个切片(arm64 / arm64e),切片与单独构建产物字节一致;
- 依赖白名单(CoreFoundation / UIKit / Foundation / libobjc / libSystem);
- 没有
__cfstring段、没有静态 ObjC 类/category/protocol 注册表——这两条是”回归检测”,防止后续改动把静态元数据又带回来; - 未启用 chained fixups、最低系统版本、install name、dylib 版本号;
- 每个代码签名页的哈希逐页校验,且确认是 ad-hoc 签名。
脚本自己也会打印一句免责声明:静态分析与签名校验不能替代设备测试,报告里的 onDeviceVerified 是 false。
六、已知限制#
- 只切界面语言:App 界面、地图 POI 地名、Siri 导航语音属于不同的数据链路,插件只能改前者,不可能改服务器返回的矢量地名与语音引擎。
- 必须冷启动生效:多任务划掉重开,只回桌面无效。
- 只列地图自带语言:地图包里有哪个语言才能选哪个。
- 未全面验证:iPad、多窗口、VoiceOver、其它 iOS 版本都没有测过;0.1.0 是预发布版。
七、恢复与卸载#
⚠️ 仅在 TrollFools 里移除 dylib 不会撤销已经保存的语言偏好。
| 方案 | 操作 | 适用场景 |
|---|---|---|
| A. 跟随系统 | 面板里选「Follow System / 跟随系统」→ 划掉地图重开(可选:移除 dylib) | 常规还原 |
| B. 恢复原设置 | 面板里选「Restore Original / 恢复原设置」→ 划掉地图 | 要回到首次修改前 |
| C. 应急恢复 | 先移除主库,单独注入 MapsLingoRestore-0.1.0.dylib → 打开地图等 3~5 秒 → 移除恢复库 | 面板打不开时 |
八、排错#
- 面板不弹:首次提示关掉后不会每次再弹,用双指长按 1.2 秒唤出;部分辅助功能或手势插件可能冲突。
- 确认注入目标:TrollFools 日志里应出现
Best matched Mach-O is .../Maps.app/Maps,才算注入到了主程序。 mapping process is a platform binary, but mapped file is not:加载或签名异常。先在 TrollFools 里 Eject All,确认原版地图能正常启动,再重来。不要删 Apple 系统框架,也不要盲目叠加注入。- 闪退日志:「设置 → 隐私与安全性 → 分析与改进 → 分析数据」,找
Maps-开头的.ips文件;公开反馈前记得隐去设备标识与路径。