内存管理

YVTU

内存管理

引用计数

iOS的Objective-C和Swift运行时使用引用计数来管理对象的生命周期。新建对象时引用计数+1,指针指向对象时+1,指针不再指向对象时-1,当引用计数为0时对象被销毁。

TaggedPointer为小型数据(如NSNumber、NSDate、NSString等)提供的一种内存节省技术。TaggedPointer是一个特别的指针,它分为两部分:一部分直接保存数据,另一部分作为特殊标记。TaggedPointer是一种特别的指针,用于存储小型数据,以减少内存占用。它分为数据部分和标记部分,不指向任何实际的内存地址。

ARC与MRC的区别

ARC(自动引用计数)是iOS 5及之后版本引入的一种编译器特性,自动管理对象的生命周期,减少了内存泄漏和野指针的风险。ARC下,开发者不再需要手动调用retain、release、autorelease等内存管理方法。

MRC(手动引用计数)在ARC之前,开发者需要手动管理对象的内存,通过retain、release、autorelease等方法来控制对象的生命周期。

SideTables和SideTable

SideTables是一个包含8个SideTable的哈希数组,用于存储对象的引用计数和弱引用信息。每个SideTable对应多个对象。

SideTable包含三个主要成员:自旋锁(spinlockt)、引用计数表(RefcountMap)、弱引用表(weaktable_t)。自旋锁用于防止多线程访问冲突,引用计数表存储对象的引用计数,弱引用表存储对象的弱引用信息。

weak_table_t和weak_entry_t

weak_table_t是一个存储弱引用信息的哈希表,其元素是weak_entry_t类型。

weak_entry_t存储了弱引用该对象的指针的指针,即objc_object new_referrer。当对象被销毁时,weak引用的指针会被自动置为nil,防止野指针的出现。

相关提案和技术演进

相关提案包括 SE-0349 Unaligned Loads and Stores from Raw MemorySE-0334 Pointer API Usability ImprovementsSE-0333 Expand usability of withMemoryRebound

Set 使用新的 Temporary Buffers 功能,让 intersect 速度提升了 4 到 6 倍。

自动引用计数(ARC)机制

ARC 是 Swift 用来管理应用程序内存的技术,它会自动跟踪和管理你的应用程序使用的内存,不需要开发者手动分配或释放内存。

ARC 的基本原理

ARC 在对象的生命周期内自动管理内存的分配与释放。每当你创建一个类的实例时,ARC 会为这个实例分配内存,并在不再需要它时自动释放内存。ARC 通过维护一个“引用计数”来决定对象的生命周期:每次引用增加时计数加一,每次引用减少时计数减一。当引用计数为零时,ARC 会自动释放对象所占的内存。

强引用、弱引用和无主引用

ARC 的核心在于管理引用类型变量之间的强引用(strong reference)。强引用意味着只要对象被某个变量持有,引用计数就不会为零,对象也就不会被释放。然而,如果两个对象之间形成了强引用循环(即两个对象互相引用),ARC 将无法自动释放它们,导致内存泄漏。

为了解决强引用循环的问题,Swift 引入了弱引用(weak reference)和无主引用(unowned reference)。弱引用不增加对象的引用计数,当对象被释放时,弱引用会自动变为 nil。无主引用则是一种非可选类型引用,当对象被释放时,无主引用不会变为 nil,但如果访问已经被释放的对象则会导致运行时错误。因此,无主引用通常用于对象生命周期一致的情况下,而弱引用则适用于对象可能会独立释放的情况。

循环引用的示例与解决方案

当两个类互相持有对方的强引用时,会导致循环引用问题,导致内存无法正确释放。这类问题通常发生在闭包与类实例之间。为了打破这种循环引用,可以在闭包中使用捕获列表(capture list)将闭包中的引用声明为弱引用或无主引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import SwiftUI

class Element {
let title: String
let description: String?

lazy var convertToWeb: () -> String = { [unowned self] in
if let description = self.description {
return "<div class='line'><h2>\(self.title)</h2><p>\(description)</p></div>"
} else {
return "<div class='line'><h2>\(self.title)</h2></div>"
}
}

init(title: String, description: String? = nil) {
self.title = title
self.description = description
}

deinit {
print("\(title) is being deinitialized")
}
}

