9.8 KiB
iOS 地图点击重叠 Source 触发 MLNStyle 空 Peer 崩溃调查 v1
- 日期:2026-10-09(Asia/Tokyo)
- 状态:问题已复现 / 原生崩溃堆栈已确认 / 修复与回归未执行
- 分类:Particle iOS Native / MapLibre Darwin Style API 与 maplibre-react-native 点击兼容
- 严重程度:高;普通用户点击海图物体可使 NavSea 直接退出
- 本文件性质:跨项目缺陷移交与定位证据,不是已实施修复、测试 PASS 或新版本发行结论。
1. 用户复现现象
在 iOS NavSea 正常启动并显示地图后:
- 直接点击地图上的「唐津ヨット ハーバ」位置点,App 异常退出。
- 点击其他普通地图物体,未观察到同样问题。
- 打开「+键盘」(十字光标)后,将光标移动到该港湾位置,查询/选取正常,不退出。
因此该问题优先定位 地图直接 Tap 的原生可点击 Source 命中/图层排序链路,而不是仅根据地点名称推断港湾数据损坏、JS 详情渲染异常或粒子 GPU 绘制故障。
2. 实际环境与 Git 事实
| 项目 | 本次调查事实 |
|---|---|
| iOS 设备 | Pleiades,iPad mini(第 5 代,iPad11,2) |
| iOS 崩溃日志声明的系统 | iPhone OS 26.5.2 (23F84) |
| App | NavSea,bundleID com.navsea.app |
| App 调查工作树 | /Users/tei/dev/NavSea-release-v22 |
| App 调查 HEAD | 4bfe5257(release/particle-v22-android-debug-20261009) |
| Particle 本次核对仓库 | /Users/tei/dev/MapLibreParticleLab,particle/main |
| Particle 调查起点 HEAD | 6df21dac38f072c5ad35b8dfc7ec5217509602bc |
| 崩溃时间 | 2026-10-09 20:22:40 +0900 |
| 崩溃日志 | 设备端 NavSea-2026-10-09-202240.ips(调查时只读复制到 Mac /tmp/) |
设备还存在多份同日晚间 NavSea 崩溃报告,本次详细分析的证据为上述 20:22:40 报告。请不要把其余报告自动认定为相同堆栈,除非逐份核查。
Particle 工作区在调查前已有其他未跟踪文档,均不属于本问题,不得顺手删除、覆盖或纳入本次提交。
3. 崩溃证据(直接来自 iPad .ips)
exception: EXC_CRASH / SIGABRT
exceptionReason.name: NSInvalidArgumentException
exceptionReason.composed_message:
*** -[__NSArrayM insertObject:atIndex:]: object cannot be nil
faultingThread: 0 (com.apple.main-thread)
-[MLRNMapView didTapMap:]
-> -[MLRNMapView handleTapAtPoint:]
-> -[MLRNMapView getTouchableSourceWithHighestZIndex:]
-> -[MLNStyle layers]
-> -[__NSArrayM insertObject:atIndex:]
-> NSInvalidArgumentException / abort()
上述调用栈直接证明 崩溃发生在地图原生 Tap 处理访问 Style Layer 列表时,且 Objective-C 数组插入了 nil。它并不证明具体是哪一个 Style Layer 的 Peer 为 nil;这一点需要后续补充定位日志或调试证据。
4. 已核实的对应代码
4.1 Darwin Style 列表 getter:空对象缺少防护
文件:platform/darwin/src/MLNStyle.mm,调查时约第 285–293 行。
- (NSArray<__kindof MLNStyleLayer *> *)layers {
auto layers = self.rawStyle->getLayers();
NSMutableArray<__kindof MLNStyleLayer *> *styleLayers =
[NSMutableArray arrayWithCapacity:layers.size()];
for (auto layer : layers) {
MLNStyleLayer *styleLayer = [self layerFromMBGLLayer:layer];
[styleLayers addObject:styleLayer];
}
return styleLayers;
}
layerFromMBGLLayer: 位于同一文件约第 376 行。该方法在已有 Peer 缺失时调用 LayerManagerDarwin::createPeer(rawLayer);若工厂无法产生 Objective-C Peer,返回值可能为 nil,随后无条件 addObject 会直接触发本次异常。
4.2 部分 Particle/Weather Layer 仅注册为 CoreOnly
文件:platform/darwin/src/MLNStyleLayerManager.mm。
addLayerTypeCoreOnly(std::make_unique<ParticleValidationLayerFactory>());
addLayerTypeCoreOnly(std::make_unique<RasterParticleLayerFactory>());
addLayerTypeCoreOnly(std::make_unique<WeatherColorLayerFactory>());
同文件的 LayerManagerDarwin::createPeer:当 getPeerFactory(layer->getTypeInfo()) 不存在时返回 nullptr。因此以上 CoreOnly Layer 是造成空 Peer 的高优先级怀疑对象,但需要定位日志确认实际 layer id/type,不能将具体类型预写为已证实。
4.3 RN MapView:多 Source 命中触发列表遍历
NavSea 当前安装依赖源码:
node_modules/@maplibre/maplibre-react-native/ios/components/map-view/MLRNMapView.m。
handleTapAtPoint:查询所有可点击 Source 的命中;存在命中时调用getTouchableSourceWithHighestZIndex:。getTouchableSourceWithHighestZIndex:在touchableSources.count == 1时直接返回唯一 Source。- 在 同时命中两个或更多可点击 Source 时,方法读取
self.style.layers并逆序遍历,决定顶部 Source;因此会进入第 4.1 节的危险路径。
崩溃堆栈已进入 getTouchableSourceWithHighestZIndex: 的 self.style.layers,符合多 Source 命中逻辑。需要现场记录所命中的具体 Source / layer IDs,以确认唐津位置的重叠对象组成。
4.4 NavSea 点击与十字光标是不同路径
- 直接点击地图:
src/map/MapScreen.tsx的MapLibreMap onPress,以及src/map/layers/MooringSource.tsx的GeoJSONSource onPress。 - 港湾点直点 Host 回调:
src/map/cursor/crossCursorEngine.ts的handleMooringPress。 - 「+键盘」选取:
crossCursorEngine.ts中的queryRenderedFeaturesAtPointCompat/queryRenderedFeaturesInRectCompat查询。
本次异常在原生触摸处理阶段已发生;即使 JS 侧在 onPress 增加 try/catch,也无法覆盖此 Objective-C SIGABRT。
5. 修复方向(待 Particle iOS 实施者决定)
- 首先精确定位:在不改变业务和地图 Style 行为的前提下,记录
layerFromMBGLLayer:转换失败的 raw layer id/type、Peer Factory 类型及命中 Source / Layer 列表,确认 CoreOnly Layer 是否为实际来源。 - 修复 Darwin 公共访问契约:
MLNStyle.layers不得把nil加入NSMutableArray;优先设计与现有 CoreOnly Layer 共存的稳定方案。可评估安全过滤不可桥接 Layer,或补齐对应的 Peer/排序能力,但不可仅凭猜测引入无意义的 ObjC wrapper。 - 保持集合访问一致性:若采取过滤不可桥接 Layer 的方式,须同时检查
countOfLayers、objectInLayersAtIndex:、getLayers:range:、KVC/KVO 以及插入/移除行为,避免 getter 屏蔽后出现数量/索引语义不一致或再次写入nil。 - 保持点击 z-order 正确性:RN 的多 Source 命中不能因为跳过 CoreOnly Layer 而改变真实可点击 Layer 的前后顺序;必要时直接在 Native 查询层顺序而不是依赖不完整 ObjC Peer 列表。
- 限定修改范围:优先在 Particle iOS Native / MapLibre 层完成,不通过 NavSea JS 屏蔽唐津地点、删除天气层或建立私有桥接来回避崩溃。若确实需要改 RN 公共接口或跨项目契约,应返回 Integration 项目协调。
切勿直接删除 Particle、WeatherColor 或 Validation 的 Core Layer 来“消除异常”;它们是正式渲染能力。
6. 修复后必须验证的场景
| 场景 | 期望 |
|---|---|
| 同位置只命中一个可点击 Source | 正常选取,无崩溃 |
| 两个及以上可点击 Source 重叠(重点复现) | 正确选中 z-order 最高且有效的 Source,无崩溃 |
| 唐津ヨット ハーバ 直接点击 | 不退出,港湾选取/摘要功能正常 |
| 「+键盘」移动到同一港湾 | 行为与修复前保持一致 |
| 其他海图对象直接点击 | 继续正常,无点击回归 |
| Style 含 RasterParticleLayer / WeatherColorLayer / ParticleValidationLayer 组合 | Style introspection 稳定,不向数组插入 nil |
| 天气/粒子图层启用与关闭后重复点击 | 无崩溃、无源层级混乱、无隐藏的点击遮挡 |
layers getter、KVC 索引 getter、层增删 |
不出现 nil/越界,计数与索引契约保持一致 |
最少应使用 Mac 构建出的实际 iOS Native 二进制 + iPad 真机 做点击复现/回归。仅能编译或仅有单元测试通过,不等于用户场景已修复。
7. 责任、验收与完成条件
- Particle iOS:调查具体 nil layer、修复 Darwin Style API/多 Source 点击兼容、实施 iOS 构建与真机验证。
- Particle Shared Core:仅在确认是通用 Layer/Peer 协议缺陷时参与;不得未经证据改变 Android 行为。
- NavSea:在收到经过验证的新 iOS 发行后进行最小消费端验收;业务层不承担原生崩溃掩盖责任。
- Integration 项目:若涉及公共 API、跨平台协议、消费版本协调,则在现有 COORD/RFC/ADR/Contract/Status 框架内正式记录。
修复完成报告应写明:引发空 Peer 的真实 layer id/type、实际改动文件、验证命令及结果、真机版本和复现结果、Git commit/分支,以及正式发行时遵照 GSRL 的 Source Lock/获取方式/校验材料。本调查报告本身不构成新发行,也不授权覆盖现有版本。
8. 当前未完成事项
- 找到 iPad NavSea 原生崩溃日志,确认主线程异常及调用栈
- 核对 Particle
MLNStyle.mm、MLNStyleLayerManager.mm与 RNMLRNMapView.m源码 - 说明直接点击与十字光标查询的不同调用路径
- 获取本次返回
nil的具体 Style Layer id/type - 编写并执行 Native 修复及目标回归
- 重新构建 iOS、在实际 iPad 复现原场景并确认不再退出
- 按 GSRL 验证新版本消费与正式发行(如进入发行)
结论:高可信度机制为 Native Style CoreOnly/缺失 Peer 与 RN 多 Source hit-test 排序不兼容;异常位置已由 .ips 确认,具体 nil Layer 身份与最终修复仍待 Particle iOS 验证。