代码之家  ›  专栏  ›  技术社区  ›  kennyc

在Objective-C块中泄漏C++shared\u ptr

  •  3
  • kennyc  · 技术社区  · 6 年前

    总结:

    在下面的示例应用程序中 shared_ptr 正在Objective-C块中捕获。正在将Objective-C块分配给 ivar 使用Objective-C运行时API动态创建的类的 object_setIvarWithStrongDefault . 释放Objective-C对象时 共享\u ptr 正在泄漏,并且未删除其保留的C++对象。为什么会这样?

    什么时候 object_setIvar 则可以防止泄漏,但 ivar公司 一旦块超出范围,则指向垃圾 object\u setIvar对象 假定分配 unsafe_unretained .

    假定 这与Objective-C如何捕获C++对象、复制块以及如何 共享\u ptr 句柄正在被复制,但我希望有人能对此有所了解,而不仅仅是下面列出的文档。

    参考文献:

    背景故事:

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

    (是的,这一切都可以通过自己编写大量getter来手动完成,但我不愿意。C++类有足够的信息来知道属性的名称及其类型,因此我想使用一些元编程技术来“解决”这一问题。)

    笔记:

    在理想的世界里,我只能定义 iVar 关于适当的Objective-C类 共享\u ptr 但是我不知道如何使用Objective-C运行时API来实现这一点。

    鉴于此:

    std::shared_ptr<BackingStore<T>> backingStore

    如何使用:

    class_addIvar object\u setIvar对象

    由于我无法理解这一点,我决定将shared\u 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 回复  |  直到 6 年前
        1
  •  3
  •   Richard J. Ross III    6 年前

    让我们快速进入时光机,C.a.2010。这是一个更简单的时间,在处理多架构片、64位和其他奇特的东西之前,比如重要的ARC。

    在这个看似遥远的世界里,直到今天,当你有了记忆,你必须自己释放它 喘息 . 这意味着,如果你的类中有一个iVar,你必须显式地,在 dealloc 呼叫 release 在上面。

    实际上,这并不随弧度变化。唯一改变的是 编译器 产生所有这些美好的 释放 在内部呼叫您 释放内存 ,即使没有定义方法。多好啊。

    然而,这里的问题是编译器实际上并不知道包含该块的iVar—它完全是在运行时定义的。那么编译器如何释放内存呢?

    答案是没有。你需要做一些魔术来确保在运行时发布这些东西。我的建议是迭代类的ivar,并将它们设置为 nil ,而不是直接调用objc\u release(因为如果使用ARC,会导致大量的哭泣和咬牙切齿)。

    类似这样:

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

    现在,如果您在这个类中故意添加一个\uu不安全\u未维护的ivar,您可能会遇到更多问题。但你真的不应该继承这样的类,嗯?