手机网站支付转Native支付时iOS卡UI且未安装支付宝无响应
支付老李
结论
这两个现象通常不是支付接口本身“卡住”,而是 iOS 端的调起方式有问题:
- UI 长时间无响应,常见原因是主线程正在执行同步网络请求、轮询等待或阻塞式 URL 处理。支付请求应异步执行,主线程只负责更新
WKWebView或打开支付页面。 - 未安装支付宝时,如果客户端仍尝试打开
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 名称应以实际支付页面或支付宝接入文档为准,不要自行拼接支付参数。
降级处理
未安装支付宝时,页面不能一直停留在“正在打开支付宝”。可以按以下流程处理:
- 服务端创建手机网站支付订单,返回 HTTPS 收银台地址。
- iOS 加载该 HTTPS 地址。
- 页面尝试调起支付宝客户端时,由客户端判断 Scheme 是否可用。
- 如果支付宝可以打开,则跳转到支付宝。
- 如果无法打开,则继续使用手机网站收银台,或者提示用户选择浏览器支付、安装支付宝或更换支付方式。
降级地址应由支付接口或业务服务端提供。不能直接把拦截到的 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 配置。
备注:内容仅供参考。