struct ContentView: View {
@State private var elm: Element? = Element(title: "Inception", description: "A mind-bending thriller by Christopher Nolan.")

var body: some View {
VStack {
if let html = elm?.convertToWeb() {
Text(html)
.padding()
.background(Color.yellow)
.cornerRadius(10)
}
Button("Clear") {
elm = nil
}
.padding()
.background(Color.red)
.foregroundColor(.white)
.cornerRadius(10)
}
.padding()
}
}

在这个示例中,convertToWeb 是一个闭包,使用了 [unowned self] 捕获列表,以避免闭包与 Element 实例之间的强引用循环。

闭包中的循环引用

闭包是一种在 Swift 中广泛使用的功能,但由于闭包会捕获它们使用的任何外部变量,可能会引入循环引用。为了防止这种情况发生,Swift 提供了捕获列表,使你可以显式指定在闭包中如何捕获外部变量(如使用 weak 或 unowned 关键字)。

iOS虚拟内存

可以通过ADRP(Address Relative to Page)汇编指令找到特定符号在虚拟内存中的地址。ADRP(Address Relative to Page)是ARM架构中用于计算地址的汇编指令,其作用是计算一个标签(label)或符号相对于当前页面基地址的偏移量,并将这个偏移量乘以页面大小(通常为4KB,即0x1000)后的结果存储到指定的寄存器中。这一指令在处理大范围的内存地址时非常有用,尤其是在虚拟内存环境中,可以有效减少地址计算所需的指令数量。

iOS中的地址空间布局随机化(ASLR, Address Space Layout Randomization)技术。虚拟内存是现代操作系统为了提供更大内存空间、实现内存保护和管理而采用的一种技术。它将物理内存与逻辑内存(即用户视角的内存)分离,通过分页机制(Paging)实现。ASLR则是一种安全特性,它通过在程序加载时随机化代码和数据在内存中的位置,来增加攻击者预测和利用内存地址的难度。

在iOS中,ASLR会导致每次启动App时,App的虚拟内存地址都会发生变化。这就使得在调试过程中,需要通过一些技巧来获取符号的真实地址。其中,一种常用的方法是通过ADRP指令来计算符号的虚拟内存地址。

在ARM64汇编中,ADRP指令的格式如下:

1
ADRP Xd, label

其中,Xd是目标寄存器,label是一个符号的地址。

ADRP指令的作用是将一个符号的地址的高20位加载到寄存器中,然后通过ADD指令将低12位加上去。这样就可以得到一个符号的虚拟内存地址。

在LLDB中,可以通过image list命令查看当前App的ASLR偏移量,然后通过image lookup -n命令查看目标符号的地址。在GDB中,可以通过info sharedlibrary命令查看当前App的ASLR偏移量,然后通过info address命令查看目标符号的地址。

在iOS的Mach-O文件中,符号(如类名、函数名等)的地址在编译链接阶段就已经确定,但在程序运行时可能会受到ASLR的影响而发生变化。然而,在Mach-O文件中直接查看时,这些地址是不包含ASLR偏移量的。因此,为了获取符号在运行时的真实地址,需要通过ADRP指令计算出符号的虚拟内存地址,然后再加上ASLR偏移量。

Swift内存安全性及其实现机制

Swift 通过确保内存的“独占访问”(Exclusive Access)来避免常见的内存安全问题,例如数据竞争(Data Race)和未定义行为(Undefined Behavior)。在 Swift 中,内存的访问方式被严格控制,尤其是在对变量进行读写时,系统会确保在一个时间点上,只有一个代码段可以对该内存进行修改。这一设计通过限制变量的访问方式和时间,极大程度上避免了内存冲突。

内存安全的重要性

内存安全是指在编程中确保内存的正确访问和管理,以避免常见的错误,如越界访问、使用已释放的内存或并发访问导致的数据损坏等。Swift 作为一种现代编程语言,通过其设计和编译时检查,实现了内存安全的目标。

