Skip to content

Latest commit

 

History

History
256 lines (155 loc) · 14.7 KB

File metadata and controls

256 lines (155 loc) · 14.7 KB

内存

1 iOS内存机制

1.1 内存空间

iOS使用虚拟内存机制,内存是采用分段和分页管理的,在iOS系统下,内存从高位到低位分别为:

栈区:存储临时创建的局部变量和函数参数等,作用域执行完毕之后会被系统回收,分配的地址由高到低分布。

堆区:用于存储程序运行中动态分配的内存段(通过alloc、new等函数),例如创建的新对象,默认由ARC进行管理,MRC模式下需要手动进行内存释放,其中分配的地址由低到高分布。

全局静态区:由编译器分配,主要是存放全局变量和静态变量,分配地址由低到高分布。其中包含了BBS区:存放未初始化的全局变量和静态变量。数据区:存放已初始化的全局变量和静态变量。

常量区:存放常量,程序结束后系统释放。

代码区:存放程序代码。


1.2 内存空间

目前最新的iPhone13系列最高的RAM是6GB,相对于安卓机来说不算大,但iOS为每个进程提供了非常大的虚拟内存空间,32位进程有4GB的可寻址空间,而64位的进程更有256TB的可寻址空间,对于正常程序开发绰绰有余。


1.3 内存压缩机制

当物理内存不够用时,iOS会按照LRU原则,将部分物理内存压缩,在需要读写时再解压,从而达到节约内存的效果。

在其他操作系统下,还可以用压缩的内存进行内存和磁盘数据交换,提升效率。交换后的压缩内容放在内存里,发生page fault时再解压缩出来,那么从时间上,只有CPU压缩和解压缩的开销,而没有耗时多的I/O传输的开销,当然代价就是,从空间上,需要占用一部分内存资源。


1.4 内存类型

iOS的内存分为Clean Menory 和 Dirty Memory,上面提到的压缩得到的Commpressed Memory 也属于Dirty类型,对于开发者,通常只需要Dirty Memory即可。

一般创建申请空间时,都是clean类型,写入数据后才变成Dirty。


1.5 VM Region

为了更好的管理内存,系统将一组连续的内存页定义为一个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; 
};

1.6 内存警告

当系统内存不够时,会向当前进程发送内存警告,开发者通过接受内存警告,主动进行处理,以防止进程崩溃。

接受内存警告的三种方式:

  • UIApplicationDelegateapplicationDidReceiveMemoryWarning:

  • UIViewControllerdidReceiveMemoryWarning

  • NSNotificationCenterUIApplicationDidReceiveMemoryWarningNotification

通常情况下,可以清理无用内存来减少当前进程的内存占用,但由于存在压缩内存,使得处理起来相对复杂。

)

如上图所示,当接受到内存警告时,准备将Dictionary中的部分内容释放掉,但由于之前Dictionary长时间未使用,被系统自动压缩了,所以需要先将其解压后再释放,此时物理内存反而比清理前更大了,甚至可能在解压缩时达到内存临界点,产生OOM。


1.7 Jetsam机制和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。


1.8 常见内存问题及优化

1.8.1 内存泄漏
  • ARC模式下,内存泄漏基本都是由于对象循环引用引起的,通过weak或者强制断开的策略可以避免循环。

  • 除开正常的OC对象,如果使用了CoreFoundation对象或者C、C++类型申请了空间,需要在合适的位置进行release。

  • block会对变量进行捕获和持有,很容易产生循环引用,通过__weak__Strong来配合避免循环引用。


1.8.2 WKWebView白屏问题

WKWebView在发生内存问题崩溃时,因为是专门的进程,在app内表现形式为白屏,处理方式是在收到webViewWebContentProcessDidTerminate时reload页面。


1.8.3 图片内存
  • 图片读取

    • 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% 内存,且支持保存辅助图片(深度图、视差图等)。

1.9 OOM监控

  • 指 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)等做数据存储分析。


1.10 其他优化

  • 构建缓存时使用NSCache替代NSMutableDictionary

NSCache是线程安全的,当内存不足时会自动释放,并且可以通过countLimit和totalCostLimit属性设置上限,另外对存在Compressed Memory情况下的内存警告也做了优化,这些都是NSDictionary不具备的。

  • 不要将序列化的数据文件当作数据库使用

plist、XML、JSON等文件修改都是必须替换整个文件,拓展性差,且开销大,容易误用

NSUserDefaults默认是plist

  • iOS Memory Entitlement

苹果在Entitlement中有两个Menory相关的配置,可以进行申请com.apple.developer.kernel.extended-virtual-addressingcom.apple.developer.kernel.increased-memory-limit

两者分别是开启xnu中的jumbo mode,地址进行扩充以及为app申请更多内存。解决了大部分内存问题引起的卡顿,副作用是潜在不合理的申请空间和泄漏不容易被发现的问题。


2 iOS内存分析工具

  • 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 可查,包括电池、启动时间、卡顿情况、内存情况、磁盘读写五部分。