0x1 前语

本文将介绍怎么运用Xcode检测和确诊内存问题。首先需求了解内存构成,内存占用对app的影响、以及一些常见的内存问题。终究将介绍leaks、vmmap、malloc_history等东西来剖析定位内存问题。

体系的内存是有限,更低的、合理的运用内存能使app取得更好的体会:

  1. 更快的运用程序激活(进步热启动概率,防止进入后台后,因占有较大内存被体系回收进程)
  2. 更快速的响应(削减卡顿)
  3. 处理更复杂的功能(加载视频、动画)
  4. 更够在更多的设备上运转(低内存设备)

0x2 内存构成

内存是由体系办理,一般以页为单位来区分。在iOS上,每一页包括16KB的空间。一段数据或许会占用多页内存,所占用页总数乘以每页空间得到的便是这段数据运用的总内存。 app的内存占用可分为以下三类:

  1. 脏内存
  2. 紧缩内存
  3. 干净的内存

1.脏内存

脏内存是现已被app写入的内存,如下:

  • 所有堆上的内存
  • 图片解码的缓冲区
  • Frameworks中的__DATA和__DATA_DIRTY部分

2.紧缩内存

当内存不足的时分,体系会依照一定策略来腾出更多空间供运用,比较常见的做法是将一部分低优先级的数据挪到磁盘上,之后当再次拜访到这块数据的时分,体系会担任将它从头搬回内存空间中。然后关于移动设备而言,频频对磁盘进行IO操作会下降存储设备的寿数。所以从iOS7开端,体系开端采用紧缩内存的办法来开释内存空间。

在iOS中当内存紧张时能够将最近未运用过的脏内存占用紧缩至原有巨细的一半以下,而且能够在需求时解压复用。在节约内存的一同进步体系的响应速度,有以下特色:

  • 削减了不活跃的内存占用
  • 改善电源效率,经过紧缩削减磁盘IO带来的损耗
  • 紧缩/解压十分迅速,能够尽或许削减CPU的时刻开支
  • 支撑多核操作

iOS在内存紧张时运用的是内存紧缩技术,而MacOS在内存紧张时运用内存紧缩和磁盘交换技术

3.干净的内存

还没有被写入的内存或能够被体系铲除且在需求时能从头加载的内存(内存是按页分配的,只有整页的数据被铲除才能够被体系从头分配,只被铲除部分数据,导致体系无法从头分配该页)

  • 内存映射文件
  • 能够被整页开释的内存
  • Frameworks中的__DATA_CONST部分
  • 运用的二进制可履行文件

4.小结:

  1. 运用内存占用巨细 = 脏内存巨细 + 紧缩内存巨细
  2. 削减运用的内存占用 = 削减脏内存巨细 = 削减堆上内存占用 + 图片解码缓冲区巨细

0x2 内存走漏

运用程序请求内存后,无法开释已请求的内存空间,一次内存走漏的危害能够疏忽,但内存走漏堆积的后果很严重,无论多少内存,早晚会被占光。 依据内存走漏原因,能够分为以下两类:

  • 无主内存(没有指针指向的内存,现已无法被开释)
  • 循环引证

0x3 堆巨细问题

堆是进程地址空间的一部分,用来存储动态生成的方针。堆上简单呈现以下问题:

  1. 堆分配回归
  2. 碎片化

堆分配回归的办理策略:

  • 移除无用内存分配
  • 削减过大内存的分配
  • 不再运用的内存需求开释
  • 需求时才去分配内存

什么是碎片化? page(内存页)是体系授予进程的固定巨细、不可分割的最小内存块。由于page是不可分割的,当进程写入page的任意部分,整个page都会被认为是dirty(脏内存)而且进程将会办理它,即使page的大部分没有被运用到。

当进程的dirty page没有被100%占用时,就会产生碎片化。 举个例子: 假如有有一个脏内存页,该页被运用了一半的内存(8KB),此刻创立了一个需求16KB巨细的方针,则该页无法放下,所以需求运用一个新的内存页进行放入该方针。假如有n个类似的脏内存页(未被100%运用),即使它们未被运用的总内存大于新方针所需求的内存,体系也无法进行分配至这些页中,导致内存利用率低。这种现象便是内存碎片

下降内存碎片化的办法便是创立内存相邻,生命周期类似的方针。这能确保这些方针会被一同开释,这样进程就会得到一大块连续的空闲内存进行方针分配。

0x4 定位内存问题

下面以MemoryGraphDemo为例,分别介绍Xcode 内存图与指令行东西的运用办法,来讲述怎么定位循环引证、无主内存和内存追溯。

循环引证场景:

两个方针相互强引证的场景

1.翻开项目(demo已设置)按过程设置 Edit Scheme -> Run -> Diagnostics -> Malloc Stack Logging -> Live Allocations Only

