Как правильно хранить SKProduct* (Objective-C) в C++ std::map?

У меня есть 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...
}

Я делаю что-то неправильно при сохранении/извлечении SKProduct* из std::map? Я не знаком с моделью подсчета ссылок Objective-C.

(Обратите внимание, что код немного упрощен для ясности по сравнению с исходным кодом)

Вы компилируете его без включенного ARC?

The Dreams Wind 21.11.2022 08:46

Кроме того, не могли бы вы подробнее рассказать, как происходит переход между store_map(map) и get_map()? Сохраняет в файл/восстанавливает из файла? Или он хранится в строке, а затем восстанавливается? (мне очень интересно, используются ли одни и те же экземпляры SKProduct * в функции make_purchase

The Dreams Wind 21.11.2022 09:26

@TheDreamsWind Карта просто хранится как переменная (код обновлен, чтобы быть более понятным). Я хотел бы, чтобы код работал независимо от того, включен или отключен ARC. По сути, я не знаю, когда SKProduct* на карте уничтожается.

Viktor Sehr 21.11.2022 09:34

Мой [product retain]; просто там, чтобы убедиться, что он не уничтожен. Я не знаю, что я делаю. Например, должно быть map[product_id] = product; быть map[product_id] = [product retain];

Viktor Sehr 21.11.2022 09:37

Я сомневаюсь, что вы можете сделать код ARC-агностическим, потому что ARC запрещает явные сообщения для методов подсчета ссылок (retain/release/autorelease и т. д.). Однако в MRC этот код имеет несколько чрезмерных retain, но в целом я не вижу причин для его сбоя. Какое именно сообщение об ошибке вы получаете здесь и в какой строке?

The Dreams Wind 21.11.2022 10:23

И да - вы можете поставить retain встроенный, это не должно иметь никакого значения

The Dreams Wind 21.11.2022 10:29

Я не могу воспроизвести сбой самостоятельно, я получаю только стеки вызовов сбоев (с именем objc_retain_x0 вверху) от репортера сбоев. Но если я правильно понимаю, SKProduct* не должен как минимум уничтожаться под капотом, даже из-за того, что у меня слишком много сохранений? (Обратите внимание, что ARC отключен, но слабые ссылки в ручном сохранении включены)

Viktor Sehr 21.11.2022 10:41
Как настроить Tailwind CSS с React.js и Next.js?
Как настроить Tailwind CSS с React.js и Next.js?
Tailwind CSS - единственный фреймворк, который, как я убедился, масштабируется в больших командах. Он легко настраивается, адаптируется к любому...
LeetCode запись решения 2536. Увеличение подматриц на единицу
LeetCode запись решения 2536. Увеличение подматриц на единицу
Увеличение подматриц на единицу - LeetCode
Переключение светлых/темных тем
Переключение светлых/темных тем
В Microsoft Training - Guided Project - Build a simple website with web pages, CSS files and JavaScript files, мы объясняем, как CSS можно...
Отношения &quot;многие ко многим&quot; в Laravel с методами присоединения и отсоединения
Отношения &quot;многие ко многим&quot; в Laravel с методами присоединения и отсоединения
Отношения "многие ко многим" в Laravel могут быть немного сложными, но с помощью Eloquent ORM и его моделей мы можем сделать это с легкостью. В этой...
В PHP
В PHP
В большой кодовой базе с множеством различных компонентов классы, функции и константы могут иметь одинаковые имена. Это может привести к путанице и...
Карта дорог Беладжар PHP Laravel
Карта дорог Беладжар PHP Laravel
Laravel - это PHP-фреймворк, разработанный для облегчения разработки веб-приложений. Laravel предоставляет различные функции, упрощающие разработку...
1
7
64
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 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 clear() {
        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.clear();
    }

    id_unordered_map& operator=(const id_unordered_map& rhs) {
        clear();
        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) {
        clear();
        for (const auto& element: rhs.m_underlying_map) {
            m_underlying_map[element.first] = [element.second retain];
        }
        rhs.clear();
        return *this;
    }

    void setObject(const Key& key, id object) {
        removeObject(key);

        if (object) {
            m_underlying_map[key] = [object retain];
        }
    }

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

    void removeObject(const 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() {
        clear();
    }
};

Я предлагаю здесь поверхностный подход к копированию, так как он согласуется с тем, как работают собственные коллекции Cocoa. Создание глубокой копии считается исключительным случаем и требует отдельного метода (например, initWithDictionary:copyItems: конструктор NSDictionary)

Однако лично я не чувствую, что в предоставленном коде есть очевидная ошибка, которая приводит к сбою приложения. Ошибка, которую вы наблюдаете, обычно возникает, когда сообщение отправляется объекту, для которого не установлено значение nil, но который уже выпущен. И при условии, что никакие release сообщения не отправляются объектам на карте между функциями, ваш SKProduct объект должен выжить.

Вот несколько вещей, которые следует учитывать:

  • productsRequest:didReceiveResponse: поток вызова не указан, и он на 99% отличается от потока пользовательского интерфейса, из которого, как я предполагаю, вызывается ваша make_purchase функция. Это означает, что объекты, порожденные в потоке делегатов, могут выйти из пула автоматического освобождения, в котором они были созданы (это, однако, не должно быть проблемой, если вы retain добавили объекты и при чтении/записи на карту не возникает состояние гонки) .
  • [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, readonly, 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];
    [_productsSyncQueue release];
    [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];

    decltype(_products) newProducts;
    // Artificial Products
    for (NSString *identifier in response.invalidProductIdentifiers) {
        newProducts.setObject(identifier.UTF8String, [[SKProduct new] autorelease]);
    }
    self.products = newProducts;

}

Здесь вы можете видеть, что доступ/чтение карты синхронизировано с использованием свойств GCD и Objective-C, что, я признаю, ужасно неэффективно, когда речь идет об объектах C++, доступ к которым осуществляется по значению. Вы захотите оптимизировать его, НО я считаю, что он должен работать без сбоев.

P.S. обычно вы также хотите синхронизировать чтение из/запись в набор pendingRequests, но это не имеет отношения к контексту вопроса, поэтому я пропустил эту часть.


Вы также можете подумать о том, чтобы просто взять массивы продуктов по ссылке, не используя объекты C++, что должно работать нормально и намного проще:

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

Большое спасибо, будем реализовывать. Кроме того, хорошей идеей будет просто сохранить весь массив. Кроме того, я думаю о том, что вместо того, чтобы обертывать всю карту, я могу просто создать класс, который обертывает объект Objective-C (и сохраняет/освобождает внутри конструктора копирования и т. д.)

Viktor Sehr 22.11.2022 10:04

Другие вопросы по теме