fix(sdk): single-owner App/Page visibility with ready-gated delivery - #313
Draft
lbb00 wants to merge 3 commits into
Draft
fix(sdk): single-owner App/Page visibility with ready-gated delivery#313lbb00 wants to merge 3 commits into
lbb00 wants to merge 3 commits into
Conversation
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
force-pushed
the
fix/unify-cross-mini-lifecycle
branch
from
August 24, 2026 11:23
6990ad3 to
bc83146
Compare
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
这个 PR 在统一什么
小程序 App/Page 两级可见性只应该有一个 owner:所有可见性来源走同一个派发入口,接收方尚未就绪时保留意图而不是丢弃它。
跨小程序切换只是这套语义的一个子场景,不是它存在的理由。真正要统一的来源有四类:系统前后台(Home 键)、同一小程序内的路由、跨小程序切换、以及启动尚未完成的窗口期。
为什么改
页面与 App 的可见性此前存在多个 owner,导致生命周期错序、丢失,或在资源就绪后迟到重放:
onResume/onPause,同一小程序页面切换或系统弹窗可能误报前后台,跨小程序时还会受 Activity 回调穿插影响;AppHide,之后再补一个孤立的PageHide;onAppShow/onAppHide,当前页面收不到Page.onShow/onHide。这是对齐微信官方 App/Page 生命周期标准语义——App 级可见性变化本就该联动当前页面的可见性事件。评审这次改动时另外发现的两个回归,一并处理:
DiminaActivity.onStart/onStop在绕过MiniApp.openApp的重建场景(配置变更、进程死亡恢复)可能先于 JsCore 真正创建就触发,原先的创建式getJsCore会在主线程同步解压 + 求值 service.js;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.yml的paths加入harmony/**,否则纯 Harmony 改动触发不了跨端一致性文本比对测试。一并修复的历史遗留问题:关闭入口不还原 opener
这组与上面的可见性主线没有因果关系,是评审和真机验证过程中发现的独立缺陷,机制是"UI 入口接到了错误的函数"。判据也不同(看 opener 收到的
scene/referrerInfo),可以独立验证。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调用"。DMPPageContainer.closeMiniProgram与DMPCapsuleButton的关闭默认分支同样统一改走exitMiniProgram。DMPNavigator.launch/DMPApp.openPage改为@discardableResult ... -> Bool,跨小程序回滚路径据此判断真实成败——此前启动失败无法向上传导,回滚拿到假的"成功",会把 opener 卡在没有返回路径的状态。新增内部测试专用 seammarkOpenedByMiniProgramForTesting,配合DMPNavigatorCapsuleTests走真实DMPAppManager路径覆盖 opener 还原。验证
单测
./gradlew :dimina:testDebugUnitTest --rerun-tasks(判决读build/test-results/**/*.xml)xcodebuild test(diminaTests)hvigorw -p module=dimina@default test(判决读.test/default/intermediates/test/coverage_data/test_result.txt,失败时 hvigorw 仍 exit 0)Android 新增用例与 HarmonyOS 新增用例都做过变异验证:分别还原胶囊调用、还原菜单调用、给两个
dispatch加回isEmpty()早返回,三次都如期转红。真机/模拟器
跨小程序 opener 还原在 Android 真机、iOS 模拟器、HarmonyOS 模拟器上各跑了两轮(
wx.navigateBackMiniProgram与原生胶囊「×」两条返回通道),判据取 opener 侧App.onShow收到的scene与referrerInfo,以及 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(同一小程序内返回)场景无法端到端复现,仍依赖单测。