heap memory with Xcode

堆内存概述

堆内存是直接或间接通过malloc()分配的内存。多数开发者控制的堆内存是“脏的”。开发者可以控制和优化堆内存的使用。

Dirty memory 是指在内存中分配并已经被写入数据的部分。当系统需要腾出内存时,dirty memory 会比 clean memory 更消耗资源。原因是 clean memory 可以直接丢弃,而 dirty memory 需要写入到磁盘(即进行页面交换)或进行其他处理以防止数据丢失。三种脏内存:

  1. 应用程序中的对象分配:当应用程序分配内存并向其中写入数据时,那部分内存就变为 dirty memory。
  2. 自动释放池(Autorelease Pool):在处理 autocad 平台时,临时对象和不能立即销毁的对象也会导致 dirty memory 增长。
  3. 缓存等:一些临时缓存也会占用 dirty memory,尤其是需要频繁读写的数据。

虚拟内存:

  • 绿色部分代表应用程序的可执行文件。这是应用程序的二进制代码部分,是我们常说的应用程序文件。该部分主要包含应用程序自身的指令和代码,程序启动时运行的代码就在这部分存储。
  • 黄色部分代表应用程序的动态分配内存,也被称为堆。这是应用在运行阶段动态分配的内存,用于分配对象和数据结构。通过诸如 malloc 或 new 这样的函数来分配。
  • 橙色部分代表栈内存。栈内存主要用于函数调用时保存局部变量和函数调用信息。当函数调用自身或被其他函数调用时,调用数据(如参数和返回地址)会被推入栈内。调用结束时,这些数据会从栈中弹出。栈的大小是固定的,不会像堆内存那样动态增长。
  • 红色部分代表 Metal 资源。Metal 是苹果为开发者提供的高性能图形和计算 API。Metal 资源部分主要包含应用程序使用 Metal API 时分配的图形资源和计算数据。通常包括纹理、缓冲区和其他图形资源,用于高效的图形渲染和图像处理。
  • 蓝色部分代表链接库。链接库指的是应用程序依赖的外部库或框架,比如系统提供的库(如 UIKit、Foundation 等)和第三方库。这些库被动态加载到应用程序的地址空间中,供应用程序使用。
  • 紫色部分代表资源部分。资源包括应用程序使用的各种资源文件,如图像、音频、视频、配置文件等。这些资源文件在应用程序运行时加载到内存,以供使用。

与操作系统内存布局不同:

虚拟内存中堆内存部分:

内存被划分为不同的区域,以便更有效地管理。每个区域进一步划分为页面,每个页面可以是脏的 (dirty)、干净的(clean) 或已经被交换 (swapped)。黄色代表未分配区域,黑色代表已经分配,橙色代表交换内存。脏的内存页面:包含修改过的数据,必须保存到磁盘以防止丢失。干净的内存页面:未修改或数据已保存到磁盘,可直接释放。交换内存页面:内存被压缩并交换到磁盘中,当需要时会从磁盘复原。

关于malloc函数:

  • 使用 malloc 分配内存时,这段内存的生命周期会超出当前作用域,直到你显式调用 free() 函数释放它。这表示分配的内存不依赖于函数/代码块的作用域结束,而是直到程序明确释放它为止。
  • malloc 分配的内存块至少为 16 字节,以确保内存对齐,提高内存访问效率。内存对齐有助于在不同硬件架构上实现高效的内存访问,避免未对齐访问带来的性能损失。
  • 当释放较小的内存块时,malloc 会将这些内存块内的数据清零。这是一种安全措施,防止敏感数据泄露或遗留在未释放的内存中。
  • 如何最终调用malloc:

  • 打开xcode的malloc调试:

