iOS内存的深入探究(WWDC 2018 session 416)

WWDC 2018 session 416: iOS Memory Deep Dive

概述

首先设备硬件资源是固定的,所以app的内存资源是有限的。较低的内存占用可以提高用户体验以及性能。如果内存占用过大,可能会被系统杀掉。所以每个开发者都应该注意内存问题。本session主要分为以下几方面:

  • 为什么要减少内存占用
  • 内存占用
  • 分析内存占用的工具
  • 图像
  • 在后台时,对内存的优化
  • 演示demo

为什么要减少内存占用?

答案很简单,为了更好的用户体验。减少内存占用能同时减少其对CPU时间维度上的消耗,从而不仅使您所开发的App,其他App以及整个系统也都能表现的更好。

内存占用

并非所有的内存占用都是相等的。而要减少的内存占用其实指的是虚拟内存(Virtual Memory) 占用。

Pages

内存是由系统管理,一般以页为单位来划分。
image
在iOS 上,每一页包含16KB的空间。系统会按照页来分配内存,堆上可能会有多个对象在一页上,也可能一个对象占用多页。

所占用页总数乘以每页空间得到的就是这段数据使用的总内存。
image

iOS 以及 macOS 都采用了虚拟内存技术来突破物理内存(RAM) 的大小限制,每个进程都拥有一段由多个大小相同的page 所构成的逻辑地址空间。处理器和内存管理单元MMU(Memory Management Unit) 维护着由逻辑地址空间到物理地址的 page映射表,当程序访问逻辑内存地址时由MMU根据映射表将逻辑地址转换为真实的物理地址。在早期的苹果设备中,每个page的大小为4KB;基于A7和A8处理器的系统为64位程序提供了16KB的虚拟内存分页和4KB的物理内存分页;而在A9之后,虚拟内存和物理内存的分页大小都达到了16KB。

内存页按照各自的分配和使用状态,可以分为CleanDirty两类。

举个例子,如果我申请了一个20000个整型的数组(80000个字节)。系统可能会分配给我6页内存。

  • 当我申请空间后,他们都是Clean
  • 如果我在数组的第一个位置写入数据,那么该页就会变Dirty
  • 如果我在数组最后一个位置写入数据,那么该页就会变Dirty了。
  • 中间的几页都是Clean的,因为他们还未被写入。
    image
内存映射文件

当 App 访问一个文件时,系统内核会负责调度,将磁盘上的文件加载并映射到内存中。如果这是只读的文件,它所占用到的内存页是Clean的。
如下图所示,一个50KB的图片被加载到内存中时,需要分配4页内存来存储。其中第四页中有2KB的空间会被用来存储这个图片的数据,剩余空间可能会被用来存储其它数据。前三页总是可以被系统清除的。
image

典型app内存类型

当内存不足的时候,系统会按照一定策略来腾出更多空间供使用,比较常见的做法是将一部分低优先级的数据挪到磁盘上,这个操作称为Page Out。之后当再次访问到这块数据的时候,系统会负责将它重新搬回内存空间中,这个操作称为Page In

Clean Memory

Clean Memory是指那些可以用以Page Out的内存,只读的内存映射文件,或者是App所用到的frameworks。每个frameworks都有_DATA_CONST段,通常他们都是Clean的,但如果用runtime进行swizzling,那么他们就会变Dirty

Dirty Memory

Dirty Memory是指那些被App写入过数据的内存,包括所有堆区的对象、图像解码缓冲区,同时,类似Clean memory,也包括App所用到的frameworks。每个framework都会有_DATA段和_DATA_DIRTY段,它们的内存是Dirty的。

值得注意的是,在使用framework的过程中会产生Dirty Memory,使用单例或者全局初始化方法是减少Dirty Memory不错的方法,因为单例一旦创建就不会销毁,全局初始化方法会在类加载时执行。

Compressed Memory

由于闪存容量和读写寿命的限制,iOS 上没有Disk swap机制,取而代之使用Compressed memory

Disk swap 是指在 macOS 以及一些其他桌面操作系统中,当内存可用资源紧张时,系统将内存中的内容写入磁盘中的backing store (Swapping out),并且在需要访问时从磁盘中再读入 RAM (Swapping in)。与大多数 UNIX 系统不同的是,macOS 没有预先分配磁盘中的一部分作为 backing store,而是利用引导分区所有可用的磁盘空间。

Compressed memory是在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度,特点总结起来如下:
* Shrinks memory usage 减少了不活跃内存占用
* Improves power efficiency 改善电源效率,通过压缩减少磁盘IO带来的损耗
* Minimizes CPU usage 压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销
* Is multicore aware 支持多核操作

