PAYATHON 2026

手机网站支付转Native支付时iOS卡UI且未安装支付宝无响应

支付老李

结论

这两个现象通常不是支付接口本身“卡住”,而是 iOS 端的调起方式有问题:

  1. UI 长时间无响应,常见原因是主线程正在执行同步网络请求、轮询等待或阻塞式 URL 处理。支付请求应异步执行,主线程只负责更新 WKWebView 或打开支付页面。
  2. 未安装支付宝时,如果客户端仍尝试打开 alipays:// 等自定义 Scheme,系统将无法完成跳转。页面若未正确处理失败回调,就可能一直停留在“正在打开支付宝”。客户端需要先检测支付宝能否打开,并准备手机网站支付或浏览器收银台作为备用方案。

处理 UI 卡顿

不要在主线程中发起同步请求,例如:

// 不建议:可能阻塞主线程
let data = try? Data(contentsOf: paymentURL)

也不要用循环等待支付 URL 或支付结果:

// 不建议:会阻塞 UI
while paymentURL == nil {
    // wait
}

服务端创建订单后,应异步返回支付 URL。客户端成功获取 URL,再切回主线程加载页面:

import UIKit
import WebKit

final class PaymentViewController: UIViewController {
    private let webView = WKWebView()

    override func viewDidLoad() {
        super.viewDidLoad()

        webView.frame = view.bounds
        webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        view.addSubview(webView)
    }

    func requestPaymentURL() {
        guard let apiURL = URL(string: "https://example.com/api/create-payment") else {
            return
        }

        var request = URLRequest(url: apiURL)
        request.httpMethod = "POST"
        request.timeoutInterval = 30

        URLSession.shared.dataTask(with: request) { [weak self] data, response, error in
            guard error == nil, let data else {
                DispatchQueue.main.async {
                    self?.showPaymentError("创建支付订单失败")
                }
                return
            }

            // 此处按服务端实际响应结构解析,不要在客户端生成签名。
            guard
                let result = try? JSONDecoder().decode(PaymentResponse.self, from: data),
                let paymentURL = URL(string: result.paymentURL)
            else {
                DispatchQueue.main.async {
                    self?.showPaymentError("支付地址格式错误")
                }
                return
            }

            DispatchQueue.main.async {
                self?.webView.load(URLRequest(url: paymentURL))
            }
        }.resume()
    }

    private func showPaymentError(_ message: String) {
        let alert = UIAlertController(
            title: "无法发起支付",
            message: message,
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "确定", style: .default))
        present(alert, animated: true)
    }
}

private struct PaymentResponse: Decodable {
    let paymentURL: String
}

如果服务端直接返回完整的支付 URL,无需先下载页面内容,直接加载即可:

webView.load(URLRequest(url: paymentURL))

WKWebView.load(_:) 不会像同步网络请求那样阻塞主线程。如果 UI 仍然卡顿,可以使用 Xcode 的 Main Thread Checker 或 Time Profiler 查看调起支付前后的调用栈,重点检查:

  • Data(contentsOf:)
  • 已废弃的同步 NSURLConnection
  • DispatchSemaphore.wait()
  • 在主线程调用 sleep
  • 轮询等待支付参数
  • 大量 JSON、签名或加解密操作
  • 重复创建和销毁 WKWebView

处理未安装支付宝时无响应

如果业务需要拦截支付宝 Scheme 并跳转到支付宝 App,客户端应先判断系统能否打开该 URL:

import UIKit
import WebKit

