iOS Memory Deep Dive

iOS Memory Deep Dive_第1张图片

前言


这个topic主要介绍了如何分析iOS app的内存占用和如何做内存优化,包括以下几部分,

  • 什么是内存占用
  • 怎么分析内存占用
  • 内存杀手---图像

什么是内存占用(Memory Footprint)


Not all memory is created equal.
Memory page --- 内存管理中最小的单位。它是系统分配的,有可能一个page持有多个对象,也可能有些大的对象可以跨越多个pages。

iOS Memory Deep Dive_第2张图片
Memory Pages

通常它是16KB大小,有三种类型的page。


iOS Memory Deep Dive_第3张图片
Memory Page Type
Clean Memory

一开始内存分配时的page都是干净的(堆里对象的分配除外),我们的app开始写入后才变dirty。从硬盘读进内存的文件,是只读的所以也是clean pages。

iOS Memory Deep Dive_第4张图片
clean memory
Dirty Memory

只要是app向内存写入了东西,就可以认为这个被写入的memory page变脏了。包括堆上的分配、解码的图片,动态库如果调用运行时的method swizzling也会让内存变脏,因为你的app提供了自己的实现。
另外动态库的单例和类方法有助于帮助减少dirty memory,因为一直在内存中,系统不认为他们是dirty memory。

iOS Memory Deep Dive_第5张图片
dirty memory
iOS Memory Deep Dive_第6张图片
这里给内存分配了一个可以装20000个int元素的数组,蓝色是clean pages,红色是dirty pages。比较疑惑为什么不是5个pages?
Compressed Memory

iOS并没有传统的swap操作,而是在iOS7引入了memory compressor(内存压缩器),对于一段时间没有使用的内存对象,内存压缩器会把对象压缩,释放出更多的pages。需要访问被压缩的对象时,内存压缩器再对它解压。

所以app的运行内存 = pages number * page size;

Memory Warnings

  • 并不总是由app导致(低端设备上有电话进来也可能导致警告)
  • 内存压缩器导致内存释放变得更复杂,可能会占用比原先不使用内存压缩器更多的内存。。。
  • 使用缓存要谨慎,建议用NSCache,而不是NSDictionary

内存占用(Memory Footprint) = dirty memory + compressed memory。
注意跟app的运行内存是两个概念,我们一般做内存分析时,就只需要分析内存占用。
设备不同内存占用上限也不同,app通常上限较高,extension上限较低,超过上限会crash到EXC_RESOURCE_EXCEPTION

iOS Memory Deep Dive_第7张图片
Memory Footprint Limits

分析Memory Footprint


首先是debug navigator里的Xcode Memory Gauge,可以快速看到内存变化情况。
当发现了有内存持续增长时,我们接下来可以使用instrument来分析,通常使用以下4种工具

iOS Memory Deep Dive_第8张图片

Allocations和Leaks就不介绍了,大家应该很熟悉。
VM Tracker主要就是用来分析上面介绍过的dirty 和 compressed memory。swapped size在iOS里对应compressed memory size

iOS Memory Deep Dive_第9张图片

Virtual memory trace则提供了更详细的page输出日志,包括page cache hits and page zero fills

现在当超过内存占用极限时,Xcode10会停在EXC_RESOURCE_TYPE_MEMORY断点,一个非常实用的功能,有助于接下来缩小分析内存溢出的范围,如下图,

iOS Memory Deep Dive_第10张图片

但实际上Instrument的分析工具跟后面要介绍的比起来并不是那么强大,接下来是重磅功能---memory graph,使用一系列强大的命令对这个文件操作,可以很容易发现内存问题。

点击Debug Memory Graph -> File -> Export Memory Graph

vmmap指令

vmmap --summary App.memgraph
vmmap --verbose App.memgraph | grep 'WebKit Malloc'

注意Swapped Size显示的是压缩前的内存大小。
应该重点关注Dirty Size 和 SwappedSize,他们加起来就是我们app的内存占用。
一般通过--summary来初步定位dirty size大的Region。

vmmap 指令和一下要介绍的指令都可以和linux命令,例如grep、awk结合使用
grep命令:
http://www.runoob.com/linux/linux-comm-grep.html
awk命令:
http://www.runoob.com/linux/linux-comm-awk.html
以下是显示有多少由动态库导致的dirty pages

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

leaks指令(感觉用处不大)

leaks App.memgraph
iOS Memory Deep Dive_第11张图片
循环引用被标记出来了

heap指令
通常用来查看堆里的大对象的内存占用

heap App.memgraph -sortBySize | grep 'AppName'

进一步,-addresses all 可以看到对象的内存地址
比如 heap App.memgraph -addresses SDWebImageCombinedOperation

iOS Memory Deep Dive_第12张图片
targeted heap objects

如果想进一步看到该对象的调用栈,需要在scheme把Malloc Stack打开

iOS Memory Deep Dive_第13张图片

重新生成memgraph后,执行

malloc_history App.memgraph --fullStacks [address]  
iOS Memory Deep Dive_第14张图片
backtrace

应该根据你的需求,选择相应的内存分析命令。
如果你想知道对象创建的过程,使用malloc_history;
想知道对象间的引用关系,使用leaks;
想知道对象的大小或数量,使用vmmap & heap;

Images (图像是iOS里的内存杀手)


Memory use is related to the dimensions of the image, not the file size.
590KB的图片解码后占用了10MB内存

iOS Memory Deep Dive_第15张图片

iOS里的图像格式有许多种,从每像素1字节的格式到每像素8字节的格式都有,通常是默认的每像素4字节的SRGB

iOS Memory Deep Dive_第16张图片
多种图像格式,适用于各种场景

使用UIGraphicsBeginImageContextWithOptions,会固定创建SRGB图像,每像素占用4字节。
如果你最低支持iOS10,可以考虑使用UIGraphicsImageRenderer(iOS10以上),因为有些场景可能不需要使用SRGB,并且iOS12这个方法会自动选择最合适的图像格式

iOS Memory Deep Dive_第17张图片

结合新的api和tintColor,对于纯色图像,因为每像素只用了1字节,相比旧api可以减少75%的内存占用

下采样

别使用UIImage的drawInRect相关方法,而应该使用imageIO来压缩图片

iOS Memory Deep Dive_第18张图片

iOS Memory Deep Dive_第19张图片
ImageIO使用示例

后台优化
unload large resources you cannot see
就是退到后台或view消失时从内存中移除图片,进入前台或view出现时再加载图片

总结


  • 内存是有限并且共享的一种资源,当某个app占用内存较大,别的app能获得的内存就越少,系统可能会把别的app干掉,腾出空间给当前运行的app,这样别的app再打开时就是冷启动了。所以为了让大家的app都能在内存里停留更长时间,我们应该时刻关注自己app的内存占用,维护一个良好的内存使用环境
  • 使用memory graph和多种命令行工具来分析内存占用
  • 选用合适的图像格式,iOS10以上使用UIGraphicsImageRenderer
  • 使用ImageIO压缩图像
  • 退到后台(不显示时)unload大的图片资源

最后总结一下常用的内存分析的步骤

  • scheme里勾选malloc history
  • 死命折腾app并输出多个memgraph
  • vmmap --summary app. memgraph
  • 找大的dirtySize/swappedSize对应的region type
  • vmmap --verbose App.memgraph | grep 'some region',选取几个起始内存地址
  • malloc_history App.memgraph --fullStacks [address],观察backtrace,一般就能定位到我们app的某个方法

视频地址:
https://developer.apple.com/videos/play/wwdc2018/416/

你可能感兴趣的:(iOS Memory Deep Dive)