xcode检测内存工具

  • Xcode 的内存报告(Memory Report:展示应用程序当前的内存使用情况,并帮助开发者识别内存使用高峰和趋势。但是,它并不会直接告诉开发者为什么内存正在增长或者具体是哪块代码导致了内存增长。
  • Memory Graph Debugger 可以显示应用程序中所有对象的内存使用情况及对象之间的引用关系。可视化的图形表示可以帮助开发者识别循环引用和其他内存管理问题。
  • 命令行工具:如果不会通过man手册查看
  1. 检测内存泄漏
    • 使用 leaks <pid> 检测某个进程中的内存泄漏情况。
  1. 分析堆内存
    • 使用 heap <pid> 查看某个进程的堆内存使用情况,找出可能分配过多内存的代码部分。
  1. 查看内存映射
    • 使用 vmmap <pid> 查看虚拟内存映射,以了解内存分配的整体情况并帮助定位问题。
  1. 分析 malloc 调用
    • 使用 malloc_history <pid> <address> 查看特定内存地址的分配历史,帮助追踪某段内存的使用情况。
  • Allocations:实时监控应用程序在堆内存中分配和释放内存的情况,展示每个对象和内存块的分配和释放时间。在视频代码中(上传图片到app后内存瞬间增长),Transient Memory Growth(瞬态内存增长)指在运行时过程中内存使用的临时性增加,通常这种增长是由于临时对象的分配和使用引起的。当应用程序执行某些操作导致瞬时内存使用量大幅增加时,通常可以观察到这种现象。这些操作往往是通过数据处理、I/O操作、图形渲染等引发的,导致内存临时分配给一些短期存在的对象或数据结构。内存突增(Memory Spikes)长时间内存使用量急剧飙升,有可能导致应用程序性能下降、崩溃或其他意外行为。

Transient Memory Growth问题

以下是通过allocations查找transient:将某一峰选择后,底部会显示具体(如图)

582个autoreleasepool对象也是不合理的。下面找到了具体代码:

以下是选择所有峰通过call trees进行排查:

双击后显示具体的代码:

func loadThumbnails(with renderer: ThumbnailRenderer) {
  for photoURL in urls {
    renderer.faultThumbnail(from: photoURL)
  }
}

循环遍历时,每次遍历都会创建多个对象,并将它们放入同一个 autoreleasepool。由于 autoreleasepool 的作用范围是整个循环块,所有创建的对象只有在循环结束后才会被释放,这会导致内存使用在循环执行过程中持续增长。

因此导致内存激增的原因是autoreleasepool对象过多。下面是修改后的代码:

func loadThumbnails(with renderer: ThumbnailRenderer) {
    for photoURL in urls {
        autoreleasepool {
            renderer.faultThumbnail(from: photoURL)
        }
    }
}

在此优化后的代码中,每次循环迭代都有一个单独的 autoreleasepool,从而在每次迭代结束时释放掉在该迭代中创建的自动释放对象。

Persistent growth memory问题

持续增长内存(Persistent Growth Memory)是一个关键问题,指的是应用程序在运行过程中内存不断增加,不会随着时间释放或者降低。图表显示了每代中的内存分配随着时间的推移而累积的情况,反映了内存的持久增长。

how does xcode memory graph debugger work?

1. Strong Reference(强引用)

  • 定义:强引用明确持有对象的所有权。只要有一个强引用,引用的对象就不会被释放。
  • 特点:强引用是引用计数增加的主要原因,常用于持有对象。
  • 示例
1@property (nonatomic, strong) NSObject *myObject;
  • 解释:在这个例子中,myObject 是一个强引用属性,意味着只要 myObject 存在,对象无法被释放。

2. Weak/Unowned Reference(弱引用/无主引用)

  • 定义:弱引用和无主引用明确表示不持有对象的所有权。对象被其他强引用持有,但在最后一个强引用被移除时,弱引用和无主引用不会防止对象被释放。
  • 特点:弱引用用于避免循环引用,当对象被释放时会自动置为 nil;无主引用在 Swift 和其他 ARC 环境中使用,允许引用空指针(dangling pointer)而不会被自动置为 nil。
  • 示例弱引用在 Objective-C 中):
1@property (nonatomic, weak) id <SomeDelegate> delegate;
  • 解释:在这个例子中,delegate 是一个弱引用,表示不持有实际对象。当对象被释放时,delegate 自动变成 nil。

3. Unmanaged Reference(非托管引用)

  • 定义:非托管引用可能是一个指针,但必须手动管理其内存。开发者需要在合适的时间手动增加或者减少引用计数。
  • 特点:非托管引用一般用于桥接代码或者基于非常规对象管理方案时。
  • 示例
1NSObject * __unsafe_unretained unmanagedObject = someObject;
  • 解释:在这个例子中,unmanagedObject 是 someObject 的非托管引用,需要在手动管理生命周期,或直接进行转换与桥接(桥接 Core Foundation 对象到 Objective-C 对象时常使用)。

4. Conservative Reference(保守引用)

  • 定义:保守引用可能是一个指针,但工具和编译器必须假定其为真,还需显式处理引用的对象。
  • 特点:保守引用通常在遗留代码或者较低级别的内存管理中出现,工具通常假定其指针行为。
  • 解释:这种引用类型在特定使用场景中更可能适用于需要额外内存管理策略或优化的代码段。

回到视频:

func faultThumbnail(from photoURL: URL) {
  // Cache the thumbnail based on url + creationDate
  let timestamp = UInt64(Date.now.timeIntervalSince1970) // Bad - caching with wrong timestamp
  let cacheKey = CacheKey(url: photoURL, timestamp: timestamp)

  let thumbnail = cacheProvider.thumbnail(for: cacheKey) {
    return makeThumbnail(from: photoURL)
  }
  images.append(thumbnail.image)
}

当前时间戳会导致每次调用函数时生成不同的缓存键,从而无法命中缓存。

修改后的代码:

峰值达到max后没有持续增长,说明问题已解决。

