Skip to content

fix(sdk): single-owner App/Page visibility with ready-gated delivery - #313

Draft
lbb00 wants to merge 3 commits into
didi:mainfrom
EchoTechFE:fix/unify-cross-mini-lifecycle
Draft

fix(sdk): single-owner App/Page visibility with ready-gated delivery#313
lbb00 wants to merge 3 commits into
didi:mainfrom
EchoTechFE:fix/unify-cross-mini-lifecycle

Conversation

@lbb00

@lbb00 lbb00 commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

这个 PR 在统一什么

小程序 App/Page 两级可见性只应该有一个 owner:所有可见性来源走同一个派发入口,接收方尚未就绪时保留意图而不是丢弃它。

跨小程序切换只是这套语义的一个子场景,不是它存在的理由。真正要统一的来源有四类:系统前后台(Home 键)、同一小程序内的路由、跨小程序切换、以及启动尚未完成的窗口期。

为什么改

页面与 App 的可见性此前存在多个 owner,导致生命周期错序、丢失,或在资源就绪后迟到重放:

  • iOS/HarmonyOS 的返回、退出和启动失败回滚会绕过 navigator 的页面调度;
  • Android 把 App 生命周期绑定到单个 Activity 的 onResume/onPause,同一小程序页面切换或系统弹窗可能误报前后台,跨小程序时还会受 Activity 回调穿插影响;
  • Web 在 render 尚未 ready 时可能先发 AppHide,之后再补一个孤立的 PageHide
  • iOS/HarmonyOS 的系统级前后台此前只派发 App 级 onAppShow/onAppHide,当前页面收不到 Page.onShow/onHide。这是对齐微信官方 App/Page 生命周期标准语义——App 级可见性变化本就该联动当前页面的可见性事件。

评审这次改动时另外发现的两个回归,一并处理:

  • Android DiminaActivity.onStart/onStop 在绕过 MiniApp.openApp 的重建场景(配置变更、进程死亡恢复)可能先于 JsCore 真正创建就触发,原先的创建式 getJsCore 会在主线程同步解压 + 求值 service.js;
  • HarmonyOS DMPNavigatorManager.dispatchPageShow/Hide 依赖 getCurNavigator(),navigator 栈瞬时为空(如回到栈底)时会静默丢弃 pageHide/pageShow。

怎么改

iOS/HarmonyOS 的普通路由和跨小程序路径统一复用 navigator 的 dispatchPageShow/Hide。Android 将 App 可见性收口到 appId/JsCore 级账本,Page 仍由页面 Activity 管理;跨小程序入口在 owner 交接时同步完成 PageHide → AppHide,恢复时为 AppShow → PageShow,service/Bridge 未 ready 的状态会保留而不是丢弃。Web 同样按实际可见性派发;从未 show 的页面不会补发孤立 PageHide

Android

  • MiniApp.peekJsCore 提供非创建式查询;DiminaActivity.dispatchMiniProgramShow/Hide 改用它,createBridge 后调用新增的 reconcileAppVisibilityWithCore() 补放期间被跳过的可见性意图。
  • Bridge 里裸的 desiredPageVisible/sentPageVisible 抽成 PageVisibilityLedger。它活在 Bridge 里、不在 JsCore:它的就绪判据是"这个页面的 WebView 和 JsCore 两条线是否都已回执",只有同时持有两者引用的 Bridge 能看到这个事实。对应地,JsCore 内部的 AppVisibilityLedger 留在 JsCore 里,因为它的就绪判据(service 端 App 是否已建好)是 JsCore 自己的内部状态。
  • 加固 DiminaActivityBackgroundHookTest,新增 PageVisibilityLedgerTest/AppVisibilityLedgerTest

iOS

  • navigateBack 补上根页面的 dispatchPageShow 与导航栈清空前的 notifyMiniProgramHide
  • DMPAppManager 新增对 UIApplication 前后台通知的订阅,转发给当前 isActiveNavigationOwner() 的小程序的 notifyMiniProgramHide/Show(该方法已同时联动 App+Page 两级派发)。
  • 散落在各路由分支里的裸 pageLifecycle?.onShow/onHide 收敛到 dispatchPageShow/Hide 单一入口。

HarmonyOS

  • DMPNavigatorManager 自持一个无状态的 DMPPageLifecycle 实例,dispatchPageShow/Hide 不再依赖 getCurNavigator()。新增本地单测 DMPNavigatorManagerPageDispatch.test.ets 钉住"navigator 栈为空时仍派发";为了在不构造真 DMPApp(构造即拉起真实 DMPService/Worker)的前提下观察派发,抽出 DMPPageLifecycleLike 接口并允许构造函数注入,生产调用点仍全部传真实 app。
  • DMPAppLifecycle.onAppShow/onAppHide 改走 notifyMiniProgramShow/Hide(已同时派发 App+Page 两级事件,且保留 app 自身 scene/path/query,不再用裸 bridgeId body 覆盖 getEnterOptionsSync)。
  • DMPApp 的页面可见性不再裸发 ContainerToService({type:'pageHide'/'pageShow'}),改走 navigatorManager.dispatchPageHide/Show

