【问题标题】:The correct way to manage cocoa memory when working with Objective-C from Rust从 Rust 使用 Objective-C 时管理可可内存的正确方法
【发布时间】:2017-03-30 19:59:35
【问题描述】:

我正在努力解决与可可基金会内存管理相关的一个问题。基本上我有一个项目,用 Rust 编写,我使用 cocoa-rsobjc-rs 与 Objective-C 交互。我熟悉 CoreFoundation 和 CocoaFoundation 中的内存管理(我已经阅读了文档中的相应文章)。当我使用 CoreFoundation 函数时,我没有任何内存问题,但是当我使用 CocoaFoundation 相关的东西时,我遇到了很多问题,似乎从 CocoaFoundation 获取任何对象都会泄漏内存。

这是导致记忆记忆的功能之一的简化​​版本:

pub fn enumerate_apps()-> Vec<Rc<AppInfo>> {
    let mut apps_list = Vec::new();
    unsafe {
        let shared_workspace: *mut Object = msg_send![class("NSWorkspace"), sharedWorkspace];
        let running_apps: *mut Object = msg_send![shared_workspace, runningApplications];

        let apps_count = msg_send![running_apps, count];
        for i in 0..apps_count {
            let app: *mut Object = msg_send![running_apps, objectAtIndex:i];

            // Those ones are not used at the moment, but I actually need them,
            // I just removed all business logic to keep the example simple and compilable
            // to demonstrate the problem.
            let bundle_url: *mut Object = msg_send![app, bundleURL];
            let app_bundle: *mut Object = msg_send![class("NSBundle"), bundleWithURL:bundle_url];
            let info_dict: *mut Object = msg_send![app_bundle, infoDictionary];

            apps_list.push(Rc::new(AppInfo {
                pid: msg_send![app, processIdentifier],
            }));
        }
    }
    apps_list
}

我尝试在循环中调用此函数以使内存泄漏可见:

fn main() {
    loop {
        for i in 0..200 {
            enumerate_apps();
        }
        std::thread::sleep(std::time::Duration::from_millis(5000));
    }
}

当我运行应用程序时,我可以看到随着时间的推移它消耗的内存越来越多。

我的问题是:为什么?在这样的 FFI 代码中管理内存的正确方法是什么?如果我在 XCode 中使用普通的 Objective-C 运行相同的代码,它可以正常工作并且似乎不会泄漏内存。嗯,XCode中之所以不泄露内存,是因为默认启用了ARC。据我所知,当我们以这种方式使用 Rust 的 Objective-C 时,ARC 并没有启用,所以基本上这意味着我们必须自己管理内存。注释包含bundle_urlapp_bundleinfo_dict 的 3 行会造成内存泄漏消失的错觉(不注释它们,进程每 2 秒泄漏几兆字节的内存),但实际上内存仍然泄漏,但没那么快。

我尝试了什么:

  1. 我尝试在函数的开头创建一个NSAutoreleasePool,并在创建时为bundle_urlapp_bundle 调用autorelease()。没用,内存还是泄露了。
  2. 我尝试在bundle_urlapp_bundle上手动调用release(),没有任何效果。
  3. 甚至尝试在他们身上拨打dealloc()(我认为这是错误的方式),这也无助于解决我的问题。

我做错了吗?还是objc-rs 中的错误(我想这不太可能,但谁知道)?

