You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

iOS 15.2中IAP订阅崩溃:布局引擎线程异常求助

iOS 15.2 内购(IAP)崩溃问题修复方案

问题背景

已完成内购(IAP)集成(含Signing & Capabilities添加IAP能力、App Store Connect配置订阅服务),代码在iOS 13运行正常,但iOS 15.2触发崩溃,报错信息:

Terminating app due to uncaught exception 'NSInternalInconsistencyException', reason: 'Modifications to the layout engine must not be performed from a background thread after it has been accessed from the main thread.'

已尝试添加DispatchQueue.main.async但未解决,原代码如下:

import StoreKit
import UIKit
class BuyProfileBoosterVC: UIViewController, SKPaymentTransactionObserver, SKProductsRequestDelegate {

var products = [SKProduct]()
func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
    products = response.products
    showSingleButtonWithMessage(title: "Alert", message: "You have total \(products.count) product in App Store connect.", buttonName: "Okay")
}

override func viewDidLoad() {
    super.viewDidLoad()
    DispatchQueue.main.async{
        SKPaymentQueue.default().add(self)
    }
}

enum Products: String, CaseIterable {
    case threeDays
}
// MARK: - Popup related
func fetchProduct() {
    let request = SKProductsRequest(productIdentifiers: Set(Products.allCases.compactMap({$0.rawValue})))
    request.delegate = self
    request.start()
}
func buyProduct(productId: String) {
    fetchProduct()
    DispatchQueue.main.async{
        if SKPaymentQueue.canMakePayments() {
            let paymentRequest = SKMutablePayment()
            paymentRequest.productIdentifier = productId
            SKPaymentQueue.default().add(paymentRequest)
        }
        else {
            self.view.makeToast("You can't make payment.")
        }
    }
}

func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
    
    DispatchQueue.main.async
    {
        transactions.forEach { (transaction) in
            switch transaction.transactionState {
            case .purchasing: self.view.makeToast("Transaction started.")
            case .purchased, .restored: self.view.makeToast("Transaction successful/restored.")
            case .failed, .deferred: self.view.makeToast("Transaction failed.")
            @unknown default: self.view.makeToast("Unknown state.")
            }
        }
    }
}

@IBAction func jumpBuyAction(_ sender: UIButton) {
    DispatchQueue.main.async {
        self.fetchProduct()
        self.buyProduct(productId: "threeDay")
    }
}
}

崩溃原因

  1. StoreKit代理回调线程问题:productsRequest(_:didReceive:)、paymentQueue(_:updatedTransactions:)默认在后台线程执行,原代码中productsRequest直接调用弹窗UI操作未切换主线程,这是崩溃核心触发点
  2. 逻辑冗余导致线程冲突:buyProduct和jumpBuyAction中重复调用fetchProduct,且支付请求未等待产品获取完成就发起,加剧线程冲突风险
  3. 产品ID拼写不匹配:buyProduct(productId: "threeDay")与枚举Products.threeDays的rawValue不一致,可能导致产品获取失败

修复方案

1. 强制UI操作在主线程执行

将所有代理回调中的弹窗、Toast操作包裹在DispatchQueue.main.async内

2. 修正内购逻辑

移除重复的fetchProduct调用,确保支付请求仅在产品获取完成后发起

3. 统一产品ID

修正枚举与调用参数的ID匹配问题

4. 完善内购流程

添加finishTransaction调用,避免交易残留引发内存泄漏

修改后的完整代码

import StoreKit
import UIKit

class BuyProfileBoosterVC: UIViewController, SKPaymentTransactionObserver, SKProductsRequestDelegate {

    var products = [SKProduct]()
    
    func productsRequest(_ request: SKProductsRequest, didReceive response: SKProductsResponse) {
        products = response.products
        // 弹窗操作强制切主线程
        DispatchQueue.main.async {
            self.showSingleButtonWithMessage(title: "Alert", message: "You have total \(self.products.count) product in App Store connect.", buttonName: "Okay")
        }
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        // viewDidLoad本身在主线程,无需额外包裹async
        SKPaymentQueue.default().add(self)
    }

    // 统一产品ID,与调用参数匹配
    enum Products: String, CaseIterable {
        case threeDays = "threeDay"
    }
    
    // MARK: - Popup related
    func fetchProduct() {
        let request = SKProductsRequest(productIdentifiers: Set(Products.allCases.compactMap({$0.rawValue})))
        request.delegate = self
        request.start()
    }
    
    func buyProduct(productId: String) {
        DispatchQueue.main.async {
            guard SKPaymentQueue.canMakePayments() else {
                self.view.makeToast("You can't make payment.")
                return
            }
            // 确保产品已获取到再发起支付
            guard self.products.contains(where: { $0.productIdentifier == productId }) else {
                self.view.makeToast("Product not found.")
                return
            }
            let paymentRequest = SKMutablePayment()
            paymentRequest.productIdentifier = productId
            SKPaymentQueue.default().add(paymentRequest)
        }
    }

    func paymentQueue(_ queue: SKPaymentQueue, updatedTransactions transactions: [SKPaymentTransaction]) {
        DispatchQueue.main.async {
            transactions.forEach { transaction in
                switch transaction.transactionState {
                case .purchasing:
                    self.view.makeToast("Transaction started.")
                case .purchased, .restored:
                    self.view.makeToast("Transaction successful/restored.")
                    SKPaymentQueue.default().finishTransaction(transaction)
                case .failed, .deferred:
                    self.view.makeToast("Transaction failed.")
                    SKPaymentQueue.default().finishTransaction(transaction)
                @unknown default:
                    self.view.makeToast("Unknown state.")
                    SKPaymentQueue.default().finishTransaction(transaction)
                }
            }
        }
    }

    @IBAction func jumpBuyAction(_ sender: UIButton) {
        // 先获取产品,可根据业务需求在productsRequest回调中自动触发购买
        fetchProduct()
    }
}

内容的提问来源于stack exchange,提问作者Swift

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.15 02:46:24