独占访问的概念

Swift 如何通过独占访问机制来实现内存安全。当变量正在被一个代码段修改时,Swift 不允许其他代码段同时访问该变量的内存地址。Swift 强制执行这一规则,即使在单线程环境中也不例外。通过这种方式,Swift 有效避免了数据竞争和未定义行为。

在 Swift 中,内存的访问被分为读访问和写访问。读访问是指读取变量的值,而写访问是指修改变量的值。Swift 允许多个读访问同时发生,但在写访问期间,任何其他访问(无论是读还是写)都会导致冲突,从而触发编译错误。

访问冲突的具体示例

Swift 中可能导致内存访问冲突的情况。一个常见的例子是,当一个函数在执行时同时访问同一变量,且该变量的某些属性正在被修改,这种情况下就会产生冲突。例如:

1
2
3
4
5
6
7
8
func adjustRatings(_ rating1: inout Int, _ rating2: inout Int) {
let total = rating1 + rating2
rating1 = total / 2
rating2 = total - rating1
}

var movieRating = 85
adjustRatings(&movieRating, &movieRating)

在这个示例中,adjustRatings(&movieRating, &movieRating) 试图同时修改 movieRating 的两个属性,这在 Swift 中会触发编译错误,因为这种情况下无法保证内存的独占访问。

结构体的内存访问

Swift 中结构体的内存管理。与类不同,结构体是值类型,这意味着它们的每一个实例都有自己独立的内存空间。因此,对结构体的访问也需要遵守独占访问规则。特别是在结构体内部有多个属性时,Swift 会确保在修改一个属性时,其他属性不会被同时访问。

内存访问的例外情况

尽管 Swift 对内存访问进行了严格的控制,但也有一些例外情况。在某些情况下,Swift 会允许非独占访问,例如全局变量和静态变量。这些变量的生命周期与程序一致,通常不会发生数据竞争,因此 Swift 对它们的访问控制相对宽松。

Swift 的内存安全与性能

尽管 Swift 的内存安全机制确保了程序的稳定性,但它也带来了一定的性能开销。如何在不牺牲安全性的前提下优化程序性能。例如,通过在合适的场景下使用 inout 参数,开发者可以在保留独占访问的同时,减少不必要的内存复制。

Swift内存操作

Swift 编程语言中进行不安全内存操作的技术和注意事项。

Swift 通常通过引用计数和内存自动管理来保证内存安全,然而在某些高性能或特定底层操作中,开发者可能需要直接操作内存。这时就需要使用到 Swift 的 Unsafe 系列指针类型,例如 UnsafeMutablePointer 和 UnsafePointer。

UnsafePointer 是一个指向某种类型的指针,它允许只读访问内存地址上的数据。这意味着你可以读取该地址的数据但不能修改它。相反,UnsafeMutablePointer 允许你修改指针指向的内存区域内的数据。使用 UnsafeMutablePointer 修改内存时,必须确保内存已经正确地分配且不会被其他代码同时访问。否则,可能会导致程序崩溃或出现难以调试的问题。Swift 提供的一些辅助工具 withUnsafePointer(to::) 和 withUnsafeMutablePointer(to::),它们可以在有限的范围内确保内存操作的安全性。这些函数的使用可以帮助开发者避免一些常见的错误,确保指针的生命周期和作用域受到控制。

内存分配器libMalloc

libMalloc

iOS底层堆内存分配器libMallociOS底层堆内存分配器libMalloc,libMalloc是iOS系统中广泛使用的内存分配器,提供高效的内存管理策略。学习libMalloc源码有助于深入理解iOS内存管理机制,优化内存使用效率。

Zone是libMalloc管理内存的基础单元,负责处理内存分配、释放等操作。常见的内存操作API(如malloc、free、realloc)均通过Zone实现。Zone的核心数据结构,包含多个函数指针,分别对应不同的内存操作函数。如size、malloc、calloc、valloc、free、realloc等,分别处理内存块的大小查询、分配、释放等操作。reserved1和reserved2,用于保留给CFAllocator使用,用户不应直接访问。通过malloc_zone_register_while_locked函数将Zone注册到全局Zone列表中。在内存分配时,根据配置和当前状态选择合适的Zone进行处理。