例如,当我们使用Dictionary去缓存数据的时候,假设现在已经使用了3页内存,当不访问的时候可能会被压缩为1页,再次使用到时候又会解压成3页。

本质上,Compressed memory 也是 Dirty memory。
因此, memory footprint = dirty size + compressed size ,这也就是我们需要并且能够尝试去减少的内存占用。

Memory Warning
  • 内存警告,不一定总是应用自身导致的

    如果您使用的设备内存较小,那么在接电话时,可能会触发内存警告。

  • 内存压缩技术使得释放内存变得复杂

    假设一个App的Dirty Memory中有一个NSDictionary对象占了三页空间,当我们不访问它的时候,就会被压缩成一页。这样我们就多了2页的可用空间。
    但此时如果收到内存警告,我们决定要将整个字典中的内容移除,这时我们就需要访问压缩后的字典,它就会被解压-释放对象-然后内存占用又回到了一页。这也就是说,我们努力释放了一些对象,但却没有增加可用空间,甚至可能加剧内存紧张的态势,也增加了CPU的开销。

  • 缓存策略

    我们对数据进行缓存的目的是想减少 CPU 的压力,但是过多的缓存又会占用过大的内存。在一些需要缓存数据的场景下,可以考虑使用NSCache代替 NSDictionaryNSCache分配的内存实际上是Purgeable Memory,可以由系统自动释放。这点在Effective Objective 2.0一书中也有推荐NSCacheNSPureableData的结合使用既能让系统根据情况回收内存,也可以在内存清理的同时移除相关对象。

小结

通常情况下,我们所说的内存占用是指Dirty MemoryCompressed MemoryClean Memory不需要过多关心。

App 能使用比较多的内存空间,但是上限会根据设备不同而不同。Extension能使用的最大内存则要低很多,所以当你在开发Extension的时候尤其要注意内存使用。当使用的内存超出限制的时候,系统会抛出EXC_RESOURCE_EXCEPTION异常。

分析内存占用的工具

Xcode Memory Gauge

image

在Xcode中,你可以通过Memory Gauge工具,很方便快速的查看App运行时的内存情况,包括内存最高占用、最低占用,以及在所有进程中的占用比例等。如果想要查看更详细的数据,就需要用到Instruments了。

Instruments

image
在 Instruments 中,你可以使用AllocationsLeaksVM TrackerVirtual Memory Trace对App进行多维度分析。

  • Allocations

    追踪程序的虚拟内存占用和堆信息,提供对象的类名、大小以及调用栈等信息

  • Leaks

    用于检测程序运行过程中的内存泄露,并记录对象的历史信息
    第三方内存泄漏检测工具MLeaksFinder

  • VM Tracker

    能够区分程序运行时前文所述的Dirty MemoryCompressed Memory占用情况(swapped代表Compressed)。
    image

  • Virtual Memory Trace

    隐藏在System Trace中的Virtual Memory Trace工具能够从page层面更深层次剖析应用程序的虚拟内存操作。WWDC 2016中Syetem Trace in Depth中给出了详细的介绍。
    image

Debug Debugger - Memory Resource Exceptions

当你使用 Xcode 10以前的版本进行调试时,在内存过大时,debug session会直接终止,并且在控制台打印出异常。从Xcode 10开始,debugger会自动捕获EXC_RESOURCE RESOURCE_TYPE_MEMORY异常,并断点在触发异常抛出的地方,十分方便定位问题。
image

Xcode Memory Debugger

Xcode Memory Debugger的内存调试器是在Xcode 8中提供的,它可以帮助您跟踪对象依赖性,周期和泄漏。在Xcode 10中,优化了界面布局。
image

你也可以点击File->Export Memory Graph将其导出为memgraph文件,通过命令行对其进行分析。下面说下几个命令行工具

vmmap

vmmap 能够打印出进程信息,所有分配给该进程的 VM区域以及VM区域的种类、内存占用信息等内容。

系统中将一系列连续的内存页关联到一个VMObject进行管理,VMRegionVMObject所管理IDE区域。 Finding iOS Memory中对每种VMRegion作出了详细的解释。

利用--summary则能够根据不同的区域类型打印出详细的内存占用类型和信息。这里需要注意的是 SWAPPED SIZE在iOS上指的是Compressed memory size且其值表示压缩前的占用大小

vmmap --summary App.memgraph  

image
如果您希望查看更多的信息,那么直接调用即可。您将获得所有区域的内容。

vmmap App.memgraph  

image

配合管道命令查看所有动态库的Ditry Pages的总和

vmmap -pages xxx.memgraph | grep '.dylib' | awk '{sum += $6} END { print "Total Dirty Pages:"sum}'

更多使用方式请查看vmmap的文档

man vmmap
Leak

