鸿蒙支付SDK调用getLastWindow获取SubWindow异常
结论
window.getLastWindow() 返回当前应用最近使用的窗口,不保证是主窗口。应用创建并显示 SubWindow 后,该接口可能返回子窗口。
因此,@alipay/authbase 调用 getLastWindow 时,存在获取到挂件子窗口的风险。设备未安装支付宝 App 时,支付通常会进入应用内页面等降级链路。如果该链路依赖所获取的窗口展示页面,而实际拿到的是 SubWindow,可能出现页面尺寸、层级、路由或显示状态异常。
不过,API 扫描结果只能说明依赖包中存在这项调用,还不能据此认定支付异常一定是它引起的。需要继续确认:
getLastWindow是否位于未安装支付宝 App 时实际执行的代码路径中;- SDK 获取窗口后执行了哪些操作;
- 异常发生时,该接口是否确实返回了挂件子窗口。
原因分析
getLastWindow 获取的是最近使用的窗口,并非当前 UIAbility 对应的主窗口。挂件子窗口创建并显示后,可能成为“最后窗口”,后续调用便可能返回它。
如果 SDK 使用该窗口执行以下操作,问题可能更明显:
- 获取
UIContext; - 加载支付页面;
- 创建或挂载弹窗、Web 页面;
- 获取窗口尺寸;
- 设置窗口背景、布局或系统栏属性;
- 判断窗口状态;
- 将支付 UI 挂载到该窗口对应的页面上下文。
安装支付宝 App 时,支付流程可能直接跳转到支付宝客户端,较少依赖应用内窗口。未安装支付宝 App 时,如果 SDK 改为在当前应用内承载支付页面,窗口选择错误就更容易暴露出来。
建议的排查步骤
1. 确认异常发生时返回的窗口
在调起支付前,分别记录主窗口、最后窗口和子窗口的信息。重点比较窗口名称、类型及尺寸,不要只检查窗口对象是否存在。
import { window } from '@kit.ArkUI';
async function inspectLastWindow(context: Context): Promise<void> {
try {
const lastWindow = await window.getLastWindow(context);
const properties = lastWindow.getWindowProperties();
console.info(`lastWindow name: ${properties.windowName}`);
console.info(`lastWindow type: ${properties.windowType}`);
console.info(
`lastWindow rect: ${JSON.stringify(properties.windowRect)}`
);
} catch (error) {
console.error(`getLastWindow failed: ${JSON.stringify(error)}`);
}
}
具体可以读取哪些属性,应以当前 HarmonyOS SDK 中的 WindowProperties 定义为准。如果当前 API 版本没有某个字段,请删除对应日志,不要使用未提供的属性。
建议对比两组场景:
- 不创建挂件
SubWindow,直接调起支付; - 创建并显示挂件
SubWindow,然后调起支付。
如果只有第二种场景出现异常,且日志确认 getLastWindow 返回了子窗口,基本可以确定窗口选择与该问题直接相关。
2. 验证 SDK 是否执行了该调用
静态扫描只能证明依赖包中包含 getLastWindow,无法证明发生异常的支付流程确实执行了它。可通过调试日志、断点或临时插桩确认:
- 调用来自
@alipay/authbase的哪个模块; - 未安装支付宝 App 时是否会进入该分支;
- 调用发生在支付页面创建之前还是之后;
- 获取窗口后又调用了哪些窗口或 UI 接口。
依赖包的构建产物还可能经过合并或压缩,因此,最终调用路径不一定与源码目录完全一致。
3. 临时隐藏或销毁子窗口进行验证
可以在调起支付前暂时隐藏挂件子窗口,支付流程结束后再恢复显示。如果隐藏后支付页面恢复正常,说明问题很可能与 SubWindow 有关。
async function startPaymentWithWidgetHidden(): Promise<void> {
try {
await widgetSubWindow.hideWindow();
await startAlipayPayment();
} finally {
try {
await widgetSubWindow.showWindow();
} catch (error) {
console.error(`restore widget window failed: ${JSON.stringify(error)}`);
}
}
}
widgetSubWindow 和 startAlipayPayment() 只是示意名称,需要替换为业务中的实际对象和支付调用。恢复子窗口的时机也要根据支付接口的完成方式处理。如果支付调用返回的只是“已成功拉起”,而非等待整个支付页面关闭,就不能在该 Promise 完成后立即恢复窗口。
推荐处理方式
方案一:SDK 显式使用主窗口
如果可以修改 SDK,建议不要在需要主窗口的场景中使用 window.getLastWindow()。可以由 UIAbility 的 WindowStage 显式获取主窗口,再将主窗口或对应的 UIContext 传给支付模块。
import { UIAbility } from '@kit.AbilityKit';
import { window } from '@kit.ArkUI';
export default class EntryAbility extends UIAbility {
private mainWindow?: window.Window;
onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.getMainWindow()
.then((mainWindow: window.Window) => {
this.mainWindow = mainWindow;
return mainWindow.loadContent('pages/Index');
})
.catch((error) => {
console.error(`get main window failed: ${JSON.stringify(error)}`);
});
}
getMainWindow(): window.Window | undefined {
return this.mainWindow;
}
}
更稳妥的设计是让宿主应用在初始化支付 SDK 时传入主窗口、UIContext 或页面承载能力,避免 SDK 根据“最近使用窗口”自行判断。
支付宝 SDK 团队需要结合以下版本,确认当前是否已有相应的初始化参数:
{
"@alipay/afservicesdk": "1.0.241118203225",
"@cashier_alipay/cashiersdk": "15.8.23",
"@alipay/authbase": "1.3.9-250912"
}
在没有官方接口说明的情况下,不应自行假设 SDK 支持某种窗口注入参数。
方案二:支付期间避免让子窗口成为最后使用窗口
如果暂时无法修改 SDK,可以先从业务侧规避:
- 调起支付前隐藏挂件子窗口;
- 必要时关闭子窗口,支付结束后再重新创建;
- 调起支付前避免更新、聚焦或重新显示子窗口;
- 从主窗口页面执行支付入口;
- 确保传给支付 SDK 的
Context来自主窗口所属的UIAbility。
这类方案适合临时处理,但会依赖窗口操作的时序,稳定性通常不如让 SDK 显式使用主窗口。
方案三:升级或由 SDK 侧提供修复版本
建议向 SDK 团队提供最小复现工程,并附上以下信息:
- 创建
SubWindow的代码和调用时序; - 支付调用代码;
- 安装与未安装支付宝 App 时的对比结果;
getLastWindow返回窗口的日志;- 主窗口与子窗口的名称、类型和尺寸;
- 页面异常的截图或录屏;
- 设备型号、HarmonyOS 版本和 API 版本;
- 三个支付宝依赖的完整版本及实际解析出的依赖树。
请 SDK 团队重点确认:
@alipay/authbase中的getLastWindow是否用于收银台降级页面;- 当前实现是否默认把返回的窗口当作主窗口;
- 是否可以改用
WindowStage.getMainWindow(); - 是否可以由宿主传入主窗口或
UIContext; - 是否已有兼容
SubWindow场景的版本。
注意事项
- 不建议通过调用一次主窗口 API 来“刷新”窗口顺序。这种方式依赖系统内部行为,无法保证稳定。
- 不要长期缓存已经销毁的窗口对象。
UIAbility重建或窗口销毁后,应及时清理缓存并重新获取主窗口。 - 如果应用支持多实例、多窗口或折叠屏场景,不能通过全局单例无条件保存唯一的主窗口,应按对应的
UIAbility实例进行管理。 - 不要因为“安装支付宝 App 时正常”就排除 SDK 问题。客户端支付与应用内降级支付可能采用不同的执行路径。
- 如果隐藏子窗口后仍然异常,应继续检查支付页面使用的
Context、页面路由、窗口尺寸、网络请求和 Web 组件初始化,不要把所有异常都归因于getLastWindow。
备注:内容仅供参考。
