iOS使用虚拟内存机制,内存是采用分段和分页管理的,在iOS系统下,内存从高位到低位分别为:
栈区:存储临时创建的局部变量和函数参数等,作用域执行完毕之后会被系统回收,分配的地址由高到低分布。
堆区:用于存储程序运行中动态分配的内存段(通过alloc、new等函数),例如创建的新对象,默认由ARC进行管理,MRC模式下需要手动进行内存释放,其中分配的地址由低到高分布。
全局静态区:由编译器分配,主要是存放全局变量和静态变量,分配地址由低到高分布。其中包含了BBS区:存放未初始化的全局变量和静态变量。数据区:存放已初始化的全局变量和静态变量。
常量区:存放常量,程序结束后系统释放。
代码区:存放程序代码。
目前最新的iPhone13系列最高的RAM是6GB,相对于安卓机来说不算大,但iOS为每个进程提供了非常大的虚拟内存空间,32位进程有4GB的可寻址空间,而64位的进程更有256TB的可寻址空间,对于正常程序开发绰绰有余。
当物理内存不够用时,iOS会按照LRU原则,将部分物理内存压缩,在需要读写时再解压,从而达到节约内存的效果。
在其他操作系统下,还可以用压缩的内存进行内存和磁盘数据交换,提升效率。交换后的压缩内容放在内存里,发生page fault时再解压缩出来,那么从时间上,只有CPU压缩和解压缩的开销,而没有耗时多的I/O传输的开销,当然代价就是,从空间上,需要占用一部分内存资源。
iOS的内存分为Clean Menory 和 Dirty Memory,上面提到的压缩得到的Commpressed Memory 也属于Dirty类型,对于开发者,通常只需要Dirty Memory即可。
一般创建申请空间时,都是clean类型,写入数据后才变成Dirty。
为了更好的管理内存,系统将一组连续的内存页定义为一个VM Region,每个VM Region都包含了Dirty页数、Compressed页数和已映射到虚拟内存页的列表等信息。
struct vm_region_submap_info_64 {
vm_prot_t protection; /* present access protection */
vm_prot_t max_protection; /* max avail through vm_prot */
vm_inherit_t inheritance;/* behavior of map/obj on fork */
memory_object_offset_t offset; /* offset into object/map */
unsigned int user_tag; /* user tag on map entry */
unsigned int pages_resident; // 已经被映射到物理内存的虚拟内存页列表
unsigned int pages_shared_now_private; /* only for objects */
unsigned int pages_swapped_out; /* only for objects */
unsigned int pages_dirtied; /* only for objects */
unsigned int ref_count; /* obj/map mappers, etc */
unsigned short shadow_depth; /* only for obj */
unsigned char external_pager; /* only for obj */
unsigned char share_mode; /* see enumeration */
boolean_t is_submap; /* submap vs obj */
vm_behavior_t behavior; /* access behavior hint */
vm_offset_t object_id; /* obj/map name, not a handle */
unsigned short user_wired_count;
};
当系统内存不够时,会向当前进程发送内存警告,开发者通过接受内存警告,主动进行处理,以防止进程崩溃。
接受内存警告的三种方式:
-
UIApplicationDelegate的
applicationDidReceiveMemoryWarning:
-
UIViewController的
didReceiveMemoryWarning
-
NSNotificationCenter的
UIApplicationDidReceiveMemoryWarningNotification
通常情况下,可以清理无用内存来减少当前进程的内存占用,但由于存在压缩内存,使得处理起来相对复杂。
如上图所示,当接受到内存警告时,准备将Dictionary中的部分内容释放掉,但由于之前Dictionary长时间未使用,被系统自动压缩了,所以需要先将其解压后再释放,此时物理内存反而比清理前更大了,甚至可能在解压缩时达到内存临界点,产生OOM。
MacOS/iOS是一个从BSD衍生来的系统,内核是XNU,XNU的微内核是Mach,其处理内存警告和异常使用的是Jetsam机制。
该机制,系统从内核中开启了最高优先级的线程来监控整个系统的内存情况,当系统内存不足时,会自动发出内存警告或触发OOM杀死低优先级的进程,腾出内存供其他高优先级进程使用。
OOM(Out Of Memory),是一种系统管理内存的机制,当内存不够时,会自动将低优先级的进行kill,腾出内存供其他高优先级进程使用。xnu本身代码是开源的,可在苹果官方下载,其中内存状态管理相关代码主要在/bsd/kern/kern_memorystatus.h/c
文件中。
#define JETSAM_PRIORITY_REVISION 2
#define JETSAM_PRIORITY_IDLE_HEAD -2
/* The value -1 is an alias to JETSAM_PRIORITY_DEFAULT */
#define JETSAM_PRIORITY_IDLE 0
#define JETSAM_PRIORITY_IDLE_DEFERRED 1 /* Keeping this around till all xnu_quick_tests can be moved away from it.*/
#define JETSAM_PRIORITY_AGING_BAND1 JETSAM_PRIORITY_IDLE_DEFERRED
#define JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC 2
#define JETSAM_PRIORITY_AGING_BAND2 JETSAM_PRIORITY_BACKGROUND_OPPORTUNISTIC
#define JETSAM_PRIORITY_BACKGROUND 3
#define JETSAM_PRIORITY_ELEVATED_INACTIVE JETSAM_PRIORITY_BACKGROUND
#define JETSAM_PRIORITY_MAIL 4
#define JETSAM_PRIORITY_PHONE 5
#define JETSAM_PRIORITY_UI_SUPPORT 8
#define JETSAM_PRIORITY_FOREGROUND_SUPPORT 9
#define JETSAM_PRIORITY_FOREGROUND 10
#define JETSAM_PRIORITY_AUDIO_AND_ACCESSORY 12
#define JETSAM_PRIORITY_CONDUCTOR 13
#define JETSAM_PRIORITY_DRIVER_APPLE 15
#define JETSAM_PRIORITY_HOME 16
#define JETSAM_PRIORITY_EXECUTIVE 17
#define JETSAM_PRIORITY_IMPORTANT 18
#define JETSAM_PRIORITY_CRITICAL 19
#define JETSAM_PRIORITY_MAX 21
/* TODO - tune. This should probably be lower priority */
#define JETSAM_PRIORITY_DEFAULT 18
#define JETSAM_PRIORITY_TELEPHONY 19
...
typedef struct memstat_bucket {
TAILQ_HEAD(, proc) list;
int count;
int relaunch_high_count;
} memstat_bucket_t;
系统定义了多个优先级,每个优先级对应一个memstat_bucket_t
结构体,存放这个优先级下的所有进程,可以看到后台应用程序优先级JETSAM_PRIORITY_BACKGROUND是3,低于前台优先级JETSAM_PRIORITY_FOREGROUND 10,所以当系统内存紧张时,前台进程之前的优先级会被kill掉,如果仍然不满足高优先级进程的内存需求,才会主动kill前台进程。
内存警告和OOM没有必然相关性,当瞬间申请了大量内存,而CPU正在执行其他任务,会导致进程没有收到内存警告就发生了OOM;当进程收到内存警告,该进程优先级较高,且系统通过杀死低优先级进程已释放了足够内存,就不会接收到OOM。
-
ARC模式下,内存泄漏基本都是由于对象循环引用引起的,通过weak或者强制断开的策略可以避免循环。
-
除开正常的OC对象,如果使用了CoreFoundation对象或者C、C++类型申请了空间,需要在合适的位置进行release。
-
block会对变量进行捕获和持有,很容易产生循环引用,通过
__weak
和__Strong
来配合避免循环引用。
WKWebView在发生内存问题崩溃时,因为是专门的进程,在app内表现形式为白屏,处理方式是在收到webViewWebContentProcessDidTerminate时reload页面。
-
图片读取
- imageNamed 会被缓存到内存中,适用于频繁使用的小图片;imageWithContentOfFile 适用于大图片,持有者生命周期结束后既被释放。
-
图片格式
-
iOS 默认创建的图片格式是 SRGB,每个像素点通常包括红、绿、蓝和 alpha 数据4个字节。而实际使用时,图像可能不需要这么多通道。
-
使用 UIGraphicsBeginImageContextWithOptions 创建的格式固定是 SRGB,可以使用 UIGraphicsImageRenderer (iOS10之后)替代,会自动选择最合适的图像格式。
-
-
缩放图像
- 将大图片加载到小空间时, UIImage (UIImage.contentsOfFile)需要先解压整个图像再渲染,会产生内存峰值,用 ImageIO框架 替代 UIImage 可避免图像峰值,ImageIO框架(CGImageSourceCreateWithURL)可以直接指定加载到内存的图像尺寸和信息,省去了解压缩的过程。
-
后台优化
- 当应用切入后台时,图像默认还在内存中 ,可以在退到后台或view消失时从内存中移除图片,进入前台或view出现时再加载图片 (通过监听系统通知) 。
-
HEIC 格式
- HEIC 是苹果推出的专门用于其系统的图片格式,iOS 11以上支持。
- 据测试,相同画质比 JPEG 节省 50% 内存,且支持保存辅助图片(深度图、视差图等)。
-
指 App 在前台因消耗内存过大导致被系统杀死,针对这类问题,我们需要记录发生 FOOM 时的调用栈、内存占用等信息,从而具体分析解决内存占用大的问题。
-
流程是监控 App 生命周期内的内存增减,在收到内存警告时,记录内存信息,获取当前所有对象信息和内存占用值,并在合适的时机上传到服务器。目前比较出名的 OOM 监控框架有 Facebook 的 FBAllocationTracker ,国内的有腾讯开源的 OOMDetector。
-
FBAllocationTracker
原理是 hook 了
malloc/free
等方法,以此在运行时记录所有实例的分配信息,从而发现一些实例的内存异常情况,有点类似于在 app 内运行、性能更好的 Allocation。但是这个库只能监控 Objective-C 对象,所以局限性非常大,同时因为没办法拿到对象的堆栈信息,所以更难定位 OOM 的具体原因。 -
OOMDetector
通过
malloc/free
的更底层接口malloc_logger_t
记录当前存活对象的内存分配信息,同时也根据系统的backtrace_symbols
回溯了堆栈信息。之后再根据伸展树(Splay Tree)等做数据存储分析。
-
- 构建缓存时使用NSCache替代NSMutableDictionary
NSCache是线程安全的,当内存不足时会自动释放,并且可以通过countLimit和totalCostLimit属性设置上限,另外对存在Compressed Memory情况下的内存警告也做了优化,这些都是NSDictionary不具备的。
- 不要将序列化的数据文件当作数据库使用
plist、XML、JSON等文件修改都是必须替换整个文件,拓展性差,且开销大,容易误用
NSUserDefaults默认是plist
- iOS Memory Entitlement
苹果在Entitlement中有两个Menory相关的配置,可以进行申请com.apple.developer.kernel.extended-virtual-addressing
和com.apple.developer.kernel.increased-memory-limit
两者分别是开启xnu中的jumbo mode,地址进行扩充以及为app申请更多内存。解决了大部分内存问题引起的卡顿,副作用是潜在不合理的申请空间和泄漏不容易被发现的问题。
-
Xcode memory gauge:在Xcode的Debug navigator中,可以粗略查看内存占用的情况。
-
Instrument - Allocations:可以查看虚拟内存占用、堆信息、对象信息、调用栈信息,VM Regions信息等,可以分析内存。
-
Instrument - Leaks:用于检测内存泄漏。
-
MLeaksFinder:通过判断UIViewController被销毁后其子View是否也都被销毁,能够检测UIViewController和子View退出时是否dealloc。
-
Instrument - VM Tracker:可以查看内存占用信息,查看各类型内存的占用情况,比如dirty memory的大小等等,可以辅助分析内存过大、内存泄漏等原因。
-
Instrument - Virtual Memory Trace:有内存分页的具体信息,具体可以参考WWDC 2016 - Syetem Trace in Depth。
-
Memory Resource Exceptions:从Xcode10开始,内存占用过大时,调试器能捕获到
EXC_RESOURCE RESOURCE_TYPE_MEMORY
异常,并断点在触发异常抛出的地方。 -
Xcode Memory Debugger:Xcode中可以直接查看所有对象间的相互依赖关系,可以非常方便的查找循环引用的问题,还可以将这些信息导出为memgraph文件。
-
memgraph + 命令行指令:结合上一步输出的memgraph文件,可以通过一些指令来分析内存情况,
vmmap
打印进程信息,以及VMRegions的信息等,结合grep
可以查看指定VMRegions的信息。leaks
可追踪堆中的对象,从而查看内存泄漏、堆栈信息等。heap
打印出堆中所有信息,方便追踪内存占用较大的对象。malloc_history
可以查看heap指令得到的对象堆栈信息,从而方便发现问题。malloc_history
-> Creation;leaks
-> Reference;heap
&vmmap
-> Size。 -
MetricsKit:iOS 13新推出的监控框架,用于收集和处理电池和性能指标,包含下面三部分
-
XCTest Metrics(开发阶段)
-(void)measureWithMetrics:(NSArray<id<XCTMetric>> *)metrics block:(void (^)(void))block;
,通过编写单元测试案例获取对应单元的性能数据。 -
MetricsKit (Beta 或 Release 阶段)
通过
[MXMetricManager.sharedManager addSubscriber:self]
订阅通知,- (void)didReceiveMetricPayloads:(NSArray<MXMetricPayload *> *)payloads
接受通知处理回调。当 App 累计运行 24 小时候后就会进行回调,回调内容包括内存情况、CPU/GPU 占用,读写磁盘数据量等数据。 -
Xcode Metrics Organize (Release 阶段)
iOS 13 后,当用户使用 App 时候,iOS 会记录各项指标,然后发送到苹果服务端上,并自动生成相关的可视化报告。通过 Window -> Organizer -> Metrics 可查,包括电池、启动时间、卡顿情况、内存情况、磁盘读写五部分。
-