概述 首先设备硬件资源是固定的,所以app的内存资源是有限的。较低的内存占用可以提高用户体验以及性能。如果内存占用过大,可能会被系统杀掉。所以每个开发者都应该注意内存问题。本session主要分为以下几方面: 为什么要减少内存占用 内存占用 分析内存占用的工具 图像 在后台时,对内存的优化 演示demo 为什么要减少内存占用? 答案很简单,为了更好的用户体验。减少内存占用能同时减少其对CPU时间维度上的消耗,从而不仅使您所开发的App,其他App以及整个系统也都能表现的更好。 内存占用 并非所有的内存占用都是相等的。而要减少的内存占用其实指的是虚拟内存(Virtual Memory) 占用。 Pages 内存是由系统管理,一般以页为单位来划分。 在iOS 上,每一页包含16KB的空间。系统会按照页来分配内存,堆上可能会有多个对象在一页上,也可能一个对象占用多页。 所占用页总数乘以每页空间得到的就是这段数据使用的总内存。 内存页按照各自的分配和使用状态,可以分为Clean和Dirty两类。 举个例子,如果我申请了一个20000个整型的数组(80000个字节)。系统可能会分配给我6页内存。 当我申请空间后,他们都是Clean的 如果我在数组的第一个位置写入数据,那么该页就会变Dirty了 如果我在数组最后一个位置写入数据,那么该页就会变Dirty了。 中间的几页都是Clean的,因为他们还未被写入。 内存映射文件 当 App 访问一个文件时,系统内核会负责调度,将磁盘上的文件加载并映射到内存中。如果这是只读的文件,它所占用到的内存页是Clean的。 如下图所示,一个50KB的图片被加载到内存中时,需要分配4页内存来存储。其中第四页中有2KB的空间会被用来存储这个图片的数据,剩余空间可能会被用来存储其它数据。前三页总是可以被系统清除的。 典型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。 Compressed memory是在内存紧张时能够将最近使用过的内存占用压缩至原有大小的一半以下,并且能够在需要时解压复用。它在节省内存的同时提高了系统的响应速度,特点总结起来如下: * Shrinks memory usage 减少了不活跃内存占用 * Improves power efficiency 改善电源效率,通过压缩减少磁盘IO带来的损耗 * Minimizes CPU usage 压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销 * Is multicore aware 支持多核操作 例如,当我们使用Dictionary去缓存数据的时候,假设现在已经使用了3页内存,当不访问的时候可能会被压缩为1页,再次使用到时候又会解压成3页。 Memory Warning 内存警告,不一定总是应用自身导致的 内存压缩技术使得释放内存变得复杂 缓存策略 小结 通常情况下,我们所说的内存占用是指Dirty Memory和Compressed Memory,Clean Memory不需要过多关心。 App 能使用比较多的内存空间,但是上限会根据设备不同而不同。Extension能使用的最大内存则要低很多,所以当你在开发Extension的时候尤其要注意内存使用。当使用的内存超出限制的时候,系统会抛出EXC_RESOURCE_EXCEPTION异常。 分析内存占用的工具 Xcode Memory Gauge 在Xcode中,你可以通过Memory Gauge工具,很方便快速的查看App运行时的内存情况,包括内存最高占用、最低占用,以及在所有进程中的占用比例等。如果想要查看更详细的数据,就需要用到Instruments了。 Instruments 在 Instruments 中,你可以使用Allocations、Leaks、VM Tracker和 Virtual Memory Trace对App进行多维度分析。 Allocations LeaksVM Tracker Virtual Memory TraceDebug Debugger - Memory Resource Exceptions 当你使用 Xcode 10以前的版本进行调试时,在内存过大时,debug session会直接终止,并且在控制台打印出异常。从Xcode 10开始,debugger会自动捕获EXC_RESOURCE RESOURCE_TYPE_MEMORY异常,并断点在触发异常抛出的地方,十分方便定位问题。 Xcode Memory Debugger Xcode Memory Debugger的内存调试器是在Xcode 8中提供的,它可以帮助您跟踪对象依赖性,周期和泄漏。在Xcode 10中,优化了界面布局。 你也可以点击File->Export Memory Graph将其导出为memgraph文件,通过命令行对其进行分析。下面说下几个命令行工具 vmmap vmmap 能够打印出进程信息,所有分配给该进程的 VM区域以及VM区域的种类、内存占用信息等内容。利用--summary则能够根据不同的区域类型打印出详细的内存占用类型和信息。这里需要注意的是 SWAPPED SIZE在iOS上指的是Compressed memory size且其值表示 压缩前的占用大小 。 vmmap--summaryApp.memgraph 1 如果您希望查看更多的信息,那么直接调用即可。您将获得所有区域的内容。 vmmap App.memgraph 1 配合管道命令查看所有动态库的Ditry Pages的总和 vmmap -pages xxx.memgraph | grep '.dylib' | awk '{sum += $6}END{ print"Total Dirty Pages:"sum}' 1 更多使用方式请查看vmmap的文档 man vmmap 1 Leak 顾名思义,就是查看内存泄漏的。 leaks xx.memgraph 1 更多使用方式也可以查看man手册。 heap 查看堆区内存 heap xx.memgraph 1 默认情况下,是按照数量排序的,当然也可以通过参数-sortBySize让其来按照大小排序。 heap xx.memgraph-sortBySize 1 排列之后,我们发现了一些巨大的NSConcreteData对象,通过下面的命令,就可以得到每个对象的内存地址。 heap xx.memgraph -addresses'NSConcreteData'#得到全部对象的内存地址#heap xx.memgraph -addresses all 1 2 3 4 有了这些地址呢,我们就可以知道他们是从哪里来的。有了这些对象的内存地址之后,我们还需要另一样工具帮助我们做下一步分析。 Enabling Malloc Stack Logging 在Product->Scheme->Edit Scheme->Diagnostics中,开启 Malloc Stack 功能,建议使用Live Allocations Only选项。 之后lldb会记录调试过程中对象创建的堆栈,配合malloc_history工具,就可以定位到那些占用了过大内存的对象是哪里创建的。 malloc_history 查看内存分配的历史,使用方法如下 malloc_historyxx.memgraph[address]malloc_historyxx.memgraph--fullStacks[address] 1 2 3 工具的选择 以上讲了很多工具,当遇到内存问题时,那我们要如何进行选择呢? 这里有三种方法来考虑。您是否想查看对象的创建?您是否想查看内存中对象的引用或者地址内容?或者您是否想查看一个实例有多大? 可以根据上图所示,按照不同情况,来使用不同的工具。 图像 图片所占内存的大小与图片的尺寸有关,而不是图片的文件大小。 举个例子,我们这里有一张590KB图片,而它的分辨率是2048px * 1536px。它实际使用的内存不是590KB,而是2048 * 1536 * 4 = 12 MB。 图片为什么会占这么多的内存?这还要从图片在iOS上显示的原理说起。一张图片文件从磁盘到展示需要经过三步: 加载 解压缩 渲染 更多关于图像以及如何优化图像的信息,请查看WWDC 2018 Image and Graphics Best Practices ,也可以直接阅读前几天我们小伙伴发布的文章 图像和图形的最佳实践) 。 图像渲染格式 sRGB格式 Wide格式 亮度和alpha 8格式 alpha 8格式 选择正确的图片格式 简单的回答是:不需要你来选择格式,而是应该让格式选择你。 用UIGraphicsImageRenderer代替UIGraphicsBeginImageContextWithOptions 使用UIGraphicsBeginImageContextWithOptions生成的图片,每个像素需要4个字节表示。建议使用UIGraphicsImageRenderer, 这个方法是从iOS 10引入,在iOS 12上会自动选择最佳的图像格式,可以减少很多内存 。UIGraphicsImageRenderer可以创建UIImage对象或者进行JPEG/PNG格式的编码。 此外,如果想修改颜色,可以直接修改tintColor,不会有额外的内存开销。 下采样 当你缩小一幅图像的时候,会按照取平均值的办法把多个像素点变成一个像素点,这个过程称为下采样(Downsampling)。 UIImage在设置和调整大小的时候,需要将原始图像加压到内存中,然后对内部坐标空间做一系列转换,整个过程会消耗很多资源。我们可以使用ImageIO,它可以直接读取图像大小和元数据信息,不会带来额外的内存开销。 这样处理,不但内存占用的更低了,而且执行速度也快了50%左右。 在后台时,对内存进行优化 假设在 App 里展示了一张很大图片,当我们切换到后台去做其它的操作时,这个图片还在占用内存。我们应该考虑在合适的时机去回收这类占用过大的数据。 监听UIApplicationWillEnterForeground和UIApplicationDidEnterBackground通知 viewWillAppear和viewDidDisappear方法 Demo演示 略过,基本上就是用上面说的命令去调试一个问题及优化方案去调试图片的内存问题 总结 内存是一个有限的共享资源,要学会使用Xcode分析内存工具,从而了解应用程序内存占用情况,并使用一些缩减应用程序内存占用空间的技巧和窍门。