iOS内存管理中的强引用问题

iOS内存管理

关于强引用循环

强引用循环是 ARC 无法自动处理的常见问题。如果两个对象互相强引用对方,就会造成引用计数不为零,导致对象无法释放。典型的情况是在闭包中引用 self 时,self 和闭包之间可能会互相持有,形成强引用循环。

解决方法: 使用 [weak self][unowned self] 来避免强引用循环,特别是在闭包内。

强引用(Strong Reference)是指一个对象在内存中持有对另一个对象的引用时,这个引用会增加被引用对象的引用计数。当一个对象的引用计数增加时,它的生命周期就被延长,直到引用计数变为零,表示没有任何对象引用它时,ARC 会自动释放这个对象的内存。

在 iOS 中,默认情况下,所有对象的引用都是强引用。强引用会导致引用的对象在内存中不会被释放,直到所有强引用都解除。

强引用的基本理解

  • 强引用 :指对象通过 strong 持有另一个对象。该对象的生命周期会被引用方延长,直到引用方解除对它的持有。
  • 当两个对象之间通过强引用相互持有时,就会形成 强引用循环(Retain Cycle),这种情况下,即使它们不再需要对方,它们的引用计数也不会降到零,从而导致内存泄漏。

强引用的例子

1. 基本的强引用示例

假设你有两个对象:PersonCarPerson 强引用 Car,意味着只要 Person 存在,Car 就不会被释放。

swift 复制代码
class Person {
    var car: Car?
}

class Car {
    var owner: Person?
}

var john: Person? = Person()
var toyota: Car? = Car()

john?.car = toyota
toyota?.owner = john

在这个例子中,john 对象强引用了 toyota 对象,同时 toyota 也强引用了 john 对象。由于两个对象互相持有对方,即使这两个对象的生命周期已经结束,它们的引用计数也不会变为零,因为它们彼此依赖,这就形成了 强引用循环。这会导致内存泄漏。

2. 解决强引用循环

可以通过使用 弱引用weak)来打破这种循环,确保当 PersonCar 不再需要时,它们能正确释放。

swift 复制代码
class Person {
    var car: Car?
}

class Car {
    weak var owner: Person?  // 使用 weak 避免强引用循环
}

var john: Person? = Person()
var toyota: Car? = Car()

john?.car = toyota
toyota?.owner = john

在这个例子中,owner 属性使用 weak 修饰符,这样当 john 被释放时,toyota 中的 owner 引用就会变为 nil,从而打破强引用循环。

3. 强引用与内存管理的关系

当我们使用强引用时,ARC 会增加被引用对象的引用计数。下面是一个具体例子,说明如何影响内存管理:

swift 复制代码
class Dog {
    var name: String
    init(name: String) {
        self.name = name
    }
    deinit {
        print("\(name) is being deinitialized")
    }
}

var dog1: Dog? = Dog(name: "Buddy")
var dog2 = dog1  // dog2 强引用 dog1

dog1 = nil  // dog1 被置为 nil,但是 dog2 仍然持有 Buddy 的引用

在上面的代码中:

  • dog1 被赋值为 nil 时,它不再持有 Buddy 对象。
  • 但是由于 dog2 仍然强引用着 BuddyBuddy 对象并不会立即被释放。
  • Buddy 只有当 dog2 也被置为 nil 后,才会释放内存。
4. 常见场景:闭包中的强引用

闭包是导致强引用循环的一个常见场景。例如,下面的代码就会导致强引用循环:

swift 复制代码
class ViewController: UIViewController {
    var someProperty: String = "Hello"
    
    func setupClosure() {
        let closure = {
            print(self.someProperty)  // 闭包捕获了 self
        }
        closure()
    }
}

let vc = ViewController()
vc.setupClosure()

在上面的代码中,closure 捕获了 self(即 ViewController 的实例),这意味着 ViewController 对象将始终被闭包引用,self 的引用计数不会变为零,导致 ViewController 永远不会被释放。为了避免这种情况,我们通常使用 弱引用无主引用

swift 复制代码
func setupClosure() {
    let closure: () -> Void = { [weak self] in
        print(self?.someProperty ?? "")
    }
    closure()
}

使用 [weak self] 来捕获 self,可以避免闭包对 ViewController 的强引用,从而避免强引用循环。

但需要注意的是,并不是所有的闭包都会产生强引用

不会产生强引用的情况

在闭包中使用 weakunowned 主要是为了避免 强引用循环 (retain cycle),尤其是当闭包捕获 self 时。是否需要使用 weakunowned 取决于闭包与被捕获对象之间的生命周期关系。以下是几种情况,你可以根据这些情况来判断何时不需要使用 weakunowned