顾名思义,就是查看内存泄漏的。

leaks xx.memgraph

image

更多使用方式也可以查看man手册。

heap

查看堆区内存

heap xx.memgraph

image

默认情况下,是按照数量排序的,当然也可以通过参数-sortBySize让其来按照大小排序。

heap xx.memgraph -sortBySize

image
排列之后,我们发现了一些巨大的NSConcreteData对象,通过下面的命令,就可以得到每个对象的内存地址。

heap xx.memgraph -addresses 'NSConcreteData'

#得到全部对象的内存地址
#heap xx.memgraph -addresses all 

image

有了这些地址呢,我们就可以知道他们是从哪里来的。有了这些对象的内存地址之后,我们还需要另一样工具帮助我们做下一步分析。

Enabling Malloc Stack Logging

Product->Scheme->Edit Scheme->Diagnostics中,开启 Malloc Stack 功能,建议使用Live Allocations Only选项。
image
之后lldb会记录调试过程中对象创建的堆栈,配合malloc_history工具,就可以定位到那些占用了过大内存的对象是哪里创建的。

malloc_history

查看内存分配的历史,使用方法如下

malloc_history xx.memgraph [address]

malloc_history xx.memgraph --fullStacks [address]

image

工具的选择

以上讲了很多工具,当遇到内存问题时,那我们要如何进行选择呢?

这里有三种方法来考虑。您是否想查看对象的创建?您是否想查看内存中对象的引用或者地址内容?或者您是否想查看一个实例有多大?

image

可以根据上图所示,按照不同情况,来使用不同的工具。

图像

图片所占内存的大小与图片的尺寸有关,而不是图片的文件大小。
举个例子,我们这里有一张590KB图片,而它的分辨率是2048px * 1536px。它实际使用的内存不是590KB,而是2048 * 1536 * 4 = 12 MB

图片为什么会占这么多的内存?这还要从图片在iOS上显示的原理说起。一张图片文件从磁盘到展示需要经过三步:

  • 加载

    将被压缩的图片文件加载到内存中(590KB的图片)

  • 解压缩

    将图片文件转换成GPU可以读取的格式。(解压缩后耗费10M)

  • 渲染

    解压缩后,就可以渲染到屏幕上了。

更多关于图像以及如何优化图像的信息,请查看WWDC 2018 Image and Graphics Best Practices,也可以直接阅读前几天我们小伙伴发布的文章图像和图形的最佳实践)

图像渲染格式
  • sRGB格式

    这个是目前比较通用的全色彩图像色域,每个像素占4个字节,分别表示红、绿、蓝通道以及alpha通道。

  • Wide格式

    iOS硬件设备支持的更生动的色域的渲染格式。每像素占用8字节,每个通道占用2字节。iPhone 7及以上的设备可以拍摄这类照片,他们可以栩栩如生地还原美好。但是因为其较大的内存开销需要谨慎使用。

  • 亮度和alpha 8格式

    每像素占用2字节,分别表示灰度和透明度。这通常在着色器中使用,例如Metal Apps等等。一般不是很常见。

  • alpha 8格式

    每像素只占用1字节,用于单色图片,如阴影、无Emoji的文字等。比sRGB小75%。

选择正确的图片格式

简单的回答是:不需要你来选择格式,而是应该让格式选择你。

用UIGraphicsImageRenderer代替UIGraphicsBeginImageContextWithOptions

使用UIGraphicsBeginImageContextWithOptions生成的图片,每个像素需要4个字节表示。建议使用UIGraphicsImageRenderer这个方法是从iOS 10引入,在iOS 12上会自动选择最佳的图像格式,可以减少很多内存。UIGraphicsImageRenderer可以创建UIImage对象或者进行JPEG/PNG格式的编码。
image
image

此外,如果想修改颜色,可以直接修改tintColor,不会有额外的内存开销。

下采样

当你缩小一幅图像的时候,会按照取平均值的办法把多个像素点变成一个像素点,这个过程称为下采样(Downsampling)。

UIImage在设置和调整大小的时候,需要将原始图像加压到内存中,然后对内部坐标空间做一系列转换,整个过程会消耗很多资源。我们可以使用ImageIO,它可以直接读取图像大小和元数据信息,不会带来额外的内存开销。
image
image
这样处理,不但内存占用的更低了,而且执行速度也快了50%左右。

在后台时,对内存进行优化

假设在 App 里展示了一张很大图片,当我们切换到后台去做其它的操作时,这个图片还在占用内存。我们应该考虑在合适的时机去回收这类占用过大的数据。

  • 监听UIApplicationWillEnterForegroundUIApplicationDidEnterBackground通知

    适用于正在显示的view对象,因为退到后台或者进入前台时,不会调用ViewController生命周期的回调函数。

  • viewWillAppearviewDidDisappear方法

    适用于UITabBarControllerUINavigationController。因为虽然拥有多个控制器,但只能有一个在屏幕上展示。
    image
    image

