干货满满,深度解析 iOS 各种引用类型的性能差异

943 阅读5分钟

这里每天分享一个 iOS 的新知识,快来关注我吧

前言

作为当代的 iOS 开发者,我们似乎很少关心引用计数了,因为现在系统使用自动引用计数(ARC)简化了内存管理的过程,但有时我们需要更高级的技术来处理内存管理问题。

今天,我们来探讨一些 iOS 开发中不同的内存管理技术,并进行性能对比。

ARC 技术

首先我们先来了解 ARC 下不同的引用类型:

强引用(Strong References)

强引用是最常见的对象引用方式。创建变量或常量,或者在另一个对象的属性中保存对对象的引用,这些都会创建一个强引用。

先说结论吧,这种引用类型性能最好,是最快的。通过强引用访问变量时,Swift 可以跳过任何运行时检查,因为编译器保证对象始终存在。

弱引用(Weak References)

我们通常在闭包中为了防止循环引用时使用弱引用,但从性能角度来看,这是最慢的引用类型。

为啥呢?首先,如果对象不存在,它会返回 nil,因此在运行时需要进行额外的检查。其次,弱引用指向一个 side-table,即使对象存在,我们也必须进行两次解引用(先指向 side-table → 指向对象)。

关于 side-table,是一个底层实现机制,能够在提升性能和内存使用效率的同时,保持对象的轻量化,关于这块儿内容,以后专门写文章给大家讲解。

非拥有引用(Unowned References)

在 iOS 面试中,一个非常常见的问题是:“弱引用和非拥有引用有什么区别?”。关于这个问题之前专门写文章探讨过,可以去翻翻之前的文章。

iOS 中 weak self 和 unowned self 的区别是什么?

访问对象时,非拥有引用比弱引用更快,因为非拥有引用已经直接指向对象。但 Swift 仍会进行额外的检查,如果对象已被释放,则会抛出运行时错误(Crash)。

unowned(unsafe)

在 ARC 中,我们还有另一种引用类型,称为 unowned(unsafe)

unowned(unsafe) 是 unowned 的一种变体,它在 Swift 5 中被引入,主要用于与 Objective-C 和 C 的互操作。与普通的 unowned 类似,它表示一个非拥有引用,但它更为危险,因为它不会在运行时进行任何安全检查。与非拥有(Unowned References)引用类似,ARC 不会增加引用计数。但对于这种类型,Swift 不会进行任何检查,也不会抛出运行时错误。

它仅仅返回给定地址的内容,这可能是一个对象、垃圾值或什么都没有。因此,这可能导致悬挂指针和未定义行为。

开发过 OC 的同学应该有点印象,unowned(unsafe) 就是源于 Objective-C,在 OC 里叫 __unsafe_unretained

手动引用计数

有时你可能比系统更清楚对象应该在何时存在和销毁。为了获得更高的性能,你可以使用手动引用计数。也就是 Unmanaged。

Unmanaged 是 Swift 中用于处理与 Core Foundation 和其他非 ARC 管理的 C APIs 之间桥接的一种机制。它允许你绕过 Swift 的自动引用计数(ARC)系统,手动管理对象的引用计数。这在与 Core Foundation 或其他 C 库互操作时非常有用,因为这些库通常不使用 ARC。

实际上,Unmanaged 内部使用的是 unowned(unsafe),并提供了多种手动引用管理方法,如 retain/release 等。因此,你可以通过手动管理的方式消除 ARC 的开销。

不同引用类型性能测试

为了测试不同引用类型的开销,我们可以分别测试强引用(strong)、弱引用(weak)、非拥有引用(unownedunowned(unsafe))、以及 Unmanaged 的性能。

以下是 Demo 的代码:

import Foundation

// 测试次数
let iterations = 10000000

func testStrong() -> Double {
    let obj = TestClass(value: 42)
    return measureTime {
        for _ in 0..<iterations {
            obj.value = obj.value + 1
        }
    }
}

func testWeak() -> Double {
    let obj = TestClass(value: 42)
    let weakRef = WeakRef(value: obj)
    return measureTime {
        for _ in 0..<iterations {
            if let value = weakRef.value?.value {
                weakRef.value?.value = value + 1
            }
        }
    }
}

func testUnowned() -> Double {
    let obj = TestClass(value: 42)
    let unownedRef = UnownedRef(value: obj)
    return measureTime {
        for _ in 0..<iterations {
            let value = unownedRef.value.value
            unownedRef.value.value = value + 1
        }
    }
}

func testUnownedUnsafe() -> Double {
    let obj = TestClass(value: 42)
    let unownedUnsafeRef = UnownedUnsafeRef(value: obj)
    return measureTime {
        for _ in 0..<iterations {
            let value = unownedUnsafeRef.value.value
            unownedUnsafeRef.value.value = value + 1
        }
    }
}

func testUnmanaged() -> Double {
    let obj = TestClass(value: 42)
    let unmanagedRef = UnmanagedRef(value: obj)
    return measureTime {
        for _ in 0..<iterations {
            let value = unmanagedRef.value.takeUnretainedValue().value
            unmanagedRef.value.takeUnretainedValue().value = value + 1
        }
    }
}

这段代码中,我们定义了五个测量函数,分别测试不同引用类型的性能。每个函数对 TestClass 对象进行了 10000000 次操作(访问和设置 value 属性)。最后,我们记录每种引用类型的执行时间,并打印出来。

总结

从结果可以看出,强引用是最快的。Unmanaged 和 unowned(unsafe) 的性能相同,因为 Unmanaged 内部使用了 unowned(unsafe)。它们比强引用慢,因为编译器在访问由这些引用管理的变量时插入了额外的 retain。非拥有引用稍慢,因为需要进行运行时检查。最后,弱引用的性能最差。

希望这篇文章对你有所帮助!如果我遗漏了一些技术,请在评论中告诉我们。别忘了点赞和分享哦!

这里每天分享一个 iOS 的新知识,快来关注我吧

本文同步自微信公众号 “iOS新知”,每天准时分享一个新知识,这里只是同步,想要及时学到就来关注我吧!