PAYATHON 2026

Xamarin binding 在 iOS SDK 7.1 中调用支付 API 超时

支付阿杰

结论

日志显示,initiate Session 从 08:13:11 持续到 08:14:11,约 60 秒后返回 DF39 = 4900。这种情况更像是支付 SDK 在等待网络响应或异步回调时超时。仅凭现有日志,无法认定问题来自 Xamarin 或 iOS 7.1。

建议先在同一设备和同一网络下,使用相同版本的支付 SDK 运行厂商提供的原生 Objective-C 示例:

  • 如果原生示例也超时,重点检查支付 SDK 与 iOS 7.1 的兼容性,以及服务器、TLS、证书、代理和设备配置。
  • 如果只有 Xamarin binding 超时,重点检查绑定签名、回调对象的生命周期、主线程要求、静态库架构和链接器裁剪。
  • 如果 Debug 正常而 Release 超时,应优先检查 Xamarin linker 是否裁剪了 Objective-C 回调入口。

日志中的 syncdefaultsd、wifid、定位扫描和屏幕亮度信息大多与这次调用无关。最后一条日志:

HID: The 'Rate Controlled' connection 'Sample' access to protected services is denied.

说明应用尝试访问某种受保护的 HID 服务,但现有信息不足以证明它导致了支付超时。除非支付 SDK 明确依赖刷卡器、PIN Pad 或其他外设,否则不能将它视为唯一线索。

建议的定位顺序

1. 先确认超时发生在哪一层

分别记录以下时间点:

C# 调用 binding
进入 Objective-C SDK
开始建立网络连接
收到 HTTP 状态码
进入 Objective-C callback
进入 C# callback

如果厂商 SDK 支持详细日志,还应记录:

  • 最终请求 URL 的主机名,敏感路径和参数需要脱敏;
  • DNS、TCP、TLS 和 HTTP 各阶段的耗时;
  • HTTP 状态码和底层 NSError;
  • SDK 返回码 DF39 = 4900 的官方含义。

4900 看起来是厂商定义的业务错误码,不是标准的 iOS 错误码。必须查阅对应支付 SDK 版本的错误码文档,不能直接将其解释为某种系统错误。

2. 用原生 Objective-C 示例做对照

在同一台 iOS 7.1 设备上运行厂商提供的原生示例,并确保以下条件完全相同:

  • 支付 SDK 二进制文件;
  • activation URL;
  • 商户或终端配置;
  • 请求参数;
  • Wi-Fi 或蜂窝网络;
  • 服务器环境。

如果原生程序同样在 60 秒后超时,binding 通常不是主要原因。此时应请支付 SDK 供应商确认该版本是否正式支持 iOS 7.1,同时检查服务端是否实际收到了请求。

3. 检查 binding 的方法签名

Objective-C 的参数类型、返回类型和 selector 必须逐项匹配。例如,原生接口为:

- (void)initiateSession:(NSDictionary *)parameters
               delegate:(id<PaymentDelegate>)delegate;

对应的绑定可以写成:

[BaseType(typeof(NSObject))]
interface PaymentClient
{
    [Export("initiateSession:delegate:")]
    void InitiateSession(
        NSDictionary parameters,
        [NullAllowed] IPaymentDelegate callback);
}

[Protocol, Model]
[BaseType(typeof(NSObject))]
interface PaymentDelegate
{
    [Abstract]
    [Export("paymentClient:didCompleteWithParameters:")]
    void DidComplete(
        PaymentClient client,
        NSDictionary parameters);

    [Abstract]
    [Export("paymentClient:didFailWithError:")]
    void DidFail(
        PaymentClient client,
        NSError error);
}

需要重点核对:

  • selector 中冒号的数量和位置;
  • NSDictionary、NSString、NSData 等类型是否正确;
  • delegate 是参数、属性还是 block;
  • 原生声明中的 delegate 属性是 assign、weak、retain 还是 strong;
  • 回调是否为可选方法;
  • block 是否需要 [BlockCallback];
  • C 结构体和枚举的底层宽度是否正确。

不要根据方法名称猜测签名,应以当前支付 SDK 版本的头文件为准。

4. 强引用 delegate 和 SDK 实例

支付 SDK 经常使用 weak 或旧式的 assign delegate。如果回调对象只保存在局部变量中,托管对象可能在异步操作结束前被垃圾回收。此时原生 SDK 即使执行了回调,C# 层也可能无法收到,最终表现为等待超时。

public sealed class PaymentService : IDisposable
{
    private readonly PaymentClient client;
    private readonly PaymentCallback callback;

    public PaymentService()
    {
        client = new PaymentClient();
        callback = new PaymentCallback();
    }

    public void Start(NSDictionary parameters)
    {
        client.InitiateSession(parameters, callback);
        GC.KeepAlive(callback);
    }

    public void Dispose()
    {
        callback.Dispose();
        client.Dispose();
    }
}

public sealed class PaymentCallback : PaymentDelegate
{
    public override void DidComplete(
        PaymentClient client,
        NSDictionary parameters)
    {
        Console.WriteLine("Payment callback completed");
    }

    public override void DidFail(
        PaymentClient client,
        NSError error)
    {
        Console.WriteLine(
            $"Payment callback failed: Domain={error.Domain}, " +
            $"Code={error.Code}, Description={error.LocalizedDescription}");
    }
}

GC.KeepAlive 只能保证对象存活到当前方法结束,不能代替字段级强引用。在异步支付流程中,应由生命周期足够长的对象持有 delegate 和 SDK client,直到收到成功、失败或取消回调。

5. 确保在主线程调用

如果支付 SDK 会创建 UI、访问外设、启动 run loop source,或者要求在主线程初始化,就应显式切换到主线程:

InvokeOnMainThread(() =>
{
    paymentService.Start(parameters);
});

还应避免以下写法:

// 不建议:可能阻塞负责派发原生回调的线程
var result = paymentTask.Result;

// 不建议:同步等待可能造成死锁
paymentTask.Wait();

应优先使用 await,并确保主线程没有被同步等待占用:

try
{
    PaymentResult result = await paymentService.StartAsync(parameters);
}
catch (NSErrorException ex)
{
    Console.WriteLine(
        $"Domain={ex.Error.Domain}, Code={ex.Error.Code}, " +
        $"Description={ex.Error.LocalizedDescription}");
}

6. 暂时关闭链接器进行验证

Xamarin linker 可能会移除那些只由 Objective-C runtime 调用的类型或成员。可以先在测试构建中将 Linker Behavior 设置为“不链接”,然后重新部署。

如果关闭链接后恢复正常,就需要保留回调类型或相关成员。例如:

using Foundation;

[Preserve(AllMembers = true)]
public sealed class PaymentCallback : PaymentDelegate
{
    public override void DidComplete(
        PaymentClient client,
        NSDictionary parameters)
    {
    }

    public override void DidFail(
        PaymentClient client,
        NSError error)
    {
    }
}

绑定项目也可以根据实际 API 使用:

[Protocol, Model]
[BaseType(typeof(NSObject))]
interface PaymentDelegate
{
    // 回调声明
}

需要保留哪些类型取决于 SDK 的调用方式。确认原因后,应将保留范围缩小到必要的类型和成员,不建议长期完全关闭链接器来掩盖问题。

7. 检查静态库及其依赖

确认支付 SDK 的 .a 或 framework 包含目标设备所需的架构。iPhone 5s 引入了 arm64,但缺少 arm64 通常会造成构建、安装或加载错误,而不仅仅是网络超时。即便如此,升级工具链时仍应检查:

lipo -info libPaymentSDK.a

还要核对厂商文档要求的系统 framework,例如:

Security.framework
SystemConfiguration.framework
CoreLocation.framework
ExternalAccessory.framework

如果 SDK 通过 Objective-C category 提供功能,静态链接时可能需要 -ObjC。不要盲目添加链接参数,应以 SDK 文档或链接日志为依据。

8. 独立检查网络和服务端

日志显示请求目标使用 HTTPS,因此还需要检查:

  • 设备时间和时区是否正确;
  • 证书链是否完整;
  • 服务器是否支持该设备和 SDK 使用的 TLS 版本及加密套件;
  • activation URL 是否发生重定向;
  • 企业代理、防火墙或 VPN 是否拦截请求;
  • iOS 7.1 设备是否能直接访问同一主机;
  • 服务端是否在 08:13:11 左右收到请求;
  • 服务端是否已经返回响应,但客户端没有正确处理。

在支付 SDK 允许的前提下,可以通过服务端 access log 或受控代理获取连接各阶段的信息。支付数据可能包含卡号、令牌和设备标识,因此调试日志及抓包文件必须脱敏,并限制访问范围。

对现有日志的判断

与业务直接相关的日志只有:

08:13:11 ----->Called initiate Session
08:14:11 Key - DF39 & Value - 4900
08:14:11 ----->DONE initiate Session

这些日志只能证明调用在约一分钟后结束,无法说明:

  • 请求是否真正发出;
  • DNS 和 TLS 是否成功;
  • 服务端是否收到请求;
  • Objective-C 回调是否执行;
  • 4900 表示客户端超时、服务端超时,还是其他厂商错误。

因此,先不要根据 wifid 或 HID 日志推测原因。应先完成原生示例和 Xamarin binding 的对照测试,再分别在 Objective-C 入口与 C# 回调处增加日志。

如果确认只有 binding 失败,应优先检查并修正 delegate 生命周期、绑定签名、主线程调用和 linker 配置。如果原生示例也失败,则应升级到厂商明确支持 iOS 7.1 的支付 SDK,或请供应商结合 DF39 = 4900 和服务端日志继续定位。

备注:内容仅供参考。