iOS 内存优化之工具介绍
翻开该配置后,内存图会记载malloc的分配仓库日志,发现内存问题后,能够经过记载的仓库回溯找到存在问题的代码。可是会给app增加额外内存占用,所以仅在调试时运用该配置。

2.运转demo,点击循环引证场景,制作一个走漏点,然后翻开内存图,点击过程如图所示

iOS 内存优化之工具介绍

3.然后过滤出走漏方针.内存图左边栏能够查看总的走漏方针个数、类型,中间的图标明该走漏是一个循环引证导致的,右边Object栏能够查看方针的具体信息,包括类型、巨细、地址信息。Bactrace则是产生走漏点的仓库,该仓库只有翻开了Malloc Stack Logging后才会有。经过点击仓库后边的小箭头,能够直接跳转到代码位置。

iOS 内存优化之工具介绍

4.leaks能够运用进程名来运转,以demo为例:

leaks MemoryGraphDemo

控制台输出对应信息,下图为部分要害信息:

iOS 内存优化之工具介绍

  • 头部:展现内存走漏的概览,产生了2个走漏方针,浪费了共96KB
  • STACK:展现了产生走漏的相关仓库
  • ROOT CYCLE:代表是循环引证导致走漏

leaks也能够经过含糊匹配进程名的办法运用,如leaks Memory也是有效的, 想了解更多的运用办法,能够运用man leaks指令查看leaks的运用手册。

无主内存场景:

内存无法被开释或未调用相关的开释函数的场景

1.从头运转项目(防止循环引证场景干扰),点击No Active References场景

2.相同的过程翻开Xcode内存图

iOS 内存优化之工具介绍
从图中能够看到,没有任何方针引证这个数组,因此它也就不或许被调用开释函数,开释这块内存。

3.运用相同的指令leaks MemoryGraphDemo,输出成果如下:

iOS 内存优化之工具介绍
ROOT LEAK:代表该走漏问题是由于没有任何指针指向该方针导致的走漏

直接持有场景

假设有一个方针A,A持有一个可变调集B,调集B里存放都是C方针,C方针强持有A。

A -> Set, Set add C, C -> A

1.再次重启demo,点击Indirect Retain Cycles,翻开Xcode内存图

iOS 内存优化之工具介绍

从图中能够看到四个方针之间产生了相互引证的联系,导致无法开释内存。

2.运用leaks东西查看

iOS 内存优化之工具介绍

从输出成果红框里能够分分出,循环引证导致的走漏(ROOT CYCLE), 一个SomeItem方针强持有(__strong)_helper,_helper方针强持有(__strong)_items, _items内持有了一个SomeItem方针。

隐式直接持有场景

该场景基本和直接持有场景基本一致,差异在于调集的持有办法。本场景运用分类的办法为helper增加一个调集方针(分类增加特点的办法objc_setAssociatedObject)。

A -> Dynamic Set, Dynamic Set add C, C -> A

这种场景内存图和leaks东西都不能直接过滤出来,需求结合代码上下文和内存图进行剖析。

1.再次重启demo,点击Dynamic Indirect Retain Cycles, 然后翻开Xcode内存图

iOS 内存优化之工具介绍

2.相同,运用leaks指令(leaks MemoryGraphDemo)的成果如下

iOS 内存优化之工具介绍

从输出成果来看,本场景没有产生循环引证和无主内存,可是在过滤框中查找someItem,会发现该方针和helper方针仍然存在内存中。

iOS 内存优化之工具介绍

3.过滤出app创立的方针,能够看到方针仍然在内存中,而且能够看出helper经过objc_setAssociatedObject办法增加的数组方针,并不会被helper直接持有。而是被objc_setAssociatedObject函数创立的一块内存持有着该数组。从Xcode右边栏Object区获取helper的Address,运用leaks MemoryGraphDemo --traceTree=Address指令能够更明晰的看出其引证联系

iOS 内存优化之工具介绍
从图中能够看出helper被someItem持有,且someItem被NSMutableArray方针持有,NSMutableArray方针由objc_setAssociatedObject创立的方针持有,终究存储在objc::AssociationManager::_mapStorage中。能够经过objc的源码剖析为什么这种引证办法形成方针不会被开释。

参照objc4-866.9关于objc_setAssociatedObject的完成 首先objc::AssociationManager::_mapStorage中是个静态变量,初始化后一直存在,所以相关的数组方针不会被开释,由于被_mapStorage这个静态变量所持有。

iOS 内存优化之工具介绍
iOS 内存优化之工具介绍
从代码中能够看出,调用_object_set_associative_reference时,获取静态变量_mapStorage,然后依据方针指针创立一个object-key,依据该key获取/创立一个hashMap,该hashMap以外部传入的key为键,以包括value的一个方针为值进行存储相关。

