PAYATHON 2026

iOS WKWebView 通过 alipays 协议无法唤起支付宝 App

支付小周

结论

decidePolicyForNavigationAction 没有被调用,通常并不是 alipays:// 被 iOS 拦截,而是 WKWebView 没有产生新的导航请求,或 WKNavigationDelegate 没有正确绑定。

这个回调只能处理已经发生的导航动作。如果支付宝页面的 JavaScript 在跳转前报错、支付请求返回异常、页面资源加载失败,或页面通过新窗口发起跳转但没有实现 WKUIDelegate,代码就不会进入预期的 alipays:// 处理逻辑。

问题中没有提供具体的 console 和网络日志,仅凭页面停在“加载中”无法确定唯一原因。排查时应先确认回调链是否正常,再检查支付宝页面是否真的生成了 alipays:// 导航。

先确认 WKNavigationDelegate 是否生效

创建 WKWebView 后,需要设置 navigationDelegate,并确认它没有被其他对象覆盖:

@interface PaymentViewController () <WKNavigationDelegate, WKUIDelegate>
@property (nonatomic, strong) WKWebView *webView;
@end

@implementation PaymentViewController

- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration *configuration =
        [[WKWebViewConfiguration alloc] init];

    self.webView =
        [[WKWebView alloc] initWithFrame:self.view.bounds
                           configuration:configuration];

    self.webView.navigationDelegate = self;
    self.webView.UIDelegate = self;

    [self.view addSubview:self.webView];
}

@end

可以先加载一个普通链接来验证代理:

NSURLRequest *request = [NSURLRequest requestWithURL:
    [NSURL URLWithString:@"https://example.com"]];
[self.webView loadRequest:request];

如果普通页面跳转也没有进入 decidePolicyForNavigationAction,需要检查:

  • navigationDelegate 是否确实指向当前控制器;
  • WKWebView 是否由其他组件创建;
  • 设置代理后,第三方 SDK 或封装层是否又覆盖了代理;
  • 控制器或代理对象是否已经释放;
  • 方法签名是否准确;
  • 页面上显示的是否就是当前监听的 WKWebView。

发起支付前可以输出以下信息:

NSLog(@"webView = %@", self.webView);
NSLog(@"navigationDelegate = %@", self.webView.navigationDelegate);
NSLog(@"UIDelegate = %@", self.webView.UIDelegate);

正确处理 alipays 导航

在导航代理中识别支付宝自定义协议,再交给系统打开:

- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {

    NSURL *URL = navigationAction.request.URL;
    NSString *scheme = URL.scheme.lowercaseString;

    NSLog(@"navigation URL: %@", URL.absoluteString);

    if ([scheme isEqualToString:@"alipays"] ||
        [scheme isEqualToString:@"alipay"]) {

        decisionHandler(WKNavigationActionPolicyCancel);

        if (@available(iOS 10.0, *)) {
            [[UIApplication sharedApplication]
                openURL:URL
                options:@{}
                completionHandler:^(BOOL success) {
                    NSLog(@"open Alipay result: %@",
                          success ? @"YES" : @"NO");
                }];
        } else {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
            BOOL success =
                [[UIApplication sharedApplication] openURL:URL];
#pragma clang diagnostic pop
            NSLog(@"open Alipay result: %@",
                  success ? @"YES" : @"NO");
        }

        return;
    }

    decisionHandler(WKNavigationActionPolicyAllow);
}

每条执行路径都必须调用 decisionHandler,而且只能调用一次。遗漏或重复调用都可能让页面导航卡住,甚至触发异常。

不要只判断完整 URL 的前缀,例如:

[URL.absoluteString hasPrefix:@"alipays://"]

直接判断 scheme 更稳妥,也能避开大小写或 URL 形式差异带来的问题。

检查是否通过新窗口发起跳转

支付页面可能使用 window.open,也可能使用带有 target="_blank" 的链接。这类请求通常会进入 WKUIDelegate 的新窗口回调,不一定会按现有代码的预期在当前页面跳转。

可以把没有目标 WKWebView 的请求加载到当前 WKWebView:

- (WKWebView *)webView:(WKWebView *)webView
createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
   forNavigationAction:(WKNavigationAction *)navigationAction
        windowFeatures:(WKWindowFeatures *)windowFeatures {

    if (navigationAction.targetFrame == nil) {
        NSURL *URL = navigationAction.request.URL;
        NSString *scheme = URL.scheme.lowercaseString;

        if ([scheme isEqualToString:@"alipays"] ||
            [scheme isEqualToString:@"alipay"]) {

            if (@available(iOS 10.0, *)) {
                [[UIApplication sharedApplication]
                    openURL:URL
                    options:@{}
                    completionHandler:nil];
            }

            return nil;
        }

        [webView loadRequest:navigationAction.request];
    }

    return nil;
}

同时设置:

self.webView.UIDelegate = self;

捕获页面脚本错误

如果页面一直显示“加载中”,日志里却从未出现 alipays://,支付宝页面很可能还没有执行到唤起 App 的代码。此时应检查 JavaScript 异常、资源加载失败和支付接口响应。

可以注入脚本,把常见的前端错误转发给 Native:

