【问题标题】:How do I correctly store an (Objective-C) SKProduct* in a C++ std::map?如何在 C++ std::map 中正确存储 (Objective-C) SKProduct*?
【发布时间】:2022-11-21 21:01:00
【问题描述】:

我有一个 std::map<std::string, SKProduct*>,我这样填充:

// Assume s_map is always accessed in a thread safe way    
static auto s_map = std::map<std::string, SKProduct*>{};

-(void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
  auto map = std::map<std::string, SKProduct*>{};
  const auto product_id = std::string(
    [product.productIdentifier UTF8String]
  );
  for(SKProduct* product in response.products) {
    if(product != nil) {
      map[product_id] = product;
      [map[product_id] retain]; 
    }
  }
  s_map = map;
}

稍后(购买时)我发现 SKProduct* 是这样的:

auto make_purchase(const product_id_t& product_id) -> void {
  // Note that the whole map is copied
  const std::map<std::string, SKProduct*> map = s_map;
  const auto product_it = map.find(product_id);
  if(it == map.end()) {
    return;
  }
  // Here somewhere I get a crash objc_retain_x0
  SKProduct* product = product_it->second;
  [product retain];
  SKPayment* payment = [SKPayment paymentWithProduct: product];
  [payment retain]; 
  // Continue the purchase from here on...
}

std::map 存储/检索SKProduct* 时我做错了什么吗?我不熟悉引用计数的 Objective-C 模型。

(请注意,与原始代码相比,为了清晰起见,代码有所简化)