leaked memory 

可达性(Reachability)用于判断某个内存块是否仍然被应用程序"引用"或者能被访问到。以下表格是对上图的描述:

类型

定义

标记

解释

图示

有用内存(Useful memory)

可达分配(Reachable allocations),这些内存块将在未来再次被使用。

绿色

这些内存块通过指针路径与应用程序的根节点相连,它们是应用程序运行所需的。

第一个路径,通过一条绿色线条,表示内存块是有用和可达的。

废弃内存(Abandoned memory)

可达分配(Reachable allocations),但这些内存块将不会再被使用。

橙色

这些内存块虽然仍然可达,但实际上已经不再需要,可能是未及时释放的资源。

第二个路径,通过一条灰色线条,表示内存块是可达的但无用。

泄漏内存(Leaked memory)

不可达分配(Unreachable allocations),这些内存块没有任何持有的引用来释放它们。

红色

这些内存块完全孤立,无法通过任何路径从根节点访问到,且无法被自动释放,导致内存泄漏。

第三个路径,通过红色线条和红色标记,表示内存块是不可达的且无用。

回到视频:

一个例子:

闭包上下文(Closure Context):在 Swift 中,闭包上下文是用来描述闭包如何捕获和持有外部变量的一种机制。

  • Paired 1:1 with active closure:每个活动的闭包都有一个与之对应的闭包上下文。
  • Reference qualifiers, no capture names:在闭包上下文中管理的是引用修饰符(如 strong, weak, unowned),而不是捕获变量的名称。这意味着它仅关心持有的引用关系,而不需要命名捕获的变量。
let swallow = Swallow()
swallow.completion = {
  print("\(swallow) finished carrying a coconut")
}

在 Swift 中,闭包可以捕获并持有外部变量的引用。如果这些捕获变量是类实例的强引用,则可能会导致循环引用,进而引发内存泄漏。

解决方法: 使用捕获列表(capture list)明确定义引用的强弱关系,避免循环引用。

swallow.completion = { [weak swallow] in
    guard let swallow = swallow else { return }
    print("\(swallow) finished carrying a coconut")
}
  • 在捕获列表中使用 [weak swallow] 声明为弱引用。
  • 通过 guard let swallow = swallow else { return } 确保 swallow 对象在闭包执行时依然存在。

回到视频:

// ...
let renderer = ThumbnailRenderer(style: .vibrant)
let loader = ThumbnailLoader(bundle: .main, completionQueue: .main)
loader.completionHandler = {
  self.thumbnails = renderer.images // implicit strong capture of renderer causes strong reference cycle
}
loader.beginLoading(with: renderer)
// ...

Leaks FAQ

这段代码存在一个故意的错误(intentional mistake):没有调用 oops.deallocate() 来释放分配的内存。如果缺少 deallocate 调用,分配的内存将不会被释放,这就形成了内存泄漏。但在保守的泄漏扫描中,如果工具无法确定某个内存分配是否确实没有被引用,它可能会假设这些分配仍然在使用并避免将其报告为泄漏。

  • 红色的 “X” 表示明确的内存泄漏。
  • 灰色的 “?” 表示不确定的内存块,这种不确定的内存块在一次扫描中可能是否定(considered leaked),而在另一次扫描中可能认为是未知(或有引用)。

这说明在不同时间或条件下,相同的内存块是否被认为是泄漏可能会有所变化,导致检测到的内存泄漏数量有可能减少。

由于编译器不执行常规的清理操作,这些局部引用可能会在函数执行过程中保留下来,而不会被释放,导致内存泄漏。

noreturn 和 -> Never

  • noreturn:在 C 和 Objective-C 中,noreturn 是一种函数属性,表示函数不会返回到调用点。也就是说,这样的函数要么会导致程序退出,要么会引发异常。
  • -> Never:在 Swift 中,Never 类型表示一个函数永远不会返回。例如,抛出致命错误或者无限循环的函数

Comparing performance of weak and unowned

弱引用允许你持有对一个对象的引用,但不会阻止该对象被释放。弱引用总是可选的(Optional),这意味着如果引用的对象被释放,弱引用会变成 nil。弱引用在防止循环引用方面非常有用。

无主引用允许你持有对一个对象的引用,但不会阻止该对象被释放。不同于弱引用总是可选的,无主引用可以被声明为非可选类型。无主引用比弱引用效率更高,因为它们不需要进行相同级别的释放检查。但是,如果引用的对象被释放(deinit),任何试图通过无主引用访问它的行为都会导致程序崩溃,因为引用不会检查对象是否仍然有效。

特征/引用类型

weak

unowned

访问被销毁对象

返回 nil

导致崩溃 (Crash)

内存成本

32 字节

无额外成本

运行时成本

约是强引用的 10 倍

约是强引用的 4 倍

cost of measurement


 

  • 10
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

**K

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值