【问题讨论】:

    标签: objective-c cocoa rust ffi


    【解决方案1】:

    由于 objc-rs/cocoa-rs 中没有实现 Objective-C ARC,您需要遵循 memory management rule,特别是对于这个问题:您不得放弃您不拥有的对象的所有权。也就是说,您不应在任何返回的对象上调用 autorelease()release()dealloc()

    你应该做的是create an NSAutoreleasePool inside the function不要碰其他任何东西。池将在释放时释放所有这些对象。

    pub fn enumerate_apps()-> Vec<Rc<AppInfo>> {
        let mut apps_list = Vec::new();
        unsafe {
            let autoreleasePool: *mut Object = msg_send![class("NSAutoreleasePool"), new];
    
            // ...
            // all code unchanged
            // ...
    
            msg_send![autoreleasePool, release];
        }
        apps_list
    }
    

    为什么在bundle_url/app_bundle/info_dict上调用autorelease()/release()/dealloc()不能减少内存?因为不仅仅是这些对象泄漏内存。最大的消耗是running_apps 对象。

    为什么显式调用autorelease()/release()/dealloc() 是错误的?让我们回顾一下 ObjC 内存管理规则,并将其与普通的 Rust 代码进行比较(我假设你知道 Rc&lt;T&gt; 类型是如何工作的):

    1. 您拥有您创建的任何对象 - 您使用名称以“alloc”、“new”、“copy”或“mutableCopy”开头的方法创建对象

      • 你可以这样想:

        // Objective-C code:
        NSMutableString* s = [NSMutableString new];
        NSMutableString* t = [s mutableCopy];
        
        // Similar to this in Rust:
        let s: Rc<NSMutableString> = Rc::new(NSMutableString::new());
        let t: Rc<NSMutableString> = Rc::new(s.mutableCopy());
        

        您的代码从未调用任何以“alloc”、“new”、“copy”或“mutableCopy”开头的方法,因此您不拥有它们中的任何一个。所有 ObjC API 都遵循此命名约定。

    2. 您可以使用保留取得对象的所有权

      • 这类似于拥有一个对象a: Rc&lt;T&gt;,然后您通过调用b = Rc::clone(&amp;a) 获得一个新的引用。现在b 也通过引用计数“拥有”原始对象:

        // Objective-C code:
        NSMutableString* u = [t retain];
        
        // Similar to this in Rust:
        let u: Rc<NSMutableString> = Rc::clone(&u);
        

        但是你从来没有打电话给retain,所以你仍然没有任何对象。

    3. 当您不再需要它时,您必须放弃您拥有的对象的所有权 — 您通过向对象发送 release 消息或 autorelease 消息来放弃对象的所有权。 p>

      • 就 Rust 而言,发送 -release 消息相当于丢弃 Rc 对象。

        // Objective-C code:
        [u release];
        
        // Similar to this in Rust:
        drop(u);
        
      • -autorelease 将所有权转移到自动释放池。将找到最近分配的 NSAutoreleasePool,将对象的所有权移到该池中,并且我们只保留借用的引用(*)

        // Objective-C code:
        NSMutableString* v = [t autorelease];
        
        // Similar to this in Rust:
        let pool: &NSAutoreleasePool = find_top_autorelease_pool()?;
        let v: &NSMutableString = pool.add_object(t);
        // `t` is passed-by-value, so `pool` now owns `t`.
        // `pool` returns a borrowed reference, 
        // so that we can still access the memory pointed to by `t`,
        // but we no longer own it.
        
    4. 您不得放弃对不属于您的对象的所有权

      • 也就是说,您永远不能通过借用的引用删除内存。在 Rust 中这是不可能的,但 Objective-C 没有借用检查器。
    5. 此外,调用-dealloc 就像在Rust 中通过drop(*s) 显式调用析构函数。这绕过了引用计数机制和is explicitly discouraged

    让我们回顾一下:

    • 你调用的方法(sharedWorkspace/runningApplications/objectAtIndex:/bundleURL/bundleWithURL:/infoDictionary)都不是以alloc/new/copycopy/987654363@/ @。
    • 你从来没有打电话给-retain
    • 这意味着你所拥有的一切都是借来的,按照规则 1 和 2。
    • 这意味着你不应该根据规则 4 致电 release()autorelease()

    在您不拥有的对象上调用 -release-autorelease 会导致双重释放。这可能会导致 SEGFAULT、无操作或任何未定义的行为。

    如果我们不提供 NSAutoreleasePool,为什么程序会像筛子一样泄漏? runningApplications/bundleWithURL: 方法确实分配对象,但遵循 Cocoa 内存管理规则,它们在内部调用 -autorelease 以确保您不会获得拥有的对象。但是如果我们不分配任何池,-autorelease 可以将所有权转移到任何地方,即那些自动释放的对象变得不为任何人所拥有,并且没有人拥有释放它们的所有权,从而泄漏。


    (*):这个类比并不完美,因为您可以使用[[x autorelease] retain] 获得新的所有权。但是这个细节在这里并不重要。

    【讨论】:

    • 谢谢,确实有效!我试图通过cocoa-rs 绑定创建NSAutoreleasePool,但事实证明,它们从未调用release,因此创建NSAutoreleasePool 本身就泄漏了内存。
    • 我只是想知道,我们为什么不将autorelease 称为NSBundle::bundleWithUrl()?根据文档,该函数可能会分配和初始化内存,是不是意味着我们必须在上面调用autorelease
    • @ScienceSE NSBundle::bundleWithUrl() 返回一个已经是 -autoreleased 的对象。再次调用-autorelease 将双重释放它。
    • 谢谢,问题现在解决了(我现在使用NSAutoreleasePool 来自cocoa-rsdrain 明确)。我只是想知道为什么当我尝试在每个对象上手动调用 release 时它不起作用。自动释放的对象是否忽略release
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-10-10
    • 2020-01-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多