Demo演示

略过,基本上就是用上面说的命令去调试一个问题及优化方案去调试图片的内存问题

总结

内存是一个有限的共享资源,要学会使用Xcode分析内存工具,从而了解应用程序内存占用情况,并使用一些缩减应用程序内存占用空间的技巧和窍门。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: iOS内存管理版本记录如下: 1. iOS 2.0及更早版本:使用手动管理内存的方式。 2. iOS 3.0:引入了基于引用计数的自动内存管理,使用retain和release函数来增加或减少对象的引用计数。 3. iOS 5.0:引入了ARC(自动引用计数)机制,ARC会在编译时自动插入retain和release代码,减少手动管理内存的工作。 4. iOS 7.0:引入了内存诊断工具Memory Usage Report,可以监测App内存使用情况,帮助开发者优化内存管理。 5. iOS 8.0:引入了一些新的API,如NSCache和NSURLSession,使得内存管理更加方便和灵活。 6. iOS 11.0:引入了基于图片大小的UIImage渲染机制,减少了内存占用。 7. iOS 13.0:引入了叫做“Scene”的多任务环境,使得内存管理更加复杂,需要更加小心谨慎地处理内存问题。 总的来说,随着iOS版本的不断更新,内存管理的机制也在不断地完善和优化,使得iOS应用能够更加高效地使用内存,提高用户体验。 ### 回答2: iOS内存管理是由操作系统自动管理的,在不同的版本中有所不同。 在iOS 5之前的版本中,内存管理主要依赖于手动管理引用计数(reference counting)来管理对象的生命周期。开发者需要手动调用retain和release方法来增加或减少对象的引用计数,以确保对象在不再需要时能够被正确释放。这种方式需要开发者非常谨慎地管理对象的引用,以避免内存泄漏或野指针等问题。 从iOS 5开始,iOS引入了自动引用计数(Automatic Reference Counting,ARC)的内存管理机制。ARC可以自动地插入retain、release和autorelease等方法的调用,使得开发者不再需要手动进行内存管理。开发者只需要关注对象的创建和使用,而不需要关心具体的内存管理细节。ARC减少了内存管理的工作量,提高了开发效率,并且减少了内存泄漏和野指针等问题的发生。不过,ARC并不是完全的自动化内存管理,开发者仍然需要遵循一些规则,比如避免循环引用等,以保证内存的正确释放。 随着iOS版本的不断更新,苹果不断改进和优化内存管理机制。每个新版本都带来了更好的性能和更高效的内存管理。开发者可以通过关注苹果的官方文档和开发者社区中的更新内容来了解每个版本中的具体变化和改进。 总结来说,iOS内存管理从手动的引用计数到自动引用计数的演变,极大地简化了开发者的工作,并提高了应用的性能和稳定性。随着不断的改进和优化iOS内存管理会越来越高效和可靠。 ### 回答3: iOS内存管理版本记录是指苹果公司在不同版本的iOS操作系统中对于内存管理方面的改进和更新记录。随着iOS版本的不断迭代,苹果在内存管理方面进行了一系列的优化和改进,以提高系统的稳定性和性能。 首先,在早期的iOS版本中,苹果采用了手动内存管理的方式,即开发人员需要手动创建和释放内存,容易出现内存泄漏和内存溢出等问题。为了解决这些问题,苹果在iOS5版本中引入了自动引用计数(ARC)机制。ARC机制能够通过编译器自动生成内存管理代码,避免了手动管理内存带来的问题。 其次,iOS6版本引入了内存分页机制。这个机制能够将应用程序内存分成不同的页,将不常用的页置于闲置列表中,从而释放出更多的内存空间。这些闲置列表中的页能够在需要时快速恢复到内存中,减少了内存压力。 此外,iOS7版本中进一步提升了内存管理的能力。苹果在这个版本中引入了内存压缩技术,将内存中的数据进行压缩,从而提高了内存利用率。此外,iOS7还引入了资源清理功能,可以自动清理不再使用的资源,释放内存空间。 最后,在iOS13版本中,苹果进一步改进了内存管理策略。该版本中引入了后台内存优化功能,能够自动优化应用在后台运行时的内存占用,减少了后台应用对于系统内存的占用和影响。 综上所述,iOS内存管理版本记录反映了苹果在不同版本的iOS操作系统中对于内存管理方面的改进和优化。这些改进和优化使得iOS系统更加稳定和高效,并且提升了应用程序的性能和用户体验。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值