WKUserContentController *controller =
    [[WKUserContentController alloc] init];

NSString *scriptSource =
    @"window.addEventListener('error', function(e) {"
     "  window.webkit.messageHandlers.jsError.postMessage({"
     "    message: e.message || '',"
     "    source: e.filename || '',"
     "    line: e.lineno || 0,"
     "    column: e.colno || 0"
     "  });"
     "});"
     "window.addEventListener('unhandledrejection', function(e) {"
     "  window.webkit.messageHandlers.jsError.postMessage({"
     "    message: String(e.reason || 'unhandledrejection')"
     "  });"
     "});";

WKUserScript *script =
    [[WKUserScript alloc] initWithSource:scriptSource
                          injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                       forMainFrameOnly:NO];

[controller addUserScript:script];
[controller addScriptMessageHandler:self name:@"jsError"];

WKWebViewConfiguration *configuration =
    [[WKWebViewConfiguration alloc] init];
configuration.userContentController = controller;

接收错误:

- (void)userContentController:(WKUserContentController *)userContentController
      didReceiveScriptMessage:(WKScriptMessage *)message {

    if ([message.name isEqualToString:@"jsError"]) {
        NSLog(@"JavaScript error: %@", message.body);
    }
}

还需要补充导航失败日志:

- (void)webView:(WKWebView *)webView
didFailProvisionalNavigation:(WKNavigation *)navigation
      withError:(NSError *)error {
    NSLog(@"provisional navigation failed: %@", error);
}

- (void)webView:(WKWebView *)webView
didFailNavigation:(WKNavigation *)navigation
      withError:(NSError *)error {
    NSLog(@"navigation failed: %@", error);
}

- (void)webView:(WKWebView *)webView
didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition,
                            NSURLCredential * _Nullable credential))completionHandler {
    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, nil);
}

在 macOS Safari 中开启“开发”菜单并连接 iPhone,远程检查 WKWebView,可以直接查看:

  • JavaScript 异常;
  • 支付页面的 Network 请求;
  • 请求状态码和响应内容;
  • 页面是否生成过 alipays:// 地址;
  • 是否有 CSP、跨域、Cookie 或资源加载错误。

检查支付请求的生成方式

手机网站支付通常由服务端调用支付接口,再返回支付宝要求的跳转内容,例如自动提交的 HTML 表单。客户端不应自行拼接未经验证的地址:

https://mapi.alipay.com/gateway.do?xxxxxx

需要逐项确认:

  • 支付产品、网关地址和签名算法是否与当前服务端接入方案一致;
  • 服务端是否正确生成了所有必填参数;
  • 参数编码、签名内容和实际发送的内容是否完全一致;
  • 服务端返回的支付跳转内容是否完整有效;
  • 是否把应通过 POST 提交的表单直接转换成了 URL;
  • 支付请求是否过期,系统时间是否正确;
  • return_url、通知地址和业务参数是否符合接入要求;
  • 是否混用了旧版网关示例和当前接口参数。

建议记录服务端响应中的支付宝错误码和错误信息,但不要在日志中写入私钥、完整签名、用户身份信息或完整支付参数。

如果支付页面本身没有完成初始化,它就不会生成 alipays:// 导航。此时修改 WKWebView 代理代码解决不了根本问题。

Info.plist 配置的作用

如果代码使用 canOpenURL: 检查支付宝是否已经安装,需要在 Info.plist 中加入查询白名单:

<key>LSApplicationQueriesSchemes</key>
<array>
    <string>alipays</string>
    <string>alipay</string>
</array>

例如:

NSURL *URL = [NSURL URLWithString:@"alipays://"];

if ([[UIApplication sharedApplication] canOpenURL:URL]) {
    NSLog(@"Alipay is available");
}

缺少 LSApplicationQueriesSchemes 主要会影响 canOpenURL: 的返回结果,无法解释 WKWebView 为什么连导航代理都没有触发。因此,不要把这项配置当作页面始终停在“加载中”的唯一原因。

如果不需要提前检测,可以直接调用 openURL:options:completionHandler:,再通过 completion handler 判断是否打开成功。

推荐排查顺序

  1. 加载普通网页,确认 navigationDelegate 确实生效。
  2. 在所有导航回调中记录完整 URL,检查是否出现过 alipays 或 alipay scheme。
  3. 实现 WKUIDelegate,处理 window.open 和 target="_blank"。
  4. 使用 Safari 远程调试查看 console 和 Network,找到页面停在“加载中”的直接原因。
  5. 核对服务端返回的支付表单、参数、签名和支付宝错误响应。
  6. 分别在已安装和未安装支付宝的设备上测试。
  7. 更换网络、iOS 版本和真机复测,排除网络拦截、Cookie、系统环境或单台设备状态的影响。
  8. 检查应用是否已经正确注册并处理支付完成后的回跳 URL。

排查的核心是先确认页面有没有发起 alipays:// 导航。如果没有,应检查页面脚本、网络请求或服务端支付参数。如果已经发起但无法打开,再检查 scheme 拦截、系统打开逻辑、支付宝安装状态和应用配置。

备注:内容仅供参考。