点赞评论,感受有用的朋友能够关注笔者公众号 iOS 成长指北,持续更新好文章。html
万字长文,建议收藏阅读node
从 OOM 崩溃出发,涉猎 iOS Jetsam 机制的相关内容,介绍如何得到设备内存阈值。介绍内存分配的基本概念,了解 iOS APP 的内存分布,以及如何分析 iOS 内存占用。引入一些实际的方法来在 iOS 开发过程当中规避内存问题。ios
一切的一切,都从一个 OOM 崩溃出发。git
《iOS Crash Dump Analysis》 一书的翻译工做,对笔者来讲意义重大。让笔者系统的学习了一下如何进行崩溃分析,以及崩溃分析的缘由。程序员
内存问题一直是致使系统崩溃的重要缘由,绝大部分的缘由多是由于开发者在开发过程当中每每会忽视内存问题,咱们常常专一于使用而忘了深究。github
因为内存问题致使的 iOS 应用程序发生的崩溃大体分为如下两种: 错误的内存访问和超出内存限制。在进行深刻以前咱们先了解一下。objective-c
咱们的崩溃报告收集工具会收集崩溃报告,并将其符号化。macos
而不管自建仍是使用自三方崩溃工具都会将崩溃报告中的 Exception Type
,也就是异常类型,放置在显眼位置。swift
若是是使用xcode
Exception Type: EXC_CRASH (SIGABRT)
Exception Codes: 0x0000000000000000, 0x0000000000000000
Exception Note: EXC_CORPSE_NOTIFY
Triggered by Thread: 0
复制代码
在 iOS 崩溃报告中,关于 应用自己 的内存异常类型有两种
EXC_BAD_ACCESS (SIGSEGV)
或 EXC_BAD_ACCESS (SIGBUS)
代表咱们的程序极可能试图访问错误的或者是咱们没有权限访问的内存地址。或因为内存压力,该内存已被释放,即访问错误已经不存在内存位置(常见的如野指针访问)。
SIGSEGV(段冲突):表示存储器地址甚至没有映射到进程地址区间。
SIGBUS(总线错误):内存地址已正确映射到进程的地址区间,但不容许进程访问内存。
笔者在 [译]《iOS Crash Dump Analysis》- 崩溃报告 中介绍了详细介绍了如何分析已有的崩溃报告,如何根据崩溃报告快速定位崩溃问题。
有些内存崩溃问题并不能直接提如今咱们的崩溃报告中。意味着并无特定的异常类型来告知咱们这种错误属于超出内存限制的崩溃。
与 macOS 相比,激进(积极)的内存管理是 iOS 的一个特色,macOS 对内存使用有很是宽松的限制。通俗来讲,移动设备是内存受限的设备。Jetsam
严格的内存管理系统为咱们提供了良好的服务,在给定的 RAM 量下保证最佳的用户体验。
iOS 存在 Foreground Out-Of-Memory (FOOM)
和 Background Out-Of-Memory (BOOM)
两种超出内存限制的 OOM 崩溃现象。从名称上能够看出来,一种是因为使用应用时自己超出内存限制致使的崩溃,另外一种因为当前设备在后台中,而用户正在使用拍照功能进行大量的拍照和图像特效时,此时内存使用量大幅度增长,为了保证正在进行的进程有足够的内存可供使用。
若是在 iOS 崩溃报告中出现异常类型为
EXC_CRASH (SIGQUIT)
时,这意味着应用的某个拓展程序花费了太长的时间或者消耗了太多的内存。
那么当内存不够用时,iOS 会发出内存警告,告知进程去清理本身的内存。iOS 上一个进程就对应一个 app。若是 app 在发生了内存警告,并进行了清理以后,物理内存仍是不够用了,那么就会发生 OOM 崩溃,也就是 Out of Memory Crash
。咱们主要关注正在使用的应用程序发生的 OOM 崩溃,也就是前文提到的 Foreground Out-Of-Memory (FOOM)
。
iOS 经过 Jetsam 机制来实现上述功能。
Jetsam 机制能够理解为操做系统为控制内存资源过分使用而采用的一种管理机制。Jetsam是一个独立运行的进程,每一个进程都有一个内存阈值,一旦超过这个阈值,Jetsam将当即杀死该进程。
在前文咱们提到,OOM 崩溃并无体如今崩溃报告中,而是出如今 Apple 自己的 Jetsam 报告中。在 iOS 中,Jetsam
是将当前应用从内存中弹出以知足当前最重要应用需求的系统。
Jetsam
一词最初是一个航海术语,指船只将不想要的东西扔进海里,以减轻船的重量。
当咱们的应用被 Jetsam 机制杀死时,手机会生成系统日志。在手机系统设置隐私分析中,找到以 JetSamEvent.
的开头的系统日志。在这些日志中,你能够获取一些关于应用程序的内存信息。能够在日志的开头,看到了pageSize,并找到了 perprocesslimit
项(不是全部日志都有,可是能够找到它)。经过使用项目的 rpages * pageSize
能够获得 OOM 的阈值。
一个 Jetsam 日志大概像下面同样:
{"bug_type":"298","timestamp":"2020-10-15 17:29:58.79
+0100","os_version":"iPhone OS 14.2
(18B5061e)","incident_id":"B04A36B1-19EC-4895-B203-6AE21BE52B02"
}
{
"crashReporterKey" :
"d3e622273dd1296e8599964c99f70e07d25c8ddc",
"kernel" : "Darwin Kernel Version 20.1.0: Mon Sep 21 00:09:01
PDT 2020; root:xnu-7195.40.113.0.2~22\/RELEASE_ARM64_T8030",
"product" : "iPhone12,1",
"incident" : "B04A36B1-19EC-4895-B203-6AE21BE52B02",
"date" : "2020-10-15 17:29:58.79 +0100",
"build" : "iPhone OS 14.2 (18B5061e)",
"timeDelta" : 7,
"memoryStatus" : {
"compressorSize" : 96635,
"compressions" : 3009015,
"decompressions" : 2533158,
"zoneMapCap" : 1472872448,
"largestZone" : "APFS_4K_OBJS",
"largestZoneSize" : 41271296,
"pageSize" : 16384,
"uncompressed" : 257255,
"zoneMapSize" : 193200128,
"memoryPages" : {
"active" : 45459,
"throttled" : 0,
"fileBacked" : 34023,
"wired" : 49236,
"anonymous" : 55900,
"purgeable" : 12,
"inactive" : 40671,
"free" : 5142,
"speculative" : 3793
}
},
"largestProcess" : "AppStore",
"genCounter" : 1,
"processes" : [
{
"uuid" : "7607487f-d2b1-3251-a2a6-562c8c4be18c",
"states" : [
"daemon",
"idle"
],
"age" : 3724485992920,
"purgeable" : 0,
"fds" : 25,
"coalition" : 68,
"rpages" : 229,
"priority" : 0,
"physicalPages" : {
"internal" : [
6,
183
]
},
"pid" : 350,
"cpuTime" : 0.066796999999999995,
"name" : "SBRendererService",
"lifetimeMax" : 976
},
.
.
{
"uuid" : "f71f1e2b-a7ca-332d-bf87-42193c153ef8",
"states" : [
"daemon",
"idle"
],
"lifetimeMax" : 385,
"killDelta" : 13595,
"age" : 94337735133,
"purgeable" : 0,
"fds" : 50,
"genCount" : 0,
"coalition" : 320,
"rpages" : 382,
"priority" : 1,
"reason" : "highwater",
"physicalPages" : {
"internal" : [
327,
41
]
},
"pid" : 2527,
"idleDelta" : 41601646,
"name" : "wifianalyticsd",
"cpuTime" : 0.634077
},
.
.
复制代码
这里能够看一下笔者的 [译]《iOS Crash Dump Analysis 2》- 系统诊断 学习如何获取设备的系统诊断报告以及对于获取的 Jetsam 报告如何解读。
固然也能够阅读一下 Identifying High-Memory Use with Jetsam Event Reports、 Monitoring Basic Memory Statistics 等官方文档。
Apple 并无准确的文档说明每一个设备的内存限制。对于设备的内存 OOM 阈值大概有如下几个方法获取。这里获取的限制最好是在重启 iPhone 之后,使得设备清空 RAM 缓存。
在前文介绍了如何从 Jetsam 日志中经过使用项目的 rpages * pageSize
能够获得 OOM 的阈值。这里就再也不赘述了。
互联网上有不少关于OOM 阈值的文章并列举了不一样设备的 OOM阈值,笔者感受比较精确的是这两个
咱们能够在 StackOverflow post 大概了解不一样设备的内存限制。有问题,StackOverflow 一下。
基于 Split 工具 获取的 Jaspers 列表
设备 RAM | 阈值范围(百分制) |
---|---|
256MB | 49% - 51% |
512MB | 53% - 63% |
1024MB | 57% - 68% |
2048MB | 68% - 69% |
3072MB | 63% - 66% |
4096MB | 77% |
6144MB | 81% |
特别的案例:
设备 RAM | 阈值范围(百分制) |
---|---|
iPhone X (3072MB) | 50% |
iPhone XS/XS Max (4096MB) | 55% |
iPhone XR (3072MB) | 63% |
iPhone 11/11 Pro Max (4096MB) | 54% - 55% |
根据笔者的经验,1GB设备 安全阈值 45%,2-3GB 设备安全阈值 50% 4GB设备安全阈值 55%。 macOS 的百分比可能更大。
利用下面的方法获取当前设备的 RAM 值
[NSProcessInfo processInfo].physicalMemory
复制代码
didReceiveMemoryWarning
当内存不够用时,iOS 会发出内存警告,告知进程去清理本身的内存, 在当前页面(Controller)中,这个方法是 - (void)didReceiveMemoryWarning
。能够经过不停地增长内存,来获取当前设备的 OOM 阈值。
咱们能够根据如下方法获取 设备的 OOM 阈值
#import "ViewController.h"
#import <mach/mach.h>
#define kOneMB 2014 * 1024
@interface ViewController ()
{
NSTimer *timer;
int allocatedMB;
Byte *p[10000];
int physicalMemorySizeMB;
int memoryWarningSizeMB;
int memoryLimitSizeMB;
BOOL firstMemoryWarningReceived;
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
// Do any additional setup after loading the view.
physicalMemorySizeMB = (int)([[NSProcessInfo processInfo] physicalMemory] / kOneMB);
firstMemoryWarningReceived = YES;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
if (firstMemoryWarningReceived == NO) {
return ;
}
memoryWarningSizeMB = [self usedSizeOfMemory];
firstMemoryWarningReceived = NO;
}
- (IBAction)startTest:(UIButton *)button {
[timer invalidate];
timer = [NSTimer scheduledTimerWithTimeInterval:0.01 target:self selector:@selector(allocateMemory) userInfo:nil repeats:YES];
}
- (void)allocateMemory {
p[allocatedMB] = malloc(1048576);
memset(p[allocatedMB], 0, 1048576);
allocatedMB += 1;
memoryLimitSizeMB = [self usedSizeOfMemory];
if (memoryWarningSizeMB && memoryLimitSizeMB) {
NSLog(@"----- memory warnning:%dMB, memory limit:%dMB", memoryWarningSizeMB, memoryLimitSizeMB);
}
}
- (int)usedSizeOfMemory {
task_vm_info_data_t taskInfo;
mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);
if (kernReturn != KERN_SUCCESS) {
return 0;
}
return (int)(taskInfo.phys_footprint / kOneMB);
}
@end
复制代码
在 iOS 13 以上的设备中,咱们可使用系统 os/proc.h
所提供的一个新的 API
__BEGIN_DECLS
/*!
* @function os_proc_available_memory
* ... 为了篇幅进行截断
* @result
* The remaining bytes. 0 is returned if the calling process is not an app, or
* the calling process exceeds its memory limit.
*/
API_UNAVAILABLE(macos) API_AVAILABLE(ios(13.0), tvos(13.0), watchos(6.0))
extern
size_t os_proc_available_memory(void);
__END_DECLS
复制代码
来获取当前设备的内存阈值
#import <mach/mach.h>
#import <os/proc.h>
...
- (int)limitSizeOfMemory {
if (@available(iOS 13.0, *)) {
task_vm_info_data_t taskInfo;
mach_msg_type_number_t infoCount = TASK_VM_INFO_COUNT;
kern_return_t kernReturn = task_info(mach_task_self(), TASK_VM_INFO, (task_info_t)&taskInfo, &infoCount);
if (kernReturn != KERN_SUCCESS) {
return 0;
}
return (int)((taskInfo.phys_footprint + os_proc_available_memory()) / 1024.0 / 1024.0);
}
return 0;
}
复制代码
你也能够定义一个方法,在使用大量内存以前,先获取一下当前的可用内存
#import <os/proc.h>
+ (CGFloat)availableSizeOfMemory {
if (@available(iOS 13.0, *)) {
return os_proc_available_memory() / 1024.0 / 1024.0;
}
// ...
}
复制代码
请用真机测试,不要使用模拟器测试!
咱们知道 iOS/macOS 的内核是 XNU,XNU 是开源的。咱们能够在开源的 XNU 内核源代码中探索 Apple Jetsam 的具体实现。
XNU 内核的内层是 Mach 层。做为一个微内核,mach 是一个只提供基本服务的薄层,好比处理器管理和调度以及IPC(进程间通讯)。XNU 的第二个主要部分是 BSD 层。咱们能够把它当作是 Mach 的外层。BSD 为最终用户的应用程序提供了一个接口。其职责包括进程管理、文件系统和网络。
内存管理中常见的抛弃时间也是由 BSD 生成的,所以让咱们能够从 BSD init 做为切入点来探讨其原理。
BSD init 初始化各类子系统,好比虚拟内存管理等等。
...
有多种缘由可能会致使堆内存增加过分并致使 FOOM 崩溃:
通常来讲致使 OOM 的主要缘由就是代码中会出现循环引用。也就是咱们常说的内存泄露
内存泄漏是指在某一时刻分配的内存,但从未被释放,也再也不被应用程序引用。因为没有对它的引用,如今就没有方法访问和释放它,内存不能再被使用。
内存泄漏无可避免地增长了应用程序的内存占用,这部分 RAM 将永远不会释放,直到应用程序中止运行。
对于正在处理须要大量内存或计算时间的频繁访问对象的开发人员而言,缓存多是相当重要的。尽管在性能方面提供了巨大的好处,可是缓存可能会占用大量内存。缓存如此多的对象,可能会你或其余应用程序没有可用的RAM,从而有可能迫使系统终止它们。
常见的缓存示例是缓存图像。
就内存而言,图像渲染是很昂贵的。该过程分为两个阶段:解码和渲染。
解码阶段是将图像数据(数据缓冲区)转换为可由显示硬件(图像缓冲区)解释的信息。这包括每一个像素的颜色和透明度。
渲染阶段是硬件消耗图像缓冲区并将其实际 绘制
在屏幕上。
在 iOS 中渲染的图片实际上所占的内存其大小是经过将每一个像素的字节数乘以图像的宽度和高度来计算的。渲染一份像素为 3024 x 4032
,颜色空间为 RGB,带 DisplayP3 色彩配置文件。该颜色配置文件每一个像素占用16位大小。所以,常规方法渲染这样一个照片须要的内存大概须要 3024 x 4032 x 8 / 1024 / 1024 ≈ 93.02 MB
的大小。
经过在设置图像属性以前简单地将图像调整为图像视图的大小,能够减小RAM数量级。能够查看 Mattt 大神的 Image Resizing Techniques 了解在 iOS 中咱们如何调整获取的图片大小。
在了解 iOS/macOS 内存以前,咱们先了解一下基本概念。
全部进程(执行的程序)都必须占用必定数量的内存,它或是用来存放从磁盘载入的程序代码,或是存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些倒是按须要动态分配和回收的。
在操做系统中,管理内存的方法是首先将连续的内存排序为内存页,而后将页面排序为段。这容许将元数据属性分配给应用于该段内的全部页面的段。这容许咱们的程序代码(程序 TEXT )被设置为只读但可执行。提升了性能和安全性。
RAM(random access memory)即随机存储内存,这种存储器在断电时将丢失其存储内容,故主要用于存储短期使用的程序。
ROM(Read-Only Memory)即只读内存,是一种只能读出事先所存数据的固态半导体存储器。
App 程序启动时,系统会将 App 程序从 Flash 或 ROM 中拷贝到内存(RAM),而后从 RAM 里面执行代码。CPU 不能直接从 ROM 中读取并运行程序。
关于 iOS 的启动过程,能够参照笔者以前的文章 深刻理解 iOS 启动流程和优化技巧 文章,获取 iOS 的启动流程。
macOS 和 iOS都包含一个彻底集成的 虚拟内存 系统。这两个系统为每一个 32 位进程提供了多达 4 GB的可寻址空间。
虚拟内存容许操做系统摆脱物理 RAM 的限制。虚拟内存管理器为每一个进程建立一个逻辑地址空间(或虚拟
地址空间),并将其划分为称为页面的大小相同的内存块。处理器和它的内存管理单元(MMU)维护一个页表来将程序的逻辑地址空间中的页面映射到计算机 RAM 中的硬件地址。当程序代码访问内存中的地址时,MMU 使用页表将指定的逻辑地址转换为实际的硬件内存地址。这种转换是自动发生的,而且对正在运行的应用程序是透明的。
就程序而言,其逻辑地址空间中的地址老是可用的。可是,若是应用程序访问当前不在物理 RAM 中的内存页上的地址,则会发生页面错误。当发生这种状况时,虚拟内存系统调用一个特殊的页面错误处理程序来当即响应错误。页面错误处理程序中止当前执行的代码,在物理内存中找到一个空闲的页面,从磁盘加载包含所需数据的页面,更新页表,而后将控制权返回给程序代码,而后程序代码能够正常访问内存地址。这个过程称为分页。
若是物理内存中没有可用的可用页面,则处理程序必须首先释放现有页面觉得新页面腾出空间。系统发布页面的方式取决于平台。在 OSX 中,虚拟内存系统一般将页写入备份存储。备份存储是一个基于磁盘的存储库,其中包含给定进程使用的内存页的副本。将数据从物理内存移动到备份存储称为 paging out
(或 swapping out
);将数据从备份存储移回物理内存称为paging in
(或 swapping in
)。
可是,iOS 不支持交换空间,而且大多数移动设备都不支持交换空间。移动设备的大容量内存一般是闪存,它的读写速度远远小于计算机使用的硬盘,这致使即便移动设备上使用了交换空间,也没法提升性能。其次,移动设备自己容量每每不足,内存的读写寿命也有限,在这种状况下,使用闪存进行内存交换有点奢侈。
页面永远不会被调出到磁盘,可是只读页面仍然能够根据须要从磁盘调出。
在早期的 iOS 设备上, 分页的大小为 4 KB,而在 A7 和 A8 芯片上,对 64 位机器其分页大小为 16 KB 而 32 为机器分页大小依旧为 4 KB。对于 A9 及以上芯片,其分页大小都为 16 KB。
针对 iOS 设备来讲,其32位机器内存分页大小为 4 KB 而 64 位机器内存分页大小为 16 KB
进程的逻辑地址空间由内存的映射区域组成。每一个映射的内存区域包含已知数量的虚拟内存页。每一个区域都有特定的属性来控制诸如继承(区域的一部分能够从父
区域映射)、写保护以及它是否被链接(即,它不能被调出)。由于区域包含已知数量的页面,因此它们是页面对齐的,这意味着区域的起始地址也是页面的起始地址,而结束地址也定义了页面的结尾。
内核将 VM 对象与逻辑地址空间的每一个区域相关联。内核使用VM对象来跟踪和管理相关区域的驻留页和非驻留页。区域能够映射到备份存储的一部分或文件系统中的内存映射文件。每一个 VM 对象都包含一个映射,该映射将区域与默认 pager 或 vnode pager 相关联。默认的寻呼机是一个系统管理器,它管理后台存储中的非驻留虚拟内存页,并在请求时获取这些页。vnode pager 实现内存映射文件访问。vnode pager 使用分页机制直接向文件提供一个窗口。这种机制容许读写文件的某些部分,就像它们位于内存中同样
VM 对象对咱们分析常驻内存具备颇有效的。后面咱们会用点时间来分析一下如何进行 iOS 内存分析。
当咱们在讨论 iOS 内存占用及内存管理时,咱们提到的都是虚拟内存
应用程序的内存使用取决于页数及内存页的大小。
上文讲到内存分页,实际上内存页也有分类,通常来讲分为 Clean Memory 、 Dirty Memory 和 Compressed Memory 的概念。
因为闪存容量和读写寿命的限制,iOS 上没有交换空间机制,所以改用压缩内存。压缩内存将压缩和存储未访问的页面。内存压缩器用于存储和检索压缩内存。 内存压缩主要执行两个操做
压缩内存可以在内存紧张时将最近使用的内存使用率压缩到原始大小的一半如下,并在须要时能够解压缩和从新使用。它不只节省了内存,并且提升了系统的响应速度。
具备如下优点:
例如,当咱们使用 NSDictionary
来缓存数据时,假设如今咱们已经使用了 3 页内存,当咱们不访问它时,它可能被压缩为 1 页,而当咱们再次使用它时,它将被解压缩为 3 页。
本质上来说,Dirty Memory 也属于 Compressed Memory 。
macOS 也存在内存压缩,经过内存压缩能够提升内存交换的效率。
仅 Dirty Memory 和 Compressed Memory 会增长内存占用量。
内存压缩技术使得内存的释放变得复杂。内存压缩技术是在操做系统级实现的,它对进程不敏感。有趣的是,若是当前进程收到内存警告,则该进程此时准备释放大量误用的内存。若是访问了太多的压缩内存,当内存被解压缩时,内存压力会更大,而后出现 OOM,当前进程被系统杀死。
缓存数据的目的是减轻 CPU 的压力,可是过多的缓存将占用过多的内存。在某些须要缓存数据的状况下,可使用 NSCache 代替 NSDictionary。 NSCache 分配的内存其实是可清除内存,能够由系统自动释放。还建议将 NSCache 和 NSPurgeableData 结合使用不只可使系统根据状况回收内存,并且还能够在清理内存的同时删除相关对象。
iOS 内存占用量能够经过 Xcode 内存量规进行测量,Instruments 提供了多种工具来分析应用程序的内存占用状况。
VM Tracker 提供 Dirty Memory 大小 、交换(预压缩)大小和驻留大小的内存分配信息。对肯定 Dirty Memory 大小有显著做用。
Xcode
选择 Product
中的 Profile
当你有了 Snapshots 以后,你能够看到 Dirty Memory 状态随着时间的推移。你还能够看到哪些对象占用了你大部分的 Dirty Memory 。若是您想更深刻地研究,可使用 VMMap,这对于高级内存调试很是有用。
与 Heap 和 Leaks 同样,VMMap是一个很好的命令行工具,用于在虚拟内存环境中调试内存对象。
在使用 VMMap 以前,首先应该准备当前应用程序的的 Memory Graph。
选择 View Memory Graph Hierarchy
.memgraph
文件VMMAP -sumary test.memgraph
命令进行分析使用 -summary
提供了虚拟内存区域中的大小,例如虚拟内存大小,常驻内存大小,Dirty Memory 大小 ,交换大小等。Dirty Memory 和交换大小是增长内存大小的主要方面。
前文咱们说过,每一个进程都有独立的虚拟内存地址空间,也就是所谓的进程地址空间。如今咱们稍微简化一下,一个 iOS app 对应的进程地址空间大概以下图所示:
iOS 中的内存大体能够分为代码区,全局/静态区,常量区,堆区,栈区。
代码段是用来存放可执行文件的操做指令(存放函数的二进制代码),也就是说是它是可执行程序在内存种的镜像。代码段须要防止在运行时被非法修改,因此只准许读取操做,而不容许写入(修改)操做——它是不可写的。
全局/静态区存放的是静态变量,静态全局变量,以及全局变量。初始化的全局变量,静态变量,静态全局变量存放在同一区域,未初始化的变量存放在相邻的区域。程序结束后由系统释放。
常量区存放的就是字符串常量,int常量等这些常量。
这块区域是由编译器自动分配并释放的,栈区存放的是函数的参数及自动变量。栈是向低地址扩展的一块连续的内存区域。分配在栈上的变量,当函数的做用域结束,系统就会自动销毁变量。
堆区内存通常是由程序员本身分配并释放的。当咱们使用 alloc 来分配内存时分配的内存就是在堆上。因为咱们如今大部分都是使用 ARC,因此如今堆区的分配和释放也基本不须要咱们来管理。堆区是向高地址扩展的一块非连续区域。
栈区是由编译器自动分配和释放,可是堆区是由程序员来分配和释放。
栈区:栈区内存由编译器分配和释放,在函数执行时分配,在函数结束时收回。只要栈区剩余内存大于所申请的内存,那么系统将为程序提供内存。 堆区:系统有一个存放空闲内存地址的链表,当程序员申请堆内存的时候,系统会遍历这个链表,找到第一个内存大于所申请内存的堆节点,并把这个堆节点从链表中移除。因为这块内存的大小不少时候不是刚恰好所申请的同样大,因此剩余的那一部分还会回到这个空闲链表中。
栈区:栈区是向低地址扩展的数据结构,也就是说栈顶的地址和栈的容量大小是由系统决定的。栈的容量大小通常是 2M,当申请的栈内存大于 2M 时就会出现栈溢出。所以栈可分配的空间比较小。 堆区:堆是向高地址扩展的数据结构,是不连续的。堆的大小受限于计算机系统中有效的虚拟空间,所以堆可分配的空间比较大。
栈:栈由系统自动分配,速度较快,可是不受程序员控制。 堆:堆是由 alloc 分配的内存,速度较慢,而且容易产生内存碎片。
应用中新建立的每一个线程都有专用的栈空间,该空间由保留的内存和初始提交的内存组成。栈能够在线程存在期间自由使用。线程的最大栈空间很小,这就决定了如下的限制。
可被递归调用的最大方法数。
每一个方法都有其本身的栈帧,并会消耗总体的栈空间。
一个方法中最多可使用的变量个数。
全部的变量都会载入方法的栈帧中,并消耗必定的栈空间。
视图层级中能够嵌入的最大视图深度。
渲染复合视图将在整个视图层级树中递归地调用 layoutSubViews 和 drawRect 方法。如 果层级过深,可能会致使栈溢出。
知道 iOS APP 内存分配对咱们是有好处的。众所周知的 sunnyxx 大神的 神经病院objc runtime入院考试 的最后一题:
@interface Sark : NSObject
@property (nonatomic, copy) NSString *name;
@end
@implementation Sark
- (void)speak {
NSLog(@"my name's %@", self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
id cls = [Sark class];
void *obj = &cls;
[(__bridge id)obj speak];
}
@end
复制代码
在理解 iOS 中内存分配的入栈顺序,以及栈地址如何进行偏移,才能更好地理解为何最终答案打印的是当前的 self
。
内存管理模型基于 持有关系 的概念。若是一个对象正处于被持有状态,那它占用的内存就不能被回收。当一个对象建立于某个方法的内部时,那该方法就持有这个对象了。若是这个对象从方法 返回,则调用者声称创建了持有关系。这个值能够赋值给其余变量,对应的变量一样会声称创建了持有关系。
一旦与某个对象相关的任务所有完成,那么就是放弃了 持有关系 。这一过程没有转移 持有关系 ,而是分别增长或减小了持有者的数量。当持有者的数量降为零时,对象会被释放相关的内存也会被回收。
这种持有关系 持有关系 被称做 引用计数(Retain Count)。
Apple 提供了两种内存管理的方法
咱们通常将手动管理引用计数的方法称为(manual reference counting,MRC)手动引用计数管理。官方文档上则将这种方式称为(manual retain-release, MRR)手动持有释放,能够经过跟踪本身拥有的对象显式地管理内存。
ARC 背后的原理是依赖编译器的静态分析能力,经过在编译时找出合理的插入引用计数管理代码,从而完全解放程序员。ARC 的工做原理是在编译时添加代码,以确保对象的生存期尽量长,但不会更长。从概念上讲,它遵循与手动引用计数相同的内存管理约定,为开发者添加了适当的内存管理调用
手动的内存管理方法,已经淹没在 iOS 开发的历史长河中。对从 ARC 时代成长起来的 iOS 开发者来讲,虽然 ARC 帮咱们解决了引用计数的大部分问题,可是一旦开发者须要与底层 Core Foundation 对象交互的话,就须要本身来考虑管理这些对象的引用计数。
NSObject protocol
中定义的方法和命名惯例一块儿提供了一个引用计数环境,内存管理的基本模式处于这种环境中。NSObject 类还定义了一个方法 dealloc,该方法在释放对象时自动调用。NSObject 类做为 Foundation 框架的根类,几乎主要的类都继承于 NSObject 类。
内存管理模型基于对象全部权。任何对象均可以有一个或多个全部者。只要一个对象至少有一个全部者,它就继续存在。若是一个对象没有全部者,运行时系统会自动销毁它。为了确保清楚地知道您什么时候拥有一个对象,何时没有,Cocoa 设置了如下策略:
要想避免内存泄漏和应用崩溃,你应当在编写 Objective-C 代码时牢记这些规则。
错误的内存管理执行会致使两种问题:
内存泄漏不等于发生了循环引用,内存泄漏是指内存在不须要访问的时候没有释放,或者说任何致使对象在内存中长时间活动的缘由均可能会致使内存泄漏。
例如,不合理的使用单例,不合理的使用全局变量(主要指非全局常量), 以及不合理的存储一些内容。
若是一个对象存活的时间足够长,而且这个对象附带了大量的对象属性的话,那么这也会致使内存泄露。
17年,网易在其公众号上发布了其称为 大白 的 iOS APP运行时Crash自动修复系统,几乎涵盖了当前 iOS 崩溃的主要缘由。在其 野指针防御 一节中,当对已经释放的内存进行访问时,提供一个临时对象用来响应消息传递。而且说明了须要控制其内存大小。
当咱们须要实现一个全局变量或单例时,或者是当咱们存储一些信息用做信息收集时,咱们须要考虑,在何时咱们应该释放掉内存。
当咱们使用 runtime(获取方法,属性、关联对象等) 或者是进行绘制时,不要忘记释放内存。
objc_property_t *props = class_copyPropertyList(class, &propsCount);
...
free(props);
复制代码
例如 FXBlurView 里,仅仅为了展现一些 Core Foundation 绘制时须要
- (UIImage *)blurredImageWithRadius:(CGFloat)radius iterations:(NSUInteger)iterations tintColor:(UIColor *)tintColor
{
...
vImage_Buffer buffer1, buffer2;
buffer1.width = buffer2.width = CGImageGetWidth(imageRef);
buffer1.height = buffer2.height = CGImageGetHeight(imageRef);
buffer1.rowBytes = buffer2.rowBytes = CGImageGetBytesPerRow(imageRef);
size_t bytes = buffer1.rowBytes * buffer1.height;
buffer1.data = malloc(bytes);
buffer2.data = malloc(bytes);
...
//copy image data
CFDataRef dataSource = CGDataProviderCopyData(CGImageGetDataProvider(imageRef));
memcpy(buffer1.data, CFDataGetBytePtr(dataSource), bytes);
CFRelease(dataSource);
...
//free buffers
free(buffer2.data);
free(tempBuffer);
...
CGImageRelease(imageRef);
CGContextRelease(ctx);
free(buffer1.data);
return image;
}
复制代码
在使用模拟器或真机运行时,咱们能够查看应用程序的内存占用
关注 Usage over Time
部分, 若是在运行过程当中发现内存峰值存在阶梯性增加,那么颇有可能发生内存泄露。可是此时咱们只知道出现了内存泄露。咱们须要去尝试一些方法来找到泄露的地方。
不可免俗的,对 iOS 来讲,真正容易致使内存泄露的仍是循环引用。尤为是在大量使用 block, delegate,NSTimer、KVO的时候。
以持有关系声明对内存的引用就会引起一个问题,对象间互相持有,致使持有关系造成一个闭环,最终任何内存都没法释放。
在这里咱们介绍几个发现循环引用的方法
经过生成 memory graph 来查看内存使用,熟练解读 memory graph 文件的相关信息,足够开发者发现开发过程当中的各类问题。让咱们来调试 iOS 内存 - Memory Graph
善用 Instruments 会极大提升咱们的开发效率,而且会应用程序保驾护航。这个 WWDC Getting Started with Instruments,能让你成为一个 Instruments 调试高手。
delloc
或 deinit
Apple 组件几乎不会引发你的循环引用的,问题绝大可能出如今咱们这里。咱们能够建立一个 Memory Checker 类用来判断 Controller 对象是否被释放,而且 Hook Controller 的 pop 或者 dispresent 方法。
能够改写下面的Swift 代码值 Objective-C 代码
import Foundation
public class MemoryChecker {
public static func verifyDealloc(object: AnyObject?) {
#if DEBUG
DispatchQueue.main.asyncAfter(deadline: .now() + 5) { [weak object] in
if let object = object {
fatalError("Class Not Deallocated: \(String(describing: object.classForCoder ?? object)))")
}
}
#endif
}
}
复制代码
import UIKit
class NavigationController: UINavigationController {
override func popViewController(animated: Bool) -> UIViewController? {
let viewController = super.popViewController(animated: animated)
MemoryChecker.verifyDealloc(object: viewController)
return viewController
}
}
复制代码
咱们的内存并非当即释放掉的,因此须要定义一个延迟时间。
经过如下几个方法来规避开发过程当中的循环引用
当咱们在子类中使用其父对象时, 例如在 subview 或 cell 中须要执行跳转时,请使用 weak 修饰父对象 Controller,或者是使用代理时,请用 weak 修饰你的 delegate。
目标对象的角色是持有者。链接对象包括如下几种。
使用 delegate 的对象。委托应该被看成目标对象,即持有者。
包含目标和 action 的对象。例如,UIButton
观察者模式中被观察的对象。观察者就是持有者,并会观察发生在被观察对象上的变化。
不要让本身成为本身的 Target 目标,请不要本身持有本身
避免直接引用外部变量并不意味之本身须要在每一个block前都是用 __weak
获取一个 weakSelf
,无脑添加 weakSelf
和 strongSelf
。如何分析当前 Block 内到底有没有造成闭环,须要开发者好好思考。
这里推荐阅读霜神的 深刻研究 Block 用 weakSelf、strongSelf、@weakify、@strongify 解决循环引用。
当咱们使用子组件来封装一些页面时,例如轮播图、计时器等操做时,能够监听页面的移除方法,而后在移除时作一些内存释放。或者执行一些方法时,在方法执行完成,将自身置为 nil。
- (void)willMoveToSuperview:(UIView *)newSuperview {
if (!newSuperview) {
[self invalidateTimer];
}
}
复制代码
NSProxy 实现根类所需的基本方法,包括那些在 NSObject protocol
协议中定义的方法。可是,做为一个抽象类,它不提供初始化方法,而且在接收到任何它不响应的消息时引起异常。
NSProxy 一般用来实现消息转发机制和惰性初始化资源。
使用 NSProxy,你须要写一个子类继承它,而后须要实现 init 以及消息转发的相关方法。
咱们可使用 NSProxy 来处理一些强引用的 target
, 打破循环引用。这里主要处理 NSTimer。
@interface WeakProxy : NSProxy
@property (weak, nonatomic, readonly) id target;
+ (instancetype)proxyWithTarget:(id)target;
- (instancetype)initWithTarget:(id)target;
@end
@implementation WeakProxy
+ (instancetype)proxyWithTarget:(id)target{
return [[self alloc] initWithTarget:target];
}
- (instancetype)initWithTarget:(id)target{
_target = target;
return self;
}
- (void)forwardInvocation:(NSInvocation *)invocation{
SEL sel = [invocation selector];
if (self.target && [self.target respondsToSelector:sel]) {
[invocation invokeWithTarget:self.target];
}
}
- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector{
return [self.target methodSignatureForSelector:aSelector];
}
- (BOOL)respondsToSelector:(SEL)aSelector{
return [self.target respondsToSelector:aSelector];
}
@end
复制代码
众所周知,整个 iOS 的应用都是包含在一个自动释放池 block 中的。
int main(int argc, char * argv[]) {
@autoreleasepool {
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
复制代码
虽然,Swift 没有 main 函数,可是有一个 @UIApplicationMain 来替代 main 函数。
系统在每一个runloop迭代中都加入了自动释放池Push和Pop
AppKit 和 UIKit 框架将事件 - 循环的迭代放入了 autoreleasepool
块中。所以,一般 不须要你本身再建立 autoreleasepool
块了。
当咱们建立一个不少临时对象的循环时
咱们说过,AppKit 和 UIKit 框架将事件 - 循环的迭代放入了 autoreleasepool
块中,因此当循环执行完成之后,内存会降到必定的值,可是咱们能够经过建立本身的 autoreleasepool
来避免内存峰值。
在异步线程中实现 autoreleasepool
iOS 应用程序包含在一个主 autoreleasepool
中,因此当主线程的 runloop 完成一次迭代时,autoreleasepool
会自动进行释放操做,而对于异步线程,能够本身主动实现一个autoreleasepool
《The case of iOS OOM Crashes at Compass》
《iOS — Advanced Memory Debugging to the Masses》
《What is the difference between ROM and RAM?》
《Learn more about oom (low memory crash) in iOS》
《iOS Memory Deep Dive - WWDC 2018 - Videos - Apple Developer》
《Advanced Memory Management Programming Guide》