Утечка C ++ shared_ptr в блоке Objective-C

Резюме:

В приведенном ниже примере приложения shared_ptr захватывается в блоке Objective-C. Блок Objective-C присваивается ivar динамически создаваемого класса с использованием API-интерфейса Objective-C object_setIvarWithStrongDefault, Когда объект Objective C освобожден, shared_ptr происходит утечка, и сохраняемый объект C ++ не удаляется. Это почему?

когда object_setIvar вместо этого, то предотвращается утечка, но ivar указывает на мусор, как только блок выходит из области видимости, как object_setIvar предполагает присвоение unsafe_unretained,

я предполагать это связано с тем, как Objective-C захватывает объекты C ++, копирует блоки и как shared_ptr обрабатывает копирование, но я надеялся, что кто-нибудь сможет пролить свет на это больше, чем на документацию, указанную ниже.

Рекомендации:

Предыстория:

Этот пример кода извлечен из гораздо более крупного проекта и был значительно сокращен до минимума, необходимого для выявления проблемы. Проект представляет собой MacOS-приложение Objective-C. Приложение содержит несколько монолитных объектов C ++, которые являются прославленными хранилищами ключей / значений. Каждый объект является экземпляром того же класса, но основан на типе ключа. Я хочу динамически создать класс Objective-C, который содержит типизированные методы получения свойств, которые поддерживаются классом C ++.

(Да, все это можно сделать вручную, просто написав множество методов получения, но я бы предпочел этого не делать. Класс C ++ имеет достаточно информации, чтобы знать имена свойств и их типы, поэтому я бы хотел бы использовать некоторые методы метапрограммирования, чтобы «решить» это.)

Заметки:

В идеальном мире я бы просто мог определить iVar на Objective-C класса соответствующего shared_ptr типа, но я не могу понять, как это сделать с помощью API-интерфейса Objective-C.

Учитывая это:

std::shared_ptr<BackingStore<T>> backingStore

Как вы используете это:

class_addIvar а также object_setIvar

Поскольку я не мог этого понять, я решил просто обернуть shared_ptr в блок Objective-C, поскольку блоки являются объектами первого класса и могут передаваться там, где id ожидается.

Образец заявки:

(Скопируйте / вставьте что-то вроде CodeRunner чтобы увидеть вывод)

#import <Foundation/Foundation.h>
#import <objc/runtime.h>
#import <memory>

typedef NSString* (^stringBlock)();

/**
*  StoreBridge
*
*  Objective-C class that exposes Objective-C properties
*  which are "backed" by a C++ object (Store). The implementations
*  for each property on this class are dynamically added.
*/
@interface StoreBridge : NSObject

@property(nonatomic, strong, readonly) NSString *storeName;

@end

@implementation StoreBridge

@dynamic storeName;

- (void)dealloc {
NSLog(@"StoreBridge DEALLOC");
}

@end