【问题讨论】:

  • 你在没有启用 ARC 的情况下编译它吗?
  • 另外,您能否详细说明store_map(map)get_map() 之间的转换是如何发生的?它是否保存到文件/从文件恢复?还是存储在字符串中然后恢复? (我很感兴趣是否在make_purchase函数中使用了相同的SKProduct *实例
  • @TheDreamsWind 地图只是存储为一个变量(更新代码更清晰)。无论是否启用/禁用 ARC,我都希望代码能够正常运行。基本上,我不知道地图中的 SKProduct* 何时被破坏。
  • 我怀疑您能否使代码与 ARC 无关,因为 ARC 禁止显式消息引用计数方法(retain/release/autorelease,等等)。然而,在 MRC 下,这段代码有一些过多的 retain's,但总的来说我找不到任何导致它崩溃的原因。你在这里得到什么确切的错误信息和哪一行?
  • 是的 - 你可以将 retain 内联,它应该没有任何区别

标签: c++ ios objective-c objective-c++


【解决方案1】:

对于标题中的问题,我会封装这样一个类来简化内存管理范围。这是此类的简约实现:

template<typename Key> // Key type must not be a retain-release object
class id_unordered_map {
    std::unordered_map<Key, id> m_underlying_map;

    void release_objects() {
        for (const auto& element: m_underlying_map) {
            [element.second release];
        }
        m_underlying_map.clear();
    }
public:
    id_unordered_map() = default;

    id_unordered_map(const id_unordered_map& rhs) {
        for (const auto& element: rhs.m_underlying_map) {
            // makes a shallow copy
            m_underlying_map[element.first] = [element.second retain];
        }
    };

    id_unordered_map(id_unordered_map&& rhs) {
        for (const auto& element: rhs.m_underlying_map) {
            m_underlying_map[element.first] = [element.second retain];
        }
        rhs.release_objects();
    }

    id_unordered_map& operator=(const id_unordered_map& rhs) {
        release_objects();
        for (const auto& element: rhs.m_underlying_map) {
            // makes a shallow copy
            m_underlying_map[element.first] = [element.second retain];
        }
        return *this;
    }

    id_unordered_map& operator=(id_unordered_map&& rhs) {
        release_objects();
        for (const auto& element: rhs.m_underlying_map) {
            m_underlying_map[element.first] = [element.second retain];
        }
        rhs.release_objects();
        return *this;
    }

    void addObject(const Key& key, id object) {
        if (object) {
            m_underlying_map[key] = [object retain];
        }
    }

    id getObject(Key key) {
        if (auto it = m_underlying_map.find(key); it != m_underlying_map.end()) {
            return it->second;
        } else {
            return nil;
        }
    }

    void removeObject(Key key) {
        if (auto it = m_underlying_map.find(key); it != m_underlying_map.end()) {
            [it->second release];
            m_underlying_map.erase(it);
        }
    }

    ~id_unordered_map() {
        release_objects();
    }
};

您可能对我在这里建议的浅拷贝方法感到有点困惑,但它与 Cocoa 自己的集合的工作方式是一致的。进行深度复制被认为是一种例外情况,需要单独的方法(例如 initWithDictionary:copyItems:NSDictionary 的构造函数)

但是,就我个人而言,我不认为所提供的代码中存在导致崩溃发生的明显错误。当消息发送到未设置为 nil 但已发布的对象时,通常会发生您观察到的错误。如果没有 release 消息发送到函数之间映射中的对象,您的 SKProduct 对象必须存活。

不过,这里有几点需要考虑:

  • productsRequest:didReceiveResponse: 调用线程未指定,它与 UI 线程有 99% 的不同,我假设你的 make_purchase 函数是从中调用的。这意味着,在委托线程中生成的对象可能会离开它们创建时所在的自动释放池(但是,如果您 retained 对象并且在读取/写入时没有竞争条件发生,这应该不是问题地图)。
  • [SKPayment paymentWithProduct: product]; 返回一个自动释放的对象,该对象不会(至少)在当前范围结束之前过期,因此您不需要 retain 它。
  • 如果您在应用程序的生命周期中多次发出产品请求,请确保在将新数据写入地图之前释放地图包含的对象并clear()它。

总结一下,你的SKProductsRequestDelegate应该看起来像这样(这里的产品是人造的,所以我在回复中即时制作):

NS_ASSUME_NONNULL_BEGIN

@interface TDWObject ()<SKProductsRequestDelegate>

@property (strong, readonly, nonatomic) dispatch_queue_t productsSyncQueue;
@property (assign, nonatomic) id_unordered_map<std::string> products;
@property (strong, nonatomic) NSMutableSet<SKProductsRequest *> *pendingRequests;

@end

NS_ASSUME_NONNULL_END

@implementation TDWObject

@synthesize products = _products;

#pragma mark Lifecycle

- (instancetype)init {
    if (self = [super init]) {
        _productsSyncQueue = dispatch_queue_create("the.dreams.wind.property_access.products",
                                                    DISPATCH_QUEUE_CONCURRENT);
        _pendingRequests = [[NSMutableSet set] retain];
    }
    return self;
}

- (void)dealloc {
    [_pendingRequests release];
    _pendingRequests = nil;
    [_productsSyncQueue release];
    _productsSyncQueue = nil;
    [super dealloc];
}

#pragma mark Properties

- (id_unordered_map<std::string>)products {
    __block id_unordered_map<std::string> *data;
    dispatch_sync(_productsSyncQueue, ^{
        // Take by pointer here, to avoid redundant copy
        data = &_products;
    });
    return *data; // makes a copy for observers
}

- (void)setProducts:(id_unordered_map<std::string>)products {
    dispatch_barrier_async(_productsSyncQueue, ^{
        _products = std::move(products);
    });
}

#pragma mark Actions

- (void)requestProducts {
    SKProductsRequest *productRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:[NSSet setWithArray:@[
        @"the.dreams.wind.sampleSKU1"
    ]]];
    productRequest.delegate = self;
    [productRequest start];
    [_pendingRequests addObject:productRequest];
}

- (void)makePurchase {
    SKProduct *product = [_products.getObject("the.dreams.wind.sampleSKU1") retain];
    // Just checking that the object exists
    NSLog(@"%@", product);
    [product release];
}

#pragma mark SKProductsRequestDelegate

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    [_pendingRequests removeObject:request];
    [response.invalidProductIdentifiers enumerateObjectsUsingBlock:^(NSString * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        SKProduct *product = [SKProduct new];
        auto products = self.products;
        products.addObject([obj UTF8String], product);
        self.products = products;
        [product release];
    }];
}

@end

您可以在这里看到属性的访问/读取与 GCD 和 Objective-C 的使用同步,我承认,当涉及到 C++ 对象(在 getter 和 setter 中复制)时,这非常低效,您会想要对其进行优化,但它应该可以在您的场景中正常工作而不会崩溃。


您也可以考虑仅通过引用获取 products 数组,而不使用 C++ 对象,这应该可以正常工作并且更直接:

- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response {
    [_pendingRequests removeObject:request];
    NSArray<SKProduct *> *products = [response.products retain];
    ...
}

【讨论】:

    猜你喜欢
    • 2023-03-27
    • 2021-06-25
    • 2014-02-24
    • 2021-11-27
    • 2011-07-14
    • 1970-01-01
    • 2011-07-28
    • 1970-01-01
    • 2012-11-24
    相关资源
    最近更新 更多