前言
这个topic主要介绍了如何分析iOS app的内存占用和如何做内存优化,包括以下几部分,
- 什么是内存占用
- 怎么分析内存占用
- 内存杀手---图像
什么是内存占用(Memory Footprint)
Not all memory is created equal.
Memory page --- 内存管理中最小的单位。它是系统分配的,有可能一个page持有多个对象,也可能有些大的对象可以跨越多个pages。
通常它是16KB大小,有三种类型的page。
Memory Page Type
Clean Memory
一开始内存分配时的page都是干净的(堆里对象的分配除外),我们的app开始写入后才变dirty。从硬盘读进内存的文件,是只读的所以也是clean pages。
clean memoryDirty Memory
只要是app向内存写入了东西,就可以认为这个被写入的memory page变脏了。包括堆上的分配、解码的图片,动态库如果调用运行时的method swizzling也会让内存变脏,因为你的app提供了自己的实现。
另外动态库的单例和类方法有助于帮助减少dirty memory,因为一直在内存中,系统不认为他们是dirty memory。
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
分析Memory Footprint
首先是debug navigator里的Xcode Memory Gauge,可以快速看到内存变化情况。
当发现了有内存持续增长时,我们接下来可以使用instrument来分析,通常使用以下4种工具
Allocations和Leaks就不介绍了,大家应该很熟悉。
VM Tracker主要就是用来分析上面介绍过的dirty 和 compressed memory。swapped size在iOS里对应compressed memory size
Virtual memory trace则提供了更详细的page输出日志,包括page cache hits and page zero fills
现在当超过内存占用极限时,Xcode10会停在EXC_RESOURCE_TYPE_MEMORY断点,一个非常实用的功能,有助于接下来缩小分析内存溢出的范围,如下图,
但实际上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
循环引用被标记出来了
heap指令
通常用来查看堆里的大对象的内存占用
heap App.memgraph -sortBySize | grep 'AppName'
进一步,-addresses all 可以看到对象的内存地址
比如 heap App.memgraph -addresses SDWebImageCombinedOperation
如果想进一步看到该对象的调用栈,需要在scheme把Malloc Stack打开
重新生成memgraph后,执行
malloc_history App.memgraph --fullStacks [address]
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里的图像格式有许多种,从每像素1字节的格式到每像素8字节的格式都有,通常是默认的每像素4字节的SRGB
多种图像格式,适用于各种场景使用UIGraphicsBeginImageContextWithOptions,会固定创建SRGB图像,每像素占用4字节。
如果你最低支持iOS10,可以考虑使用UIGraphicsImageRenderer(iOS10以上),因为有些场景可能不需要使用SRGB,并且iOS12这个方法会自动选择最合适的图像格式
结合新的api和tintColor,对于纯色图像,因为每像素只用了1字节,相比旧api可以减少75%的内存占用
下采样
别使用UIImage的drawInRect相关方法,而应该使用imageIO来压缩图片
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的某个方法
网友评论