在 Rust 中使用 Objective-C 时管理 cocoa 内存的正确方法

Sci*_*eSE 1 cocoa objective-c ffi rust

我正在努力解决与可可基金会内存管理相关的一个问题。基本上我有一个用 Rust 编写的项目,我正在使用Objective-Ccocoa-rsobjc-rs与之交互。我熟悉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
}
Run Code Online (Sandbox Code Playgroud)

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

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

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

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

我尝试过的:

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

难道我做错了什么?或者它是一个错误objc-rs(我猜这不太可能,但谁知道)?

ken*_*ytm 5

由于 Objective-C ARC 未在objc-rs/ 中实现cocoa-rs,因此您需要遵循内存管理规则,特别是对于这个问题:您不得放弃不属于您的对象的所有权。也就是说,您不应调用autorelease(),release()dealloc()任何返回的对象。

你应该做的是在函数内部创建一个 NSAutoreleasePool不要碰其他任何东西。池将在释放时释放所有这些对象。

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
}
Run Code Online (Sandbox Code Playgroud)

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

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

  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());
      
      Run Code Online (Sandbox Code Playgroud)

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

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

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

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

      但是您从未调用过retain,因此您仍然不拥有任何对象。

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

    • 在 Rust 中,发送-release消息相当于丢弃 Rc 对象。

      // Objective-C code:
      [u release];
      
      // Similar to this in Rust:
      drop(u);
      
      Run Code Online (Sandbox Code Playgroud)
    • -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.
      
      Run Code Online (Sandbox Code Playgroud)
  4. 您不得放弃不属于您的对象的所有权

    • 那就是你永远不能通过借用的引用来删除内存。在 Rust 中这是不可能的,但 Objective-C 没有借用检查器。
  5. 此外,调用-dealloc就像drop(*s)在 Rust 中显式调用析构函数一样。这绕过了引用计数机制,并且明确不鼓励这样做

让我们回顾一下:

  • 的方法都没有你所谓的(sharedWorkspace/ runningApplications/ objectAtIndex:/ bundleURL/ bundleWithURL:/ infoDictionary)开始alloc/ new/ copy/ mutableCopy
  • 你从来没有打过电话-retain
  • 这意味着你所拥有的一切都是借来的,根据规则 1 和 2。
  • 这意味着release()autorelease()根据规则 4,您永远不应该调用or

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

如果我们不提供 NSAutoreleasePool,为什么程序会像筛子一样泄漏?该runningApplications/bundleWithURL:方法做分配对象,但秉承可可内存管理规则,他们称之为-autorelease内部,以确保你没有得到所拥有的对象。但是如果我们不分配任何池,-autorelease就可以将所有权转移到任何地方,即那些自动释放的对象成为任何人都不拥有的,没有人拥有释放它们的所有权,从而泄漏。


(*):这个类比并不完美,因为您可以通过[[x autorelease] retain]. 但是这个细节在这里无关紧要。