简单的说,便是helper方针经过objc_setAssociatedObject记载的数组,终究是被_mapStorage存储,helper经过key的办法进行拜访数组,操作数组。由于helper被数组元素方针强持有了,所以终究也是被_mapStorage引证, 当helper方针没有被其他方针引证时,_mapStorage是否移除相关方针决定了helper是否能被开释。

那依照这个逻辑看的话,岂不是所有方针分类增加的特点都不会被开释?从理论上来说,这是不或许产生的,由于假如随意写一个分类,并为其增加特点的话,都会导致该分类方针无法开释,终究必然会导致很多内存走漏问题。那么,_mapStorage什么时分开释掉相关的方针? 全局查找_object_remove_associations函数,有两处调用,一处是外部调用的接口,一处是在方针进行dealloc调用的时分。

iOS 内存优化之工具介绍

经过objc_destructInstance的完成逻辑能够知道,当方针调用dealloc时,假如方针有绑定相关方针,则会进行调用_object_remove_associations办法开释_mapStorage对该方针的相关记载。

所以这种场景下,除非手动调用objc_removeAssociatedObjects函数进行开释helper的相关方针,否则只能等helper方针的dealloc履行进行主动开释相关方针。可是helper被someItem强持有,someItem被数组持有,数组终究被_mapStorage持有。所以helper并不会调用dealloc办法,而_mapStorage开释数组依赖于helper的dealloc调用,这样就形成了一个隐式的直接持有联系。

小结

所以定位这种问题,需求从事务场景,代码上下文中进行剖析,然后揣度该方针未开释是否是正常状况。比方:

  • 销毁了某个ViewController,可是该vc中的某些方针仍然存在内存中
  • GCD推迟block持有的方针

内存追溯场景

当项目随着迭代越发巨大时,关于某些场景的内存增加的原因难以经过查看代码的办法了解。本场景便是讲述怎么经过运用东西的办法在巨大的源码中定位到内存增加的代码。

假设某个迭代的版别发现内存突然增加,可是不知道是哪块代码引发的问题。比方SDWebImage加载高清图片

1.从头运转demo,点击Large Buffers

2.能够看到模拟器内存由30M+激增到300M+,真机由13M+增加到70M+ (iOS 15以上)

iOS 内存优化之工具介绍
iOS 内存优化之工具介绍

3.运用vmmap -summary MemoryGraphDemo指令,查看demo进程的内存分布状况。

iOS 内存优化之工具介绍
在iOS中SWAPPED SIZE便是紧缩内存巨细,从输出的成果来看,CG Image和CoreAnimation这两块区域占有很多内存(共330M左右)。所以排查的方针放在这两个区域。

4.运用vmmap -v MemoryGraphDemo | grep "CG image\|CoreAnimation" 查看这两块内存区的详细信息。其中会包括相应的占用内存地址规模和巨细。

iOS 内存优化之工具介绍
能够对比图中脏内存和紧缩内存的巨细来锁定大内存块的开始地址和结束地址。

5.运用malloc_history MemoryGraphDemo -fullStacks 0x288000000指令经过传入内存块的开始地址,能够输出该内存块被创立时的一个调用仓库。

iOS 内存优化之工具介绍
iOS 内存优化之工具介绍

从输出的成果中能够发现仓库包括一个SDImageCoderHelper类的调用,找到该类,并定位到31行。

iOS 内存优化之工具介绍
从代码中能够看出这里只针对iOS 15以上版别调用了体系函数imageByPreparingForDisplay,从malloc_history指令的输出成果和断点的办法(该函数前后断点)测验,能够确定是该函数导致运用的内存激增。

6.那么怎么处理这个问题?

针对或许呈现大图的场景设置options

[imageView sd_setImageWithURL:url placeholderImage:nil options:SDWebImageAvoidDecodeImage];

小结

当需求查看内存的分布是否合理时,尽量掩盖事务场景(该办法的缺陷),然后经过以下过程定位内存占用

  1. vmmap -summary process:查看内存的一个整体分布
  2. vmmap -v process | grep "xxx":查看置疑区的详细信息,取得地址
  3. malloc_history process -fullStacks 地址:查看该地址内存的创立仓库
  4. 找到对应事务代码剖析

0x5 总结

本篇文章主要介绍了Xcode内存图和leaks东西的运用,以及排查内存问题的流程与思路:

  1. 运转项目,测验掩盖场景
  2. 运用内存图/leaks查看内存走漏状况
  3. 针对场景查看是否有隐式直接持有场景
  4. 依据状况修复问题
  5. 回归

这套流程足够一般中小项目进行排查内存问题,可是关于大型的、复杂的项目,该流程有显着的缺陷,便是手动操作成本比较高,运用起来并不是十分便利,且测验场景的掩盖率直接影响排查问题的准确率。

这套流程的最佳实践应该是利用UITest测验将内存图文件导出来,并结合leaks、vmmap、malloc_history东西对内存图文件进行剖析,完成主动化输出可视化成果的一套流程。