在Objective-C Block中泄漏C ++ shared_ptr

时间:2018-02-14 22:10:36

标签: c++ objective-c objective-c-blocks objective-c++ objective-c-runtime

要点:

在下面的示例应用程序中,在Objective-C块中捕获shared_ptr。使用Objective-C运行时API ivar将Objective-C块分配给动态创建的类的object_setIvarWithStrongDefault。取消分配Objective-C对象时,shared_ptr正在泄漏,并且不会删除它保留的C ++对象。这是为什么?

当使用object_setIvar代替时,会阻止泄漏,但是当ivar假定分配object_setIvar时,unsafe_unretained指向块超出范围时为垃圾

假设这与Objective-C如何捕获C ++对象,复制块以及shared_ptr处理复制的方式有关,但我希望有人可以对此进行更多阐述比下面列出的文件。

参考文献:

背景故事:

此示例代码是从一个更大的项目中提取的,并且已经显着降低到显示问题所需的最低限度。该项目是Objective-C macOS应用程序。该应用程序包含几个单独的C ++对象,这些对象是美化的键/值存储。每个对象都是同一个类的实例,但是在键类型上是模板化的。我想动态创建一个Objective-C类,它包含由C ++类支持的类型属性getter。

(是的,这可以全部通过自己编写大量的getter来手动完成,但我不愿意这样做.C ++类有足够的信息来知道属性的名称和他们的类型,因此我想使用一些元编程技术来解决这个问题。)

备注:

在理想的世界中,我能够在适当的iVar类型的Objective-C类上定义shared_ptr,但我无法弄清楚如何使用Objective-C运行时API执行此操作。

鉴于此:

std::shared_ptr<BackingStore<T>> backingStore

你如何使用它:

class_addIvarobject_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
  }
}

1 个答案:

答案 0 :(得分:2)

让我们快速跳进时间机器,C.A。在处理多架构切片,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,您可能会遇到更多问题。但你真的不应该继承这样的课程,mmkay?