1. 闭包的生命周期比捕获的对象短

如果闭包的生命周期比被捕获的对象短(即闭包不会持有对象,且对象的生命周期不会依赖于闭包),则无需使用 weakunowned

示例

  • 闭包作为局部变量在方法内使用,并且被调用后立即销毁。
  • 闭包没有持有对对象的引用,且被调用时对象已被销毁。
swift 复制代码
class MyClass {
    var value = 10
    
    func performAction() {
        // 闭包在执行后立即销毁,不会产生强引用循环
        let closure = {
            print(self.value)
        }
        closure()
    }
}

let obj = MyClass()
obj.performAction()

在这个例子中,闭包在方法内部执行时,仅仅是一个局部变量。它不会在方法外部持有 self,且方法执行完毕后,闭包就会被销毁,所以不需要使用 weakunowned 来捕获 self

2. 闭包被存储在对象的属性中,并且对象的生命周期与闭包相同

如果闭包是对象的一个属性,并且该对象的生命周期与闭包相同(即闭包和对象一起销毁),则也不需要使用 weakunowned

例如,闭包可能会作为一个委托回调、通知、或者是一些任务的完成闭包。如果该对象本身不会被提前销毁,闭包引用 self 是安全的,因为对象的生命周期会保持直到闭包不再需要。

示例

swift 复制代码
class Task {
    var completion: (() -> Void)?
    
    func start() {
        completion = {
            print("Task is done")
        }
    }
}

let task = Task()
task.start()

在这个例子中,Task 对象持有 completion 闭包,闭包本身不会引发强引用循环,因为 Task 对象会一直存在直到 completion 被销毁。这里没有强引用循环问题。

3. 捕获的对象不会导致内存泄漏

如果闭包捕获的对象是一个"非持有"对象(例如全局对象、常量、静态对象),或者闭包执行时被捕获的对象本身在执行完成后就会被销毁(比如临时创建的对象),则不需要使用 weakunowned

示例

swift 复制代码
class MyClass {
    func fetchData() {
        let completion: () -> Void = {
            print("Data fetched!")
        }
        completion()
    }
}

let instance = MyClass()
instance.fetchData()

在这个例子中,completion 闭包没有捕获 self,因此不会形成强引用循环。并且 completion 的生命周期非常短,执行完成后就会被销毁。

4. 闭包没有捕获 self

如果闭包本身不捕获 self(即没有使用 [weak self][unowned self]),且闭包本身不会导致引用循环,也可以不使用 weakunowned。这种情况下,闭包本身不会持有对象。

示例

swift 复制代码
class MyClass {
    var name = "Hello"
    
    func fetchData() {
        let completion: () -> Void = {
            print("Data fetched!")
        }
        completion()
    }
}

let instance = MyClass()
instance.fetchData()

这里 completion 不捕获 self,所以 MyClass 对象不会因为闭包而延长生命周期。

总结:

我们需要注意闭包和实例是否会相互引用,还需要注意闭包和实例的生命周期

你不需要使用 weakunowned 来捕获 self 的情况包括:

  1. 闭包的生命周期比捕获的对象短,例如临时的局部闭包。
  2. 闭包被存储在对象属性中,并且对象的生命周期与闭包相同。
  3. 闭包没有捕获 self,且不涉及持有对象的引用。
  4. 捕获的对象不会导致内存泄漏,例如全局变量、常量等对象。
相关推荐
酷虎软件5 小时前
分享链接+视频音频文案提取 API 接口文档
ide·macos·xcode
Hyyy10 小时前
为什么 macOS 应用一换 Bundle ID,之前授予的权限就全失效了?
macos·electron
CocoaKier10 小时前
Facebook登录的这条红色警告,我终于搞明白了,真是反人类设计!
ios·facebook
p似笑非笑11 小时前
实战验证——把 SDK 塞进一个 macOS 原生 Agent 应用
macos
humors22114 小时前
【转帖】安全企业发布iOS漏洞攻击风险检测工具
安全·ios·手机·苹果
p似笑非笑15 小时前
在Mac上完美配置VSCode的C/C++开发环境(GCC/G++详细教程)
c语言·vscode·macos
沐风___17 小时前
iOS 崩溃收集实战:从「只看到次数、看不到原因」到 Sentry 完整集成
ios·sentry
listening77718 小时前
HarmonyOS 6.1 跨端实战:用ArkUI-X把电商App同时跑在Android/iOS上
android·ios·harmonyos
韦胖漫谈IT19 小时前
Apple M3 Max 与 Apple M5 Max 对比:本地算力的新旧王者之争
网络·人工智能·macos·transformer
Tyler_119 小时前
iOS PlayWright mcp server
前端·ios·ai编程