From 016bf5ad93e0fdd625034b29fedd150ea6af3943 Mon Sep 17 00:00:00 2001 From: tei Date: Fri, 9 Oct 2026 21:46:59 +0900 Subject: [PATCH] fix(ios): guard Darwin style CoreOnly layer peer access --- ...STEP-107_iOS_StyleCoreOnlyPeer集合修复_v1.md | 41 ++++++ ..._StyleCoreOnlyPeer集合修复_RDC实施报告_v1.md | 42 +++++++ platform/darwin/src/MLNStyle.mm | 117 ++++++++---------- platform/darwin/test/MLNStyleTests.mm | 71 +++++++++++ 粒子开发记录.md | 8 ++ 5 files changed, 212 insertions(+), 67 deletions(-) create mode 100644 docs/50_Step记录/STEP-107_iOS_StyleCoreOnlyPeer集合修复_v1.md create mode 100644 docs/报告/STEP-107_iOS_StyleCoreOnlyPeer集合修复_RDC实施报告_v1.md diff --git a/docs/50_Step记录/STEP-107_iOS_StyleCoreOnlyPeer集合修复_v1.md b/docs/50_Step记录/STEP-107_iOS_StyleCoreOnlyPeer集合修复_v1.md new file mode 100644 index 000000000..0a4c0fe2a --- /dev/null +++ b/docs/50_Step记录/STEP-107_iOS_StyleCoreOnlyPeer集合修复_v1.md @@ -0,0 +1,41 @@ +# STEP-107 iOS Style CoreOnly Peer 集合一致性修复 v1 + +- 日期:2026-10-09(JST) +- 状态:**进行中 / iOS Native 构建 PASS / XCTest 与 iPad 实机回归未执行** +- 机器:Mac.lan;源码:`/Users/tei/dev/MapLibreParticleLab`;分支:`particle/main` +- 开工 HEAD:`24105b1be7a2c8bd3a547117a30887fbaa0a308b` +- 对应调查:`docs/20_调查/iOS地图点击重叠Source触发MLNStyle空Peer崩溃调查_v1.md` +- 涉及的已正式发行基线:`maplibre-particle/rn/13.6.1-particle.22`(不修改原 Tag / Source Lock)。 + +## 目标与边界 + +消除 Darwin `MLNStyle.layers` 因 CoreOnly Layer 没有 ObjC Peer 而插入 nil 引发的 NSInvalidArgumentException;统一 `layers`、KVC 数量/索引/区间访问与增删位置语义,保持 C++ 原始 Style 的 CoreOnly 渲染能力以及可点击 ObjC 层的可见 Z-order。 + +只允许修复 Darwin `MLNStyle.mm`、对应 iOS XCTest、Step 与报告;不得修改 NavSea、RN 11.5.0 依赖、Android、Shared Core、粒子 Shader/Renderer、V22 已发布内容或已有工作树。不得新建 worktree,禁用 Jest;不把编译通过等同 iPad 修复验收。 + +## 本轮实施 + +- [x] 检查实际 Native `MLNStyle.layers` 中未判空的 `addObject:`,及 `LayerManagerDarwin::createPeer` 对 CoreOnly 类型返回空的现状。 +- [x] getter 仅暴露有效 ObjC Peer,跳过无 Peer CoreOnly Layer;DEBUG 日志包含不可桥接 Layer ID/类型,以便后续现场确认触发者。 +- [x] `countOfLayers`、`objectInLayersAtIndex:`、`getLayers:range:` 统一使用 ObjC 可见集;修正原区间 getter 的反向遍历顺序。 +- [x] ObjC 可见索引用于 `insertObject:inLayersAtIndex:`、`removeObjectFromLayersAtIndex:`;`insertLayer:aboveLayer:` 使用下一个可见 sibling,防止隐藏 CoreOnly 层造成 nil。 +- [x] 补充 `MLNStyleTests.mm` 定向 XCTest,构造 Native-only Layer 与普通 Layer 混合、索引/顺序/增删场景。 +- [x] `git diff --check` PASS。 +- [x] RDC 在 Mac 执行 `bazel build //platform/ios:MapLibre.dynamic --color=no --curses=no --show_progress_rate_limit=15`,exit 0,Build completed successfully(982.084 秒)。 +- [x] XCFramework ZIP 存在、`unzip -tqq` PASS,SHA-256:`f16b7f2be761a101bb4b906b900175b95412abfeef09d715e2c9321b300a7b8a`。 +- [ ] 运行新增 `MLNStyleTests` XCTest,包括 KVC 索引与 CoreOnly 层回归。 +- [ ] 使用实际 iPad 和此修复构建的 Native 验证「唐津ヨット ハーバ」直接点击、多 Source 重叠、十字光标、天气开关及粒子渲染。 +- [ ] 从真实日志确认当次缺失 Peer 的具体 Layer ID/type;若原因超出 CoreOnly 假设,应补充分析。 +- [ ] 如进入正式发行,按 GSRL 创建**新版本**、独立构建与 Source Lock,并做公网校验;不得改写 V22。 + +## 事实与遗留 + +新增 XCTest 源码已编写,但本轮 Bazel XCFramework 构建目标不包含 Darwin XCTest,故不能声称测试 PASS;实际 iPad 未测试。编译日志路径:`/tmp/maplibre_v22_ios_style_fix_bazel_20261009.log`。编译警告仅见无效搜索路径 `/opt/homebrew/lib`,不影响构建。构建产物为未发行验证 ZIP,不是 V22 原版或正式新版本。 + +原本就存在的 `vendor/expected-lite` 子模块本地状态与两份未跟踪文档必须保持原状。 + +## 下一步 + +由 Particle-iOS 在 Mac 真正执行 Darwin XCTest,必要时改正测试编译接口,再由用户将包含本次修复的 iOS Native 安装到 iPad 重放现场;上述验收未完成前仅按 PARTIAL / 待实机验收处理。不得转 NavSea JS workaround 或修改跨平台业务契约。 + +报告:`docs/报告/STEP-107_iOS_StyleCoreOnlyPeer集合修复_RDC实施报告_v1.md`。 diff --git a/docs/报告/STEP-107_iOS_StyleCoreOnlyPeer集合修复_RDC实施报告_v1.md b/docs/报告/STEP-107_iOS_StyleCoreOnlyPeer集合修复_RDC实施报告_v1.md new file mode 100644 index 000000000..6cfbda80c --- /dev/null +++ b/docs/报告/STEP-107_iOS_StyleCoreOnlyPeer集合修复_RDC实施报告_v1.md @@ -0,0 +1,42 @@ +# STEP-107 iOS Style CoreOnly Peer 集合修复 — RDC 实施报告 v1 + +## 1. 执行结论 + +**PARTIAL(代码已实施,iOS Native 构建 PASS,XCTest 与 iPad 事故场景 NOT RUN)**。 + +2026-10-09,直接在 Mac `/Users/tei/dev/MapLibreParticleLab` 的 `particle/main` 执行;没有创建 worktree、没有运行 Codex;未触碰 NavSea / Android / Shared Core / 任何正式发行 Tag。实际仍未获知崩溃现场的具体 nil Layer id/type。 + +## 2. Git 基线与修改范围 + +- 开工 HEAD:`24105b1be7a2c8bd3a547117a30887fbaa0a308b`,对应远端 `particle/main`。 +- 修改:`platform/darwin/src/MLNStyle.mm` 与 `platform/darwin/test/MLNStyleTests.mm`。 +- 新增:本报告、`docs/50_Step记录/STEP-107_iOS_StyleCoreOnlyPeer集合修复_v1.md`,追加 `粒子开发记录.md`。 +- 与本任务无关的 `vendor/expected-lite` 子模块状态及原有两份未跟踪文档均保留。 +- 未运行 Codex,因此 Codex 模型/推理档位及 Codex 完成事件不适用。 +- 提交/PUSH 与远端校验以本报告后续 Git 核验补录为准。 + +## 3. 根因与实现 + +在 `MLNStyle.mm`,原 `layers` getter 对 `layerFromMBGLLayer:` 的 nil 返回直接调用 `addObject:`,遇到 CoreOnly Layer 则触发 Objective-C `NSInvalidArgumentException`。ParticleValidation、RasterParticle、WeatherColor 在 Darwin `MLNStyleLayerManager.mm` 仅注册 Core Factory,不具备 ObjC Peer。 + +修复后的 ObjC `layers` 只暴露有 Peer 的普通层,保留 C++ Style 内全部 CoreOnly Layer;缺失 Peer 时记录 DEBUG ID/type。KVC `countOfLayers`、单索引、range getter、插入/删除索引,以及 `aboveLayer:` 均按同一 ObjC 可见顺序处理。保持原始 C++ Style 与实际渲染的 Layer 存在及次序,不借由移除 Particle/Weather 来绕过问题。 + +新增 XCTest 用例通过 source-less `ParticleValidationLayer` 模拟夹在两个普通 Circle Layer 中间、以及位于顶部的 CoreOnly 层,验证 getter、KVC、区间读取、层标识、above/atIndex 插入和删除后 Native 层仍在。此 XCTest 尚未执行。 + +## 4. 验证记录 + +- `git diff --check`:PASS。 +- Mac Xcode 26.3 / Bazel 8.6.0: + `bazel build //platform/ios:MapLibre.dynamic --color=no --curses=no --show_progress_rate_limit=15`:**PASS**,exit 0,耗时 982.084 秒,691 actions。 +- 构建日志:`/tmp/maplibre_v22_ios_style_fix_bazel_20261009.log`。 +- 新 XCFramework ZIP:`bazel-bin/platform/ios/MapLibre.dynamic.xcframework.zip`,SHA-256 `f16b7f2be761a101bb4b906b900175b95412abfeef09d715e2c9321b300a7b8a`;`unzip -tqq` PASS。 +- 编译警告:链接器提示 `/opt/homebrew/lib` 不存在,但整个 Build PASS。 +- `MLNStyleTests.mm` XCTest:**NOT RUN**,Bazel XCFramework 目标不包含测试文件。 +- iOS Test Host / iPad / 唐津原始点击 / 双 Source Z-order / 天气粒子组合:**NOT RUN**。 +- Android、Gradle、ADB、Jest、NavSea、正式 Release/Tag/Source Lock:**NOT RUN**。 + +## 5. 阻塞与验收边界 + +Native 编译 PASS 不代表原始崩溃完全修复。需运行 XCTest 并用包含本次改动的新 Native 二进制真机重放:普通对象、唐津港湾直接点击、两个以上可点击 Source、十字光标、天气粒子 Layer 开关和 Style 变更。需核实 DEBUG 输出实际无 Peer Layer 的 id/type,不应将 CoreOnly 推断当作唯一已证实源。 + +原 V22 正式 Tag 和分发 ZIP 未修改,当前构建仅作为本地未发行验证制品。任何正式版本迭代需新 Tag / Source Lock / SHA-256 / 远端校验,按 GSRL 执行。 diff --git a/platform/darwin/src/MLNStyle.mm b/platform/darwin/src/MLNStyle.mm index 224e413fa..02322da27 100644 --- a/platform/darwin/src/MLNStyle.mm +++ b/platform/darwin/src/MLNStyle.mm @@ -283,12 +283,20 @@ const MLNExceptionName MLNRedundantSourceIdentifierException = // MARK: Style layers - (NSArray<__kindof MLNStyleLayer *> *)layers { - auto layers = self.rawStyle->getLayers(); + // Core-only layers (including Particle and WeatherColor) are intentionally + // not exposed as Objective-C peers. Keep them in the native Style for + // rendering, but never insert a missing peer into an NSArray. + auto rawLayers = self.rawStyle->getLayers(); NSMutableArray<__kindof MLNStyleLayer *> *styleLayers = - [NSMutableArray arrayWithCapacity:layers.size()]; - for (auto layer : layers) { - MLNStyleLayer *styleLayer = [self layerFromMBGLLayer:layer]; - [styleLayers addObject:styleLayer]; + [NSMutableArray arrayWithCapacity:rawLayers.size()]; + for (auto rawLayer : rawLayers) { + MLNStyleLayer *styleLayer = [self layerFromMBGLLayer:rawLayer]; + if (styleLayer) { + [styleLayers addObject:styleLayer]; + } else { + MLNLogDebug(@"Skipping native-only style layer id=%s type=%s (no ObjC peer)", + rawLayer->getID().c_str(), rawLayer->getTypeInfo()->type); + } } return styleLayers; } @@ -304,30 +312,30 @@ const MLNExceptionName MLNRedundantSourceIdentifierException = } - (NSUInteger)countOfLayers { - return self.rawStyle->getLayers().size(); + // KVC indices count the same ObjC-visible layers as the public getter. + return self.layers.count; } - (MLNStyleLayer *)objectInLayersAtIndex:(NSUInteger)index { - auto layers = self.rawStyle->getLayers(); - if (index >= layers.size()) { + NSArray *visibleLayers = self.layers; + if (index >= visibleLayers.count) { [NSException raise:NSRangeException format:@"No style layer at index %lu.", (unsigned long)index]; return nil; } - auto layer = layers.at(index); - return [self layerFromMBGLLayer:layer]; + return visibleLayers[index]; } - (void)getLayers:(MLNStyleLayer **)buffer range:(NSRange)inRange { - auto layers = self.rawStyle->getLayers(); - if (NSMaxRange(inRange) > layers.size()) { + NSArray *visibleLayers = self.layers; + if (inRange.location > visibleLayers.count || + inRange.length > visibleLayers.count - inRange.location) { [NSException raise:NSRangeException format:@"Style layer range %@ is out of bounds.", NSStringFromRange(inRange)]; + return; } - NSUInteger i = 0; - for (auto layer = *(layers.rbegin() + inRange.location); i < inRange.length; ++layer, ++i) { - MLNStyleLayer *styleLayer = [self layerFromMBGLLayer:layer]; - buffer[i] = styleLayer; + for (NSUInteger i = 0; i < inRange.length; ++i) { + buffer[i] = visibleLayers[inRange.location + i]; } } @@ -339,37 +347,26 @@ const MLNExceptionName MLNRedundantSourceIdentifierException = @"of MLNStyleLayer.", styleLayer]; } - auto layers = self.rawStyle->getLayers(); - if (index > layers.size()) { + NSArray *visibleLayers = self.layers; + if (index > visibleLayers.count) { [NSException raise:NSRangeException format:@"Cannot insert style layer at out-of-bounds index %lu.", (unsigned long)index]; - } else if (index == 0) { - try { - MLNStyleLayer *sibling = layers.size() ? [self layerFromMBGLLayer:layers.at(0)] : nil; - [styleLayer addToStyle:self belowLayer:sibling]; - } catch (const std::runtime_error &err) { - [NSException raise:MLNRedundantLayerIdentifierException format:@"%s", err.what()]; - } - } else { - try { - MLNStyleLayer *sibling = [self layerFromMBGLLayer:layers.at(index)]; - [styleLayer addToStyle:self belowLayer:sibling]; - } catch (std::runtime_error &err) { - [NSException raise:MLNRedundantLayerIdentifierException format:@"%s", err.what()]; - } + return; + } + + // Insert before the next visible layer, or above the entire native stack. + // Indices exposed to KVC must not count hidden core-only layers. + MLNStyleLayer *sibling = index < visibleLayers.count ? visibleLayers[index] : nil; + try { + [styleLayer addToStyle:self belowLayer:sibling]; + } catch (const std::runtime_error &err) { + [NSException raise:MLNRedundantLayerIdentifierException format:@"%s", err.what()]; } } - (void)removeObjectFromLayersAtIndex:(NSUInteger)index { - auto layers = self.rawStyle->getLayers(); - if (index >= layers.size()) { - [NSException - raise:NSRangeException - format:@"Cannot remove style layer at out-of-bounds index %lu.", (unsigned long)index]; - } - auto layer = layers.at(index); - MLNStyleLayer *styleLayer = [self layerFromMBGLLayer:layer]; + MLNStyleLayer *styleLayer = [self objectInLayersAtIndex:index]; [styleLayer removeFromStyle:self]; } @@ -466,36 +463,22 @@ const MLNExceptionName MLNRedundantSourceIdentifierException = sibling]; } - auto layers = self.rawStyle->getLayers(); - std::string siblingIdentifier = sibling.identifier.UTF8String; - NSUInteger index = 0; - for (auto siblingLayer : layers) { - if (siblingLayer->getID() == siblingIdentifier) { - break; - } - index++; + // A hidden core-only layer may immediately follow sibling in the native + // stack. Find the next *visible* sibling to avoid passing a nil ObjC peer + // to addToStyle:belowLayer:. + NSArray *visibleLayers = self.layers; + NSUInteger siblingIndex = [visibleLayers + indexOfObjectPassingTest:^BOOL(MLNStyleLayer *candidate, NSUInteger idx, BOOL *stop) { + return [candidate.identifier isEqualToString:sibling.identifier]; + }]; + if (siblingIndex == NSNotFound) { + [NSException raise:NSInvalidArgumentException + format:@"The sibling layer %@ is not present in this style.", sibling]; + return; } [self willChangeValueForKey:@"layers"]; - if (index + 1 > layers.size()) { - [NSException raise:NSInvalidArgumentException - format:@"A style layer cannot be placed above %@ in the style. " - @"Make sure sibling was obtained using -[MLNStyle layerWithIdentifier:].", - sibling]; - } else if (index + 1 == layers.size()) { - try { - [layer addToStyle:self belowLayer:nil]; - } catch (std::runtime_error &err) { - [NSException raise:MLNRedundantLayerIdentifierException format:@"%s", err.what()]; - } - } else { - MLNStyleLayer *nextSibling = [self layerFromMBGLLayer:layers.at(index + 1)]; - try { - [layer addToStyle:self belowLayer:nextSibling]; - } catch (std::runtime_error &err) { - [NSException raise:MLNRedundantLayerIdentifierException format:@"%s", err.what()]; - } - } + [self insertObject:layer inLayersAtIndex:siblingIndex + 1]; [self didChangeValueForKey:@"layers"]; } diff --git a/platform/darwin/test/MLNStyleTests.mm b/platform/darwin/test/MLNStyleTests.mm index 6733222f8..27f3d6060 100644 --- a/platform/darwin/test/MLNStyleTests.mm +++ b/platform/darwin/test/MLNStyleTests.mm @@ -1,6 +1,13 @@ #import +#import "MLNStyle_Private.h" #import "MLNVectorTileSource_Private.h" + +#include +#include + +#include +#include #import "NSBundle+MLNAdditions.h" #import @@ -238,6 +245,70 @@ XCTAssertEqual(self.style.layers.count, initialLayers.count); } +// Native-only layers must remain renderable while ObjC/KVC accessors expose +// only layers for which Darwin can construct a valid MLNStyleLayer peer. +- (void)testCoreOnlyLayersPreserveObjCCollectionAndOrdering { + NSUInteger initialCount = self.style.layers.count; + NSUInteger initialNativeCount = self.style.rawStyle->getLayers().size(); + + MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"core-only-test-source" + shape:nil + options:nil]; + [self.style addSource:source]; + + MLNCircleStyleLayer *lower = [[MLNCircleStyleLayer alloc] + initWithIdentifier:@"core-only-visible-lower" source:source]; + MLNCircleStyleLayer *upper = [[MLNCircleStyleLayer alloc] + initWithIdentifier:@"core-only-visible-upper" source:source]; + [self.style addLayer:lower]; + [self.style addLayer:upper]; + + // Mix hidden core layers between and above the public ObjC layers. + self.style.rawStyle->addLayer( + std::make_unique("core-only-middle"), + std::string("core-only-visible-upper")); + self.style.rawStyle->addLayer( + std::make_unique("core-only-top")); + + NSArray *visible = self.style.layers; + XCTAssertEqual(visible.count, initialCount + 2); + XCTAssertEqual([self.style countOfLayers], initialCount + 2); + XCTAssertEqual(self.style.rawStyle->getLayers().size(), initialNativeCount + 4); + XCTAssertEqualObjects(visible[initialCount].identifier, lower.identifier); + XCTAssertEqualObjects(visible[initialCount + 1].identifier, upper.identifier); + XCTAssertNil([self.style layerWithIdentifier:@"core-only-middle"]); + XCTAssertNotNil(self.style.rawStyle->getLayer("core-only-middle")); + XCTAssertNotNil(self.style.rawStyle->getLayer("core-only-top")); + + id kvcStyle = self.style; + XCTAssertEqualObjects([kvcStyle objectInLayersAtIndex:initialCount], lower); + MLNStyleLayer *__unsafe_unretained rangeLayers[2] = {nil, nil}; + [kvcStyle getLayers:rangeLayers range:NSMakeRange(initialCount, 2)]; + XCTAssertEqualObjects(rangeLayers[0], lower); + XCTAssertEqualObjects(rangeLayers[1], upper); + XCTAssertThrowsSpecificNamed([kvcStyle objectInLayersAtIndex:visible.count], NSException, + NSRangeException); + + // The native layer immediately above lower lacks an ObjC peer. Inserting + // above lower must still work and retain the relative visible order. + MLNCircleStyleLayer *above = [[MLNCircleStyleLayer alloc] + initWithIdentifier:@"core-only-visible-above" source:source]; + [self.style insertLayer:above aboveLayer:lower]; + XCTAssertEqualObjects(self.style.layers[initialCount + 1], above); + XCTAssertEqualObjects(self.style.layers[initialCount + 2], upper); + + // KVC's index applies to public peers, not the internal native stack. + MLNCircleStyleLayer *inserted = [[MLNCircleStyleLayer alloc] + initWithIdentifier:@"core-only-visible-inserted" source:source]; + [self.style insertLayer:inserted atIndex:initialCount + 1]; + XCTAssertEqualObjects(self.style.layers[initialCount + 1], inserted); + [kvcStyle removeObjectFromLayersAtIndex:initialCount + 1]; + XCTAssertNil([self.style layerWithIdentifier:inserted.identifier]); + XCTAssertEqualObjects(self.style.layers[initialCount + 1], above); + XCTAssertNotNil(self.style.rawStyle->getLayer("core-only-middle")); + XCTAssertNotNil(self.style.rawStyle->getLayer("core-only-top")); +} + - (void)testAddingLayersTwice { MLNShapeSource *source = [[MLNShapeSource alloc] initWithIdentifier:@"shapeSource" shape:nil diff --git a/粒子开发记录.md b/粒子开发记录.md index f4bec52c5..d1739ef29 100644 --- a/粒子开发记录.md +++ b/粒子开发记录.md @@ -1081,3 +1081,11 @@ Android V3-08 单次综合验收已完成并关闭。Native `13.4.1-particle.2` - Metal `bazel build -c opt --//:renderer=metal //:mbgl-core` 构建 PASS(642/642,875.118s,产物 `bazel-bin/libmbgl-core.a`)。 - Mac CMake/GTest 配置遇缺失 `libuv`,未安装依赖,正式 GTest NOT RUN。 - 修复 STEP-102 Android Validation 入口切换时残留 STEP-093/097 状态与轮询的问题。Android APK 编译、T1/T2 真机及 STEP-100/101 回归仍待手工构建与运行;STEP-102 维持“进行中”,尚非 CLOSED/PASS。 + + +### 2026-10-09 STEP-107 iOS CoreOnly Style Peer 崩溃修复(Native BUILD PASS,iPad 待验收) + +- 基于远端最新 `particle/main` 的 V22 后 HEAD `24105b1be7`,在唯一 Mac 主目录直接修复 Darwin `MLNStyle.layers` 对 CoreOnly 无 Peer Layer 的 nil 插入问题;补齐 KVC 数量/索引/区间、插入/删除与 aboveLayer ObjC 可见顺序语义,Native 粒子天气渲染层保持不变。 +- 新增 `MLNStyleTests.mm` 混合 CoreOnly/普通 Layer 的集合及顺序回归用例;`git diff --check` PASS。Mac Bazel iOS Native `//platform/ios:MapLibre.dynamic` PASS,982.084 秒,ZIP 校验 PASS,SHA-256 `f16b7f2be761a101bb4b906b900175b95412abfeef09d715e2c9321b300a7b8a`。 +- XCTest 尚未执行;iPad 唐津海图物体直接点击、多 Source、天气开关及粒子渲染尚未真实验收;不能宣称 Bug 已在设备上消失。未修改 V22 Tag、正式发行资料、NavSea、Android 或 Shared Core。 +- Step:`docs/50_Step记录/STEP-107_iOS_StyleCoreOnlyPeer集合修复_v1.md`;报告:`docs/报告/STEP-107_iOS_StyleCoreOnlyPeer集合修复_RDC实施报告_v1.md`。