CI

  • ios-tests.ymlpaths 加入 harmony/**,否则纯 Harmony 改动触发不了跨端一致性文本比对测试。

一并修复的历史遗留问题:关闭入口不还原 opener

这组与上面的可见性主线没有因果关系,是评审和真机验证过程中发现的独立缺陷,机制是"UI 入口接到了错误的函数"。判据也不同(看 opener 收到的 scene/referrerInfo),可以独立验证。

  • Android:胶囊「×」与菜单「关闭小程序」直接调用 closeMiniProgram(),改走 exitMiniProgram()closeMiniProgram() 只是"finish 掉该 appId 全部 Activity"的原语,既不给 opener 排 scene 1038 的返回载荷,也不走 pageHide 先于 appHide 的挂起顺序;把 UI 入口直接接到它上面,就丢掉了 navigateBackMiniProgram 免费获得的 opener 还原。没有 opener(或 opener 已死)时 queueOpenerReturn 返回 false 且无任何副作用,exitMiniProgram 退化成与 closeMiniProgram 逐字一致的行为,普通单小程序场景不受影响。测试侧新增用例钉住"UI 入口必须走 exitMiniProgram"以及"closeMiniProgram 全文件只能被 navigateBackMiniProgram/exitMiniProgram 调用"。
  • HarmonyOSDMPPageContainer.closeMiniProgramDMPCapsuleButton 的关闭默认分支同样统一改走 exitMiniProgram
  • iOSDMPNavigator.launch / DMPApp.openPage 改为 @discardableResult ... -> Bool,跨小程序回滚路径据此判断真实成败——此前启动失败无法向上传导,回滚拿到假的"成功",会把 opener 卡在没有返回路径的状态。新增内部测试专用 seam markOpenedByMiniProgramForTesting,配合 DMPNavigatorCapsuleTests 走真实 DMPAppManager 路径覆盖 opener 还原。

验证

单测

命令 结果
Android ./gradlew :dimina:testDebugUnitTest --rerun-tasks(判决读 build/test-results/**/*.xml 31 个 reporter 文件,255 tests / 0 failures / 0 errors / 0 skipped
iOS xcodebuild testdiminaTests 48/48
HarmonyOS hvigorw -p module=dimina@default test(判决读 .test/default/intermediates/test/coverage_data/test_result.txt失败时 hvigorw 仍 exit 0 Tests run: 189, Failure: 0, Error: 0

Android 新增用例与 HarmonyOS 新增用例都做过变异验证:分别还原胶囊调用、还原菜单调用、给两个 dispatch 加回 isEmpty() 早返回,三次都如期转红。

真机/模拟器

跨小程序 opener 还原在 Android 真机、iOS 模拟器、HarmonyOS 模拟器上各跑了两轮(wx.navigateBackMiniProgram 与原生胶囊「×」两条返回通道),判据取 opener 侧 App.onShow 收到的 scenereferrerInfo,以及 target 的 pageHide → appHide 是否先于 opener 的 appShow。三端两条通道均为 scene 1038 + referrerInfo.appId,顺序正确。

Android 胶囊「×」与菜单「关闭小程序」的缺陷就是这轮验证发现的:修复前 opener 收到 {"scene":1001}、无 referrerInfo,且 target 的 appHide 迟于 opener 的 appShow;改走 exitMiniProgram() 后两个入口都变为 {"scene":1038,"referrerInfo":{"appId":"..."}} 且顺序修正。referrerInfo 不带 extraData 是预期的——退出没有返回载荷,只有 navigateBackMiniProgram 那条通道才有。同一台设备上还验证了无 opener 的退化路径:干净退回宿主 Activity,无残留、无崩溃,navigateBackMiniProgram 如期返回 fail ... no opener mini program

这部分验证依赖仓库外自建的一对最小小程序与三端驱动脚本,不包含在本 PR 内,因此上游无法直接复现;本 PR 的改动面仅为上述 Android/iOS/HarmonyOS/CI 四项。

仍未覆盖:iOS 侧无触控注入工具,navigateBack(同一小程序内返回)场景无法端到端复现,仍依赖单测。

@lbb00 lbb00 changed the title fix(sdk): unify cross-mini-program lifecycle dispatch fix(sdk): 统一跨小程序生命周期分发 Aug 20, 2026
@lbb00 lbb00 changed the title fix(sdk): 统一跨小程序生命周期分发 fix(sdk): unify cross-mini-program lifecycle dispatch Aug 20, 2026
lbb00 added a commit to EchoTechFE/dimina that referenced this pull request Aug 24, 2026
跟进 PR didi#313 评审发现的问题,覆盖三端:

- Android: DiminaActivity 生命周期回调可能早于 JsCore 就绪触发,
  改用非创建式 peekJsCore + reconcileAppVisibilityWithCore 补放;
  Bridge 内裸 volatile 字段抽成 PageVisibilityLedger。
- HarmonyOS: DMPNavigatorManager 的 page 级派发不再依赖
  navigator 栈非空;App 级系统前后台改走已联动 Page 级事件的入口
  (对齐微信 App/Page 生命周期语义,非跨小程序专属行为);
  胶囊/菜单关闭统一走 exitMiniProgram 以还原 opener。
- iOS: 系统前后台通知补齐 Page 级联动;launch 失败结果向上传导;
  navigateBack 补齐遗漏的 hide/show 派发;补充 opener 还原路径的
  manager 级测试。
- CI: ios-tests.yml 路径过滤补上 harmony/**,使纯 Harmony 改动也
  触发跨端一致性文本比对测试。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
跨小程序导航与系统前后台切换此前在各端派发的 App/Page 生命周期事件不一致:
opener 被 target 覆盖或恢复时,Page 级 onShow/onHide 会丢失或与 App 级事件顺序颠倒。
四端统一为 owner 交接时 PageHide → AppHide、恢复时 AppShow → PageShow。

iOS/HarmonyOS 的普通路由与跨小程序路径统一复用 navigator 的 dispatchPageShow/Hide;
Android 将 App 可见性收口到 appId/JsCore 级账本,Page 仍由页面 Activity 管理,
service/Bridge 未 ready 期间的可见性意图会保留而不是丢弃;Web 同样按实际可见性派发,
从未 show 的页面不会补发孤立 PageHide。

iOS/HarmonyOS 的系统级前后台此前只派发 App 级事件,现联动当前页面的 Page 级事件——
这是对齐微信官方 App/Page 生命周期语义,跨小程序导航只是该语义的一个子场景。

同时修复评审与真机验证中发现的历史遗留问题:
- iOS DMPNavigator.launch 启动失败无法向上传导成败,跨小程序回滚拿到假的"成功"
- iOS navigateBack 弹到根页面/弹空导航栈两个分支缺 hide/show 派发
- HarmonyOS 胶囊按钮与菜单关闭走 closeDimina 直接销毁,跳过 exitMiniProgram 的 opener 还原
- Android 胶囊「×」与菜单「关闭小程序」直接调用 closeMiniProgram,同样丢失 opener 还原
- HarmonyOS DMPNavigatorManager.dispatchPageShow/Hide 在 navigator 栈瞬时为空时静默丢弃事件

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lbb00
lbb00 force-pushed the fix/unify-cross-mini-lifecycle branch from 6990ad3 to bc83146 Compare August 24, 2026 11:23
@lbb00 lbb00 changed the title fix(sdk): unify cross-mini-program lifecycle dispatch fix(sdk): single-owner App/Page visibility with ready-gated delivery Aug 25, 2026
lbb00 and others added 2 commits August 25, 2026 16:32
App-level show/hide raised before a mini program's JS runtime exists was
dispatched into an engine with no App instance yet and dropped entirely, so a
system background arriving inside the cross-mini-program launch window left the
target believing it was still in the foreground. Each platform now separates the
container's intended visibility from what actually reached the service, and
settles the difference once the runtime reports ready.

Android's PageVisibilityLedger also delivers from inside its own monitor, so a
transition raised on the main thread cannot overtake one still in flight from
the JavaBridge thread. iOS additionally treats navigateBack on the entry page as
an exit rather than a route, matching WeChat: unloadPage is driven only by route
events, and closing dispatches App.onHide alone.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
小程序 restart 时容器留在原地,被换掉的只是运行时。之前发给旧 service 的那条终态
App.onHide 走的是普通的容器隐藏路径,于是「容器是否可见」被改成了 false,新运行时
就绪时再按「猜一个可见」重新播种——销毁窗口期内真实发生的系统前后台变化因此丢失:
按 Home 后小程序在后台仍以为自己在前台,回到前台又收不到 App.onShow。

拆成两条线:终态 hide 只推进「已送达」,不动容器意图;运行时被替换不再重播意图。
Harmony 因此要区分「退出后重新展示」(容器本身也变了,意图回到可见),新增
markRuntimeRelaunched 给 restoreRuntimeIfDestroyed 用。

iOS 的就绪点同时挪正:loadBundle 返回只说明 logic.js 刚被投给引擎求值,App 实例
要等 loader 的 modRequire('app') 才建出来,这期间投递的 appHide 会被 service 侧
runtime.appHide() 的 this.app 判空静默丢掉。改成由 serviceResourceLoaded 触发,
并放在容器判空之前,容器缺席时不会吞掉就绪信号。

Android 的账本随 JsCore 生死、初值为未知,结构上不存在这个问题,无需改动。

iOS: diminaTests 56/56、DiminaKitTests 221/221;Harmony: Tests run 198, Failure 0。
新增 7 条用例逐条变异验证,先红后绿且只红对应用例。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant