在iOS应用内购(IAP)开发中,沙盒测试是上线前必不可少的环节。很多团队在调试阶段会发现,用户明明已经付过款、客户端也提示购买成功,但再次启动应用或者重新登录沙盒账号时,系统又弹出了同一笔交易的完成提示,甚至导致重复发货。这种现象的根本原因往往指向同一个地方:交易没有被正确地从支付队列中移除。而负责移除操作的,正是SKPaymentQueue的finishTransaction方法。

要理解为什么交易会残留在队列里,首先需要搞清楚StoreKit的队列机制。每一笔通过SKPaymentQueue添加的支付请求,都会进入一个持久化的交易队列。这个队列由系统维护,即便应用被杀死,未结束的交易也不会消失。苹果设计这一机制的目的,是保证用户付费后无论网络如何中断、应用如何崩溃,开发者都有机会补全商品交付。只有当开发者主动调用finishTransaction,系统才认为这笔交易已经处理完毕,从而将其从队列清除。
在沙盒环境中,这一机制表现得更加“顽固”。因为沙盒不会真实扣款,但会完整模拟生产环境的回调流程。如果代码里遗漏了finishTransaction,或者把它放到了错误的判断分支,沙盒就会在每次应用启动监听队列时,把未完成的交易重新推送给应用。这也是为什么很多新手会误以为“沙盒自动重复购买”,其实是自己的交易清理逻辑没跑通。
finishTransaction应该什么时候调用
最核心的原则是:只有当你的应用已经百分百完成了与这笔交易相关的所有业务动作,才能调用finishTransaction。所谓业务动作,包括但不限于:向服务器验证收据、在本地解锁功能、把虚拟货币写入用户账户、或者把购买凭证上报给后端并记录订单状态。如果在这些动作之前就调用了finishTransaction,一旦后续步骤失败,交易已经从队列移除,用户钱付了却拿不到商品,且无法补救。
反过来说,如果业务动作已经完成,却因为代码分支遗漏而没有调用finishTransaction,就会造成前文提到的队列堆积。因此,finishTransaction的调用时机必须紧贴在“交付成功”的最后一个步骤之后,而不是放在网络请求发起前,也不是只写在成功弹窗的展示代码里。尤其要注意,网络验证是异步的,不能把finishTransaction直接写在发起验证的请求函数中,而应写在验证回调确认无误之后。
不同交易状态下的处理策略
在SKPaymentTransactionObserver的updatedTransactions回调中,我们会收到一个交易数组,每个交易都有自己的transactionState。必须针对每一种状态写清楚逻辑,否则很容易漏掉某些情况下该调用的finishTransaction。
- SKPaymentTransactionStatePurchased:购买成功。此时应完成商品交付,然后调用finishTransaction。
- SKPaymentTransactionStateFailed:购买失败。需根据error判断是否为用户取消,无论哪种失败,都应调用finishTransaction结束队列中的这笔失败交易。
- SKPaymentTransactionStateRestored:恢复购买。处理完恢复的逻辑后,对每一笔restoredTransactions里的交易调用finishTransaction。
- SKPaymentTransactionStateDeferred:待定状态,常见于家长批准场景,此时不能调用finishTransaction,应等待后续状态更新。
很多开发者只在Purchased里写了finishTransaction,却忘了Failed也要写。结果用户在沙盒里取消支付后,那笔失败交易一直挂在队列,下次进应用又触发回调。同样,恢复购买如果只处理了业务没结束交易,也会让队列越来越长。
交易状态检查的完整代码示例逻辑
下面用一个常见的Objective-C风格伪结构来说明如何安全地检查状态并移除交易。重点在于:每一个非Deferred的分支,最终都要走到finishTransaction。
| 交易状态 | 是否调用finishTransaction | 说明 |
|---|---|---|
| Purchased | 是 | 验证并交付后调用 |
| Failed | 是 | 包括用户取消,必须结束 |
| Restored | 是 | 恢复完成后对原交易结束 |
| Deferred | 否 | 等待家长或系统决策 |
| Purchasing | 否 | 正在队列中,尚未有结果 |
在实际代码中,建议将“交付商品”和“结束交易”封装成独立方法。例如在收到Purchased状态时,先调用deliverProductForTransaction:,该方法内部完成本地或服务器校验,确认无误后再调用[[SKPaymentQueue defaultQueue] finishTransaction:transaction]。这样能防止提前结束或因异常跳过结束。
另外,应用启动时要尽早调用[[SKPaymentQueue defaultQueue] addTransactionObserver:]并恢复队列监听。因为系统会在启动后自动把未结束的交易再次推给观察者。如果你的观察者加得太晚,或者监听代码里没有覆盖上述全部状态,那些沙盒里没清掉的交易就会一直赖在队列里,造成反复提示。
沙盒测试中的常见误区与排查办法
第一个误区是认为沙盒交易不用认真清理,反正是测试账号。实际上沙盒的队列逻辑和生产完全一致,今天在沙盒漏写finishTransaction,明天在生产就会真实导致用户投诉。第二个误区是把finishTransaction写在了alert弹窗的按钮点击事件里,而不是写在业务完成处。如果用户不点确定,交易就永远不结束。
排查时可以这样做:在updatedTransactions里对每个交易打印state和transactionIdentifier,观察应用启动后是否立刻收到旧交易。如果收到,说明上次没结束。此时应临时确保Failed和Restored也走结束逻辑,清理完队列后,再按正规流程只在完成业务后结束。也可以登出沙盒账号、在设置里清除测试数据,但根本解决还是靠代码覆盖全状态。
记住:finishTransaction不是“告诉苹果钱收到了”,而是“告诉苹果我已经处理完了,你可以把这笔账消了”。处理完才消账,没处理完别急着消,但也不能忘了消。
只要严格遵循按状态分流、业务完成即结束、不漏掉失败与恢复分支这三个要点,iOS内购沙盒测试时交易卡在队列的问题就能彻底解决,后续接入生产环境也会平稳很多。
iOS内购沙盒测试finishTransaction修改时间:2026-08-11 13:00:44