extension PaymentViewController: WKNavigationDelegate {
    func webView(
        _ webView: WKWebView,
        decidePolicyFor navigationAction: WKNavigationAction,
        decisionHandler: @escaping (WKNavigationActionPolicy) -> Void
    ) {
        guard let url = navigationAction.request.url else {
            decisionHandler(.cancel)
            return
        }

        let scheme = url.scheme?.lowercased()

        if scheme == "http" || scheme == "https" {
            decisionHandler(.allow)
            return
        }

        if scheme == "alipays" {
            decisionHandler(.cancel)

            guard UIApplication.shared.canOpenURL(url) else {
                showAlipayUnavailable()
                return
            }

            UIApplication.shared.open(url, options: [:]) { [weak self] success in
                if !success {
                    self?.showAlipayUnavailable()
                }
            }
            return
        }

        decisionHandler(.cancel)
    }

    private func showAlipayUnavailable() {
        let alert = UIAlertController(
            title: "未安装支付宝",
            message: "无法打开支付宝,请使用手机网站支付或其他支付方式。",
            preferredStyle: .alert
        )
        alert.addAction(UIAlertAction(title: "确定", style: .default))
        present(alert, animated: true)
    }
}

使用 canOpenURL(_:) 检测支付宝 Scheme 时,需要根据实际使用的 Scheme,在 Info.plist 中配置查询白名单。例如:

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

Scheme 名称应以实际支付页面或支付宝接入文档为准,不要自行拼接支付参数。

降级处理

未安装支付宝时,页面不能一直停留在“正在打开支付宝”。可以按以下流程处理:

  1. 服务端创建手机网站支付订单,返回 HTTPS 收银台地址。
  2. iOS 加载该 HTTPS 地址。
  3. 页面尝试调起支付宝客户端时,由客户端判断 Scheme 是否可用。
  4. 如果支付宝可以打开,则跳转到支付宝。
  5. 如果无法打开,则继续使用手机网站收银台,或者提示用户选择浏览器支付、安装支付宝或更换支付方式。

降级地址应由支付接口或业务服务端提供。不能直接把拦截到的 alipays:// URL 替换为 https://,因为两者的参数和验证机制并不等价。

如果支付页面必须在系统浏览器环境中完成,可以使用 SFSafariViewController:

import SafariServices

func openWebCashier(_ url: URL) {
    guard url.scheme == "https" else {
        showPaymentError("收银台地址不安全")
        return
    }

    let controller = SFSafariViewController(url: url)
    present(controller, animated: true)
}

检查新窗口跳转

部分收银台页面会通过 target="_blank" 或 JavaScript 打开新页面。WKWebView 默认不会自动显示这类新窗口,所以用户点击后看起来也可能毫无反应。可以实现 WKUIDelegate:

extension PaymentViewController: WKUIDelegate {
    func webView(
        _ webView: WKWebView,
        createWebViewWith configuration: WKWebViewConfiguration,
        for navigationAction: WKNavigationAction,
        windowFeatures: WKWindowFeatures
    ) -> WKWebView? {
        if navigationAction.targetFrame == nil,
           let url = navigationAction.request.url {
            webView.load(URLRequest(url: url))
        }

        return nil
    }
}

初始化时设置代理:

webView.navigationDelegate = self
webView.uiDelegate = self

注意事项

  • 支付签名、私钥和订单创建必须放在服务端,不能写入 iOS 客户端。
  • 不能仅凭“已成功打开支付宝”判断付款成功。最终支付结果应通过服务端异步通知或主动查询支付状态来确认。
  • UIApplication.shared.open 的完成回调只表示 URL 是否成功交给系统处理,不表示付款成功。
  • 处理自定义 Scheme 时,每条分支只能调用一次 decisionHandler,否则可能出现异常或导致导航卡住。
  • 保留原始 HTTPS 收银台地址,作为未安装支付宝时的回退入口,避免重复打开同一个中间页并形成跳转循环。
  • 如果问题只出现在特定的 iOS、支付宝或支付 SDK 版本中,还需要结合实际版本、完整跳转 URL 类型和主线程调用栈继续定位。仅凭“正在打开支付宝”这句页面文案,无法判断问题出在收银台脚本、WebView 拦截还是客户端 Scheme 配置。

备注:内容仅供参考。