default_zone是libMalloc默认使用的Zone,通过runtime_default_zone()函数获取。default_zone的实际逻辑可能由scalable_zone或nano_zone等具体Zone实现。

scalable_zone是处理大多数内存分配请求的默认Zone。根据内存大小,采用tiny、small、medium、large四种不同策略进行管理。szone_t,扩展自malloc_zone_t,包含tiny、small、medium策略的相关数据结构和large策略的管理字段。对于大块内存的分配,系统可能选择scalable zone,该zone具有更好的扩展性和灵活性。

nano_zone专门用于管理小内存(如256字节以下)的Zone,以提高小内存分配的效率。提供了V1和V2两个版本的实现,V2版本在性能和管理逻辑上进行了优化。通过nanov2_create_zone函数创建并初始化nano_zone。nano_zone_t是nano zone的具体实现,包含malloc_zone_t基础结构和额外的内存管理信息。该函数负责创建nano zone,并对malloc_zone_t中的函数指针进行重新赋值,以使用nano zone特有的内存分配策略。nano_calloc函数根据请求的内存大小选择直接在nano zone中分配或回退到helper zone进行分配。当nano zone无法满足内存分配请求时,会回退到helper zone进行分配。nano zone中的内存分配会进行16字节对齐,以提高内存访问效率。nano zone通过维护一个高效的缓存机制来减少内存分配和释放的开销。系统提供了调试和日志记录功能,帮助开发者跟踪内存分配和释放的过程,以便发现和解决内存泄漏等问题。

malloc_zone_t是一个包含多个函数指针的结构体,用于存储内存分配、释放等操作的实现地址。包括malloc、calloc、free、realloc等函数指针,这些函数指针指向具体的内存管理函数实现。包含两个reserved字段,供CFAllocator使用,开发者不应直接访问。

calloc函数通过调用malloc_zone_calloc函数来分配内存,并将分配的内存清零。default_zone是一个特殊的zone,用于引导程序进入创建真正zone的流程。根据系统配置和内存需求,选择nano zone或scalable zone进行内存分配。

malloc_zone_register_while_locked该函数用于为zone设置名称,便于调试和日志记录中识别不同的zone。通过zone名称,开发者可以更容易地跟踪和定位内存分配和释放的问题。

系统内容分配方式

CPU 通常有内核态和用户态两种运行模式,内核态拥有更高的权限,可以访问系统资源,而用户态只能访问受限资源。系统内存分配方式通常包括内核态和用户态两种,内核态分配由内核管理,用户态分配由用户程序管理。

程序通过系统调用(System Call)从用户态向内核态传递信息,请求操作系统服务,如打开文件、网络通信等。内核态内存分配通常由内核管理,内核通过内存管理单元(MMU)管理内存映射、分页等操作,确保内存分配的安全性和有效性。

C语言标准库提供了对系统调用的封装,使程序员编写的程序能跨平台运行,无需修改底层代码。C标准库中的malloc、free等函数封装了内核态的内存分配和释放操作,提供了用户态内存管理的接口。堆区用于动态内存分配,栈区用于函数调用的局部变量等。两者之间的空白区域允许栈区增长。

malloc是标准库中的内存分配函数,负责在堆区搜索并分配空闲内存块。当malloc内部管理的空闲内存不足时,会通过系统调用(如brk或mmap)向操作系统请求扩大堆区。系统调用完成后,malloc返回的是虚拟内存地址,物理内存的分配在实际使用时才发生。

进程看到的内存是操作系统提供的虚拟内存,与物理内存不完全对应。当程序首次访问新申请的内存时,缺页中断发生,操作系统分配物理内存并完成映射。

频繁调用malloc会影响系统性能,特别是在高性能程序中。理解内存申请的复杂性有助于选择合适的内存分配器,优化内存使用策略,减少不必要的内存分配。