/**
*  BackingStore
*
*  C++ class that for this example just exposes a single,
*  hard-coded getter function. In reality this class is
*  much larger.
*/
class BackingStore {
public:
BackingStore()  {
NSLog(@"BackingStore constructor.");
}

~BackingStore() {
NSLog(@"BackingStore destructor.");
}

NSString *name() const {
return @"Amazon";
}

// Given a shared_ptr to a BackingStore instance, this method
// will dynamically create a new Objective-C class. The new
// class will contain Objective-C properties that are backed
// by the given BackingStore.
//
// Much of this code is hard-coded for this example. In reality,
// a much larger number of properties are dynamically created
// with different return types and a new class pair is
// only created if necessary.

static id makeBridge(std::shared_ptr<BackingStore> storePtr) {

// For this example, just create a new class pair each time.

NSString *klassName = NSUUID.UUID.UUIDString;
Class klass = objc_allocateClassPair(StoreBridge.class, klassName.UTF8String, 0);

// For this example, use hard-coded values and a single iVar definition. The
// iVar will store an Objective-C block as an 'id'.

size_t ivarSize = sizeof(id);
NSString *ivarName = @"_storeNameIvar";
NSString *encoding = [NSString stringWithFormat:@"%s@", @encode(id)];
SEL selector = @selector(storeName);

// Implementation for @property.storeName on StoreBridge. This
// implementation will read the block stored in the instances
// iVar named "_storeNameIvar" and call it. Fixed casting to
// type 'stringBlock' is used for this example only.

IMP implementation = imp_implementationWithBlock((id) ^id(id _self) {
Ivar iv = class_getInstanceVariable([_self class], ivarName.UTF8String);
id obj = object_getIvar(_self, iv);

return ((stringBlock)obj)();
});

// Add iVar definition and property implementation to newly created class pair.

class_addIvar(klass, ivarName.UTF8String, ivarSize, rint(log2(ivarSize)), @encode(id));
class_addMethod(klass, selector, implementation, encoding.UTF8String);

objc_registerClassPair(klass);

// Create instance of the newly defined class.

id bridge = [[klass alloc] init];

// Capture storePtr in an Objective-C block. This is the block that
// will be stored in the instance's iVar. Each bridge instance has
// its own backingStore, therefore the storePtr must be set on the
// instance's iVar and not captured in the implementation above.

id block = ^NSString* { return storePtr->name(); };
Ivar iva = class_getInstanceVariable(klass, ivarName.UTF8String);

// Assign block to previously declared iVar. When the strongDefault
// method is used, the shared_ptr will leak and the BackingStore
// will never get deallocated. When object_setIvar() is used,
// the BackingStore will get deallocated but crashes at
// runtime as 'block' is not retained anywhere.
//
// The documentation for object_setIvar() says that if 'strong'
// or 'weak' is not used, then 'unretained' is used. It might
// "work" in this example, but in a larger program it crashes
// as 'block' goes out of scope.

#define USE_STRONG_SETTER 1

#if USE_STRONG_SETTER
object_setIvarWithStrongDefault(bridge, iva, block);
#else
object_setIvar(bridge, iva, block);
#endif

return bridge;
}
};

int main(int argc, char *argv[]) {
@autoreleasepool {
std::shared_ptr<BackingStore> storePtr = std::make_shared<BackingStore>();
StoreBridge *bridge = BackingStore::makeBridge(storePtr);

NSLog(@"bridge.storeName: %@", bridge.storeName);

// When USE_STRONG_SETTER is 1, output is:
//
//   > BackingStore constructor.
//   > bridge.storeName: Amazon
//   > StoreBridge DEALLOC

// When USE_STRONG_SETTER is 0, output is:
//
//  > BackingStore constructor.
//  > bridge.storeName: Amazon
//  > BackingStore destructor.
//  > StoreBridge DEALLOC
}
}

2

Решение

Давайте прыгнем в машину времени очень быстро, C.A. 2010. Это более простое время, прежде чем иметь дело с мульти-архитектурными срезами, 64-битными и другими причудливыми вещами, такими как, что важно, ARC.

В этом, казалось бы, далеком мире до сегодняшнего дня, когда у вас была память, вы должны были выпустить ее сами удушье. Это означало, что если у вас был iVar в вашем классе, вы должны были явно, внутри dealloc вызов release в теме.

Ну, это на самом деле не меняется с ARC. Единственное, что меняется, это то, что компилятор генерирует все эти хорошие release призывает вас внутри dealloc, даже если вы не определили метод. Как мило.

Проблема здесь, однако, заключается в том, что компилятор фактически не знает о вашем iVar, содержащем блок — он полностью определяется во время выполнения. Так как же компилятор может освободить память?

Ответ — нет. Вам нужно будет сделать немного магии, чтобы убедиться, что вы выпустили этот материал во время выполнения. Мое предложение состояло бы в том, чтобы перебрать iVars класса и установить их в nilвместо непосредственного вызова objc_release (так как это вызывает много плача и скрежета зубов, если вы используете ARC).

Что-то вроде этого:

for (ivar in class) {
if ivar_type == @encode(id) {
objc_setIvar(self, ivar, nil)
}
}

Теперь, если вы когда-нибудь войдете и добавите преднамеренно __unsafe_unretained ivar в этот класс, у вас, возможно, возникнут дополнительные проблемы. Но ты действительно не должен наследовать от таких классов, ммкай?

2

Другие решения

Других решений пока нет …

По вопросам рекламы [email protected]