检测和诊断内存问题
OOM
内存泄漏,难以监控。内存泄漏是指程序在运行过程中,由于设计错误或者代码实现不当,导致程序未能释放已经不再使用的内存,从而造成系统内存的浪费,严重的会导致程序崩溃。内存泄漏是一个非常严重的问题,因为它会导致程序运行速度变慢,甚至会导致程序崩溃。因此,我们在开发过程中,一定要注意内存泄漏的问题。
OOM(Out Of Memory)指的是iOS设备上应用因内存占用过高被系统强制终止的现象。iOS通过Jetsam机制管理内存资源,当设备内存紧张时,会终止优先级低或内存占用大的进程。分为FOOM(前台OOM)和BOOM(后台OOM),FOOM对用户体验影响更大。
Jetsam日志
包括pageSize(内存页大小)、states(应用状态)、rpages(占用的内存页数)、reason(终止原因)。通过pageSize和rpages可计算出应用崩溃时占用的内存大小。
在现代操作系统中,内存管理是一项关键任务。随着移动设备和桌面系统的复杂性增加,内存资源的高效使用变得更加重要。iOS和macOS通过引入“内存压力”(Memory Pressure)机制来优化内存管理,取代了传统的基于虚拟内存分页的管理方法。
虚拟内存系统允许操作系统将物理内存(RAM)和磁盘存储结合使用,以便在内存不足时将不常用的数据移至磁盘。分页(paging)是虚拟内存管理中的一种技术,它将内存划分为小块(页面),并根据需要将它们从物理内存交换到磁盘。然而,分页存在性能瓶颈,尤其是在存储访问速度远低于内存的情况下。
随着设备硬件的变化和用户体验要求的提高,苹果公司在iOS和macOS中引入了“内存压力”机制。内存压力是一种动态监测内存使用情况的技术,它能够实时评估系统内存的使用状态,并根据不同的压力级别采取相应的措施。
内存压力机制通过系统级别的反馈来管理内存。系统会监测内存的使用情况,并将压力分为四个级别:无压力(No Pressure)、轻度压力(Moderate Pressure)、重度压力(Critical Pressure)和紧急压力(Jetsam)。
压力级别的定义与响应:
- 无压力(No Pressure):系统内存充足,没有特别的内存管理措施。
- 轻度压力(Moderate Pressure):系统内存开始紧张,操作系统会建议应用程序释放缓存或非必要的资源。
- 重度压力(Critical Pressure):系统内存非常紧张,操作系统可能会暂停后台任务或终止不活跃的应用程序。
- 紧急压力(Jetsam):这是最严重的内存压力状态,系统可能会直接强制关闭占用大量内存的应用程序,以释放资源确保系统的稳定性。
系统对内存压力的应对措施
为了应对不同的内存压力,iOS和macOS系统采取了多种策略,包括:
- 缓存管理:系统会首先清除可丢弃的缓存数据,以减轻内存负担。
- 后台任务管理:在压力增加时,操作系统会优先暂停或终止低优先级的后台任务。
- 应用程序终止:在紧急情况下,系统会选择性地关闭那些占用大量内存且当前不活跃的应用程序,这一过程被称为“Jetsam”。
使用系统提供的工具(如vm_stat、memory_pressure等)监测应用程序的内存使用情况。这些工具可以帮助开发者识别内存泄漏、过度的缓存使用等问题。开发者可以-通过这些机制感知内存压力的变化。例如,当系统发出UIApplicationDidReceiveMemoryWarningNotification通知时,应用程序应立即释放不必要的资源。
图像处理的过程中内存容易出现相当大的峰值,这会导致了崩溃。比如导入或裁剪大图时。
避免不必要的图片解码,使用更高效的图像处理方法可以对图形处理进行优化。
通过避免提前解码和加载高分辨率图片,只在需要时解码指定尺寸的图片。例如,在加载和裁剪图片时,只处理实际显示在屏幕上的部分,而不是整个图片。通过引入 CGImageSource 和 CGImageThumbnailOptions,在图像加载时直接生成缩略图,从而减少内存占用。与直接使用 UIImage 处理整个图像相比,这种方法更加高效。
示例如下:
1 | import SwiftUI |
查看内存使用情况
在 iOS 中,可以使用 mach_task_basic_info 结构体来查看应用的实际内存使用情况。mach_task_basic_info 是一个 task_info 结构体的子集,它提供了关于任务(进程)的基本信息,包括内存使用情况。特别地,你可以通过 phys_footprint 字段来获取应用程序实际占用的物理内存量。
1 | import Foundation |
在这个示例中,mach_task_basic_info 结构体用于存储基本信息,task_info() 函数用来填充这些信息,phys_footprint 字段提供了物理内存占用的实际数据。使用这些底层 API 需要适当的权限,有时可能无法在应用程序的沙盒环境中访问所有内存信息。
在 iOS 中,NSProcessInfo 的 physicalMemory 属性可以用来获取设备的总物理内存大小。这个属性返回一个 NSUInteger 类型的值,表示物理内存的大小(以字节为单位)。这个方法在 iOS 9 及更高版本中可用。
1 | import Foundation |
vm_statistics_data_t 是一个与虚拟内存相关的数据结构,它提供了关于虚拟内存的统计信息,包括系统的内存使用情况。虽然它不能直接提供应用程序使用的内存,但它可以提供有关整个系统的虚拟内存状态的信息。使用 vm_statistics_data_t 可以获取有关系统内存的更详细的统计数据。
1 | import Foundation |
vm_statistics_data_t 数据结构包含了有关虚拟内存的统计信息,如 free_count(自由页数)、active_count(活跃页数)、inactive_count(非活跃页数)和 wire_count(被锁定的页数)。
获取可用内存的方法如下:
1 | import Foundation |
free_count 表示系统中未使用的空闲内存页数。inactive_count 表示系统中未使用但可能会重新使用的内存页数。可用内存可以通过将空闲内存和非活跃内存的页数乘以页面大小来计算得到。
造成内存泄漏的常见原因
内存泄漏指的是程序中已动态分配的堆内存由于某些原因未能释放或无法释放,导致系统内存浪费,程序运行速度变慢甚至系统崩溃。
- 循环引用:对象A强引用对象B,对象B又强引用对象A,或多个对象互相强引用形成闭环。使用Weak-Strong Dance、断开持有关系(如使用__block关键字、将self作为参数传入block)。
- Block导致的内存泄漏:Block会对其内部的对象强引用,容易形成循环引用。使用Weak-Strong Dance、断开持有关系(如将self作为参数传入block)。
- NSTimer导致的内存泄漏:NSTimer的target-action机制容易导致self与timer之间的循环引用。在合适的时机销毁NSTimer、使用GCD的定时器、借助中介者(如NSObject对象或NSProxy子类)断开循环引用、使用iOS 10后提供的block方式创建timer。
- 委托模式中的内存泄漏:UITableView的delegate和dataSource、NSURLSession的delegate。根据具体场景选择使用weak或strong修饰delegate属性,或在请求结束时手动销毁session对象。
- 非OC对象的内存管理:CoreFoundation框架下的对象(如CI、CG、CF开头的对象)和C语言中的malloc分配的内存。使用完毕后需手动释放(如CFRelease、free)。
Metrics
Metrics和XCTest中的memgraph 了解和诊断 Xcode 的内存性能问题。
内存泄漏检测工具原理
内存泄漏指的是程序在运行过程中,分配的内存未能及时释放,导致程序占用的内存持续增加。内存泄漏检测工具的基本原理是监控和管理对象的生命周期,检测那些在生命周期结束后仍未被释放的对象。
FBRetainCycleDetector
FBRetainCycleDetector 是由 Facebook 开源的一个用于检测 iOS 应用中的内存泄漏的工具。内存泄漏通常是由于对象之间的强引用循环导致的,FBRetainCycleDetector 的工作原理就是检测对象图中的强引用循环,进而帮助开发者识别和修复这些泄漏。
FBRetainCycleDetector 的核心思想是通过分析对象之间的引用关系来识别可能的循环引用。它通过以下步骤实现这一点:
- 对象图构建:FBRetainCycleDetector 首先会从一个指定的对象开始,递归地遍历该对象的所有属性和关联对象,构建一个引用图。这个图的节点是对象,边是对象之间的强引用。
- 深度优先搜索 (DFS):在构建完对象图之后,FBRetainCycleDetector 会对图进行深度优先搜索,寻找从起始对象到自身的循环路径。换句话说,它会查找路径起始和终止于同一个对象的闭环。
- 循环检测:当找到一个循环路径时,FBRetainCycleDetector 就会将其标记为潜在的内存泄漏。检测到的循环会以易于理解的方式输出,帮助开发者定位和解决问题。
为了避免不必要的检测,FBRetainCycleDetector 允许开发者定义一些属性过滤规则,忽略一些不会导致泄漏的引用。例如,可以跳过一些不可见的系统属性或自定义的非持有性引用。工具能够识别并忽略弱引用(weak或unowned),因为这些引用不会导致内存泄漏。FBRetainCycleDetector 具有较高的灵活性,开发者可以通过扩展和定制对象图的遍历规则,使其适应不同的应用场景和复杂对象结构。由于对象图的遍历和循环检测可能会带来性能开销,FBRetainCycleDetector 主要用于开发和调试阶段,而不建议在生产环境中长期使用。
通常,FBRetainCycleDetector 会在调试时被使用。开发者可以通过简单的代码调用,检测指定对象是否存在循环引用。例如:
1 | FBRetainCycleDetector *detector = [FBRetainCycleDetector new]; |
通过以上代码,可以查找someObject 是否存在循环引用,并返回检测到的循环路径。
在实际应用中,FBRetainCycleDetector 被广泛用于检测复杂的对象之间的引用关系,特别是在自定义控件、大型视图控制器、网络回调等场景下,容易产生强引用循环的问题。通过早期检测和解决这些循环引用,可以大大提高应用的内存管理效率,减少内存泄漏带来的问题。
MLeaksFinder
MLeaksFinder 是一款由腾讯 WeRead 团队开源的 iOS 内存泄漏检测工具,其原理主要基于对象生命周期的监控和延迟检测机制。
MLeaksFinder 通过为基类 NSObject 添加一个 -willDealloc 方法来监控对象的生命周期。当对象应该被释放时(例如,ViewController 被 pop 或 dismiss 后),该方法被调用。在 -willDealloc 方法中,MLeaksFinder 使用一个弱指针(weak pointer)指向待检测的对象,以避免因为对象已经被释放而导致的野指针访问问题。MLeaksFinder 通过检查视图控制器的生命周期来检测内存泄漏。每个 UIViewController 都有一个 viewDidDisappear 方法,这个方法会在视图控制器从屏幕上消失时被调用。MLeaksFinder 通过在 viewDidDisappear 被调用时,检测该视图控制器是否已经被释放,如果没有被释放则认为存在内存泄漏。对于视图 (UIView),MLeaksFinder 会在视图被从其父视图中移除时(即 removeFromSuperview 调用后)检查视图是否已经被释放。如果视图没有被释放,则认为存在内存泄漏。MLeaksFinder 通过扩展 NSObject 的功能(即为 NSObject 添加一个 Category)来追踪对象的生命周期。当对象的 dealloc 方法没有在预期的时间内被调用时,就可以判断该对象是否泄漏。
在 -willDealloc 方法中,MLeaksFinder 使用 dispatch_after 函数在 GCD(Grand Central Dispatch)的主队列上设置一个延迟(通常是2到3秒)执行的 block。这个 block 在延迟时间后执行,尝试通过之前设置的弱指针访问对象。如果对象已经被释放(即弱指针为 nil),则认为没有内存泄漏;如果对象仍然存活,则认为存在内存泄漏。MLeaksFinder 通过将对象的检测任务加入到下一个 Runloop 中执行,从而避免在当前线程中直接执行检测操作。这种方式确保了不会影响主线程的性能,同时能在适当的时间进行内存泄漏的检测。
如果在延迟时间后对象仍然存活,MLeaksFinder 会执行相应的检测逻辑,并可能通过断言(assertion)中断应用(具体行为可能根据配置和版本有所不同)。MLeaksFinder 会在应用运行时自动检测内存泄漏,不需要开发者手动触发。检测到内存泄漏后,MLeaksFinder 通常会弹出警告框(alert)或通过日志(log)输出相关信息,帮助开发者定位和解决内存泄漏问题。
MLeaksFinder 使用了方法交换技术替换如dismissViewControllerAnimated:completion:等方法,确保释放时触发检测。调用willDealloc方法,设置延时检查对象是否已释放。若未释放,则进入assertNotDealloc方法,中断言提醒开发者。
当 MLeaksFinder 检测到潜在的内存泄漏时,它还可以打印堆栈信息,帮助开发者找出导致对象无法释放的具体代码路径。通过willReleaseChild、willReleaseChildren方法构建子对象的释放堆栈信息。这通常通过递归遍历子对象,并将父对象和子对象的类名组合成视图堆栈(view stack)来实现。
MLeaksFinder 还可能集成了循环引用检测功能,使用如 Facebook 的 FBRetainCycleDetector 这样的工具来找出由 block 等造成的循环引用问题。MLeaksFinder 提供了一种白名单机制,允许开发者将一些特定的对象排除在泄漏检测之外。这在某些对象确实需要持久存在的场景下非常有用。MLeaksFinder 非常轻量,不会显著影响应用的性能。集成简单,自动化检测,极大地方便了开发者发现内存泄漏问题。在某些复杂的情况下,可能会有误报(即认为对象泄漏了,但实际上没有)。
PLeakSniffer
PLeakSniffer是一个用于检测iOS应用程序中内存泄漏的工具。PLeakSniffer的基本工作原理:通过对控制器和视图对象设置弱引用,并使用单例对象周期性地发送ping通知,如果对象在控制器已释放的情况下仍然响应通知,则可能存在内存泄漏。
PLeakSnifferCitizen协议的设计及其在NSObject、UIViewController、UINavigationController和UIView中的实现。每个类都通过实现prepareForSniffer方法来挂钩适当的生命周期方法(如viewDidAppear、pushViewController等),在适当的时机调用markAlive方法,将代理对象附加到被监测的对象上,以便后续的ping操作能够检测到对象的存活状态。
代理对象PObjectProxy的功能,它主要负责接收ping通知并检查宿主对象是否应当被释放,如果检测到可能的内存泄漏,就会触发警报或打印日志。通过这种方式,PLeakSniffer能够在运行时检测到iOS应用中可能存在的内存泄漏问题。
其他内存泄漏检测工具
- LifetimeTracker
hook malloc方法
要在 iOS 上 hook malloc 方法,可以使用函数拦截技术。以下是一个示例,展示如何使用 Fishhook 库来 hook malloc 方法。
将 Fishhook 库添加到你的项目中。你可以通过 CocoaPods 或手动添加 Fishhook 源代码。
1 |
|
在实际项目中使用时,注意性能开销和日志记录的影响。
malloc logger
malloc_logger 是 iOS 和 macOS 中用于内存分配调试的一个工具。它允许开发者设置一个自定义的日志记录器函数,以便在内存分配和释放操作发生时记录相关信息。通过使用 malloc_logger,开发者可以更容易地检测和诊断内存问题,如内存泄漏、过度分配等。
以下是一个使用 Objective-C 实现的示例,展示如何设置和使用 malloc_logger:
1 |
|
在这个示例中,我们定义了一个自定义的 malloc_logger 函数 custom_malloc_logger,并在 setCustomMallocLogger 函数中将其设置为当前的 malloc_logger。然后,在 main 函数中,我们测试了内存的分配和释放操作,并通过日志记录器记录这些操作的信息。
通过这种方式,开发者可以在内存分配和释放时记录相关信息,从而更好地理解和优化应用程序的内存使用情况。
内存快照检测方案
扫描进程中所有Dirty内存,建立内存节点之间的引用关系有向图,用于内存问题的分析定位。
在 iOS 中,可以使用 vmregionrecurse_64 函数来获取所有内存区域的信息。
1 |
|
在iOS中,可以使用libmalloc库提供的malloc_get_all_zones函数来获取所有内存区域(zone)的信息。malloc_get_all_zones可以遍历所有的内存区域,并为每个区域执行一个回调函数,从而获取详细的内存分配信息。
以下是一个简单的代码示例,展示如何使用malloc_get_all_zones来获取并打印内存区域的信息:
1 |
|
使用单独的malloc_zone管理采集模块的内存使用,减少非法内存访问。遍历进程内所有VM Region(虚拟内存区域),获取Dirty和Swapped内存页数。重点关注libmalloc管理的堆内存,获取存活内存节点的指针和大小。
为内存节点赋予详细的类型名称,如Objective-C/Swift/C++实例类名等。通过运行时信息和mach-o、C++ ABI文档获取C++对象的类型信息。遍历内存节点,搜索并确认节点间的引用关系。对栈内存和Objective-C/Swift堆内存进行特殊处理,获取更详细的引用信息。
后台线程定时检测内存占用,超过设定的危险阈值后触发内存分析。内存分析过程中,对内存节点进行引用关系分析,生成内存节点之间的引用关系有向图。通过图算法,找到内存泄漏的根原因。
libmalloc 内存日志分析
通过代码控制内存日志开关,可以在内存泄漏发生时,输出内存日志。内存日志包括内存分配、释放、引用计数变化等信息,用于分析内存泄漏的原因。
在 iOS 开发中,libmalloc 提供了 turn_on_stack_logging 和 turn_off_stack_logging 方法,用于启用和禁用堆栈日志记录。这些方法可以帮助开发者在调试和分析内存问题时记录内存分配的堆栈信息。以下是一个使用这些方法的代码示例:
1 |
|
在这个示例中,我们首先调用 turnonstacklogging 方法来启用堆栈日志记录,然后进行一些内存分配和释放操作。接着,我们调用 __machstackloggingenumeraterecords 方法获取所有堆栈日志记录,并使用 __machstackloggingframesforuniquedstack 方法解析每个日志记录以获取堆栈帧信息。最后,我们调用 turnoffstacklogging 方法来禁用堆栈日志记录。
通过这种方式,开发者可以在需要时启用和禁用堆栈日志记录,并解析这些日志记录以获取详细的堆栈信息。需要注意的是,这些函数在实际项目中使用时,需要确保在合适的时机启用和禁用堆栈日志记录,以避免性能开销和不必要的日志记录。