简单总结下iOS内存方面的一些知识
磁盘内部有一个区域叫做交换空间(Swap Space),MMU(内存管理单元) 会将暂时不用的内存块内容写在交互空间上(硬盘),这就是Swap Out;当需要时候再从Swap Space中读取到内存中,这就是Swap In;Swap in和swap out的操作都是比较耗时的, 频繁的Swap in和Swap out操作非常影响系统性能;
Page In/Out和 Swap In/Out 概念类似,只不过Page In/Out是将某些页的数据写到内存/从内存写回磁盘交互区;而Swap In/Out是将整个地址空间的数据写到内存/从内存写回磁盘交互区;本质都是交互机制。
macOS支持这类交换机制,但是iOS不支持;主要有两方面考虑吧:
从高地址到低地址各区域如下:
内存页按照各自的分配和使用状态,分为 Clean
和 Dirty
两类。其中Clean Page
是可以被回收的,Dirty Page
不能;
int *array = malloc(20000 * sizeof(int)); // 第1步 array[0] = 32 // 第2步 array[19999] = 64 // 第3步 复制代码
第一步,申请一块长度为80000 字节的内存空间,按照一页 16KB 来计算,就需要 6 页内存来存储。当这些内存页开辟出来的时候,它们都是 Clean 的;
第二步,向处于第一页的内存写入数据时,第一页内存会变成 Dirty;
第三步,当向处于最后一页的内存写入数据时,这一页也会变成 Dirty;
iOS进程中所有内存就是由许许多多的VM Region
组成的;
VM Region
指一段连续的内存页(在虚拟地址空间里),VM Region的结构如下
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; /* only valid for objects */ 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; }; 复制代码
VM Region包含的重要信息有:
可以通过了解pages_dirtied和pages_swapped_out来了解VM Region
的真实物理内存使用。
从 iOS7 开始,系统开始采用Compressed Memory
机制优化内存使用,内存类型可以分为三类:
Clean Memory
:可以被释放或重建的,主要包括:
_DATA_CONST
段,当 App 在运行时使用到了某个 framework,它所对应的 _DATA_CONST
的内存就会由 Clean 变为 Dirty。Dirty Memory
:指那些被写入过数据的内存,主要包括:
_DATA
段和 _DATA_DIRTY
段)在使用 framework 的过程中会产生Dirty Memory,使用单例或者全局初始化方法是减少Dirty Memory;这是因为单例一旦创建就不会销毁,全局初始化方法会在 class 加载时执行。 复制代码
Compressed Memory
:
在内存吃紧时,系统会将不使用的内存进行压缩(Compresses unaccessed pages)
在需要的时候,进行解压 (Decompresses pages upon access)
优势:减少了不活跃内存占用;减少磁盘IO带来的损耗;压缩/解压十分迅速,能够尽可能减少 CPU 的时间开销;支持多核操作。
举例:当我们使用 NSDictionary
去缓存数据的时候,假设现在已经使用了 3 页内存,当不访问的时候可能会被压缩为 1 页,再次使用到时候又会解压成 3 页。
介绍Clear Memory
和 Dirty Memory
的Code如下:
//堆分配的内存 Dirty Memory NSString *str1 = [NSString stringWithString:@"Welcome!"]; //常量字符串, 存放在一个只读数据段里面,这段内存释放后,还可以在读取重建 Clear Memory NSString *str2 = @"Welcome!"; //分配100M虚拟内存,当没有用时没有建立映射,Clear Memory char *buf = malloc(100 * 1024 *1024); 关系 for (int i = 0; i < 3 * 1024 * 1024; ++i) { //写入数据了,Dirty Memory buf[i] = rand(); } 复制代码
说明:在内存吃紧的情况下,释放Clean Memory
,不能释放Dirty Memory
,所以Dirty Memory
的内存越多,App的稳定性越差。
Jetsam
机制Jetsam机制是操作系统为了控制内存资源过度使用而采用的一种管理机制;Jetsam是一个独立运行的进程,会把一些优先级不高或者占用内存过大的App杀掉;在杀掉App后会记录一些数据信息并保存到日志。
App优先级可以这么简单理解:前台App > 后台App; 占用内存少 > 占用内存多;
Jetsam产生的这些日志可以在手机设置->隐私->分析中找到,日志是以JetsamEvent
开头,日志中有内存页大小(pageSize),CPU时间(cpuTime)等字段。
查看设置->隐私->分析
中以JetsamEvent开头的系统日志,关注两个重要的信息;
"pageSize" : 16384, //内存页达到上限 "rpages" : 948, //App 占用的内存页数量 "reason" : "per-process-limit", //App 占用的内存超过了系统对单个 App 的内存限制。 复制代码
applicationDidReceiveMemoryWarning:
didReceiveMemoryWarning
FOOM(Foreground Out Of Memory),是指App在前台因消耗内存过多引起系统强杀。对用户而言,表现跟Crash一样。
Facebook早在2015年8月提出FOOM检测办法,大致原理是排除各种情况后,剩余的情况是FOOM;Facebook如何判定上一次启动是否出现FOOM方法:
排查法有误报的可能,因为有些被系统强杀case,但是我们捕获不到信息,也可能被归类到OOM;已知被系统强杀的case是:OOM和watchdog(Code 0x8badf00d)。
发现FOOM问题的关键:监控App使用内存增长,在收到内存警告通知时,dump 内存信息,获取对象名称、对象个数、各对象的内存值,并在合适的时机上报到服务器;加强对大内存的分配监控。
内存泄露(Memory Leak):指申请的内存空间使用完毕之后未回收,内存泄露问题多的话,对App质量影响很大;
目前引起内存泄露的主要原因是循环引用(堆内存中对象相互引用,彼此都得不到释放的机会),目前,调试阶段使用Instrument的Leaks工具发现,线上利用MLeaksFinder发现后上报;
如何避免内存泄露在后面有介绍
WKWebview白屏问题,严格来说,是一种内存方面的问题;之前的UIWebview因为内存使用过大会Crash,而WKWebview不会Crash,会白屏;
WKWebView是一个多进程组件,Network Loading
以及UI Rendering
在其它进程中执行,当WKWebView总体的内存占用比较大时,WebContent Process
会Crash,从而出现白屏现象。
解决办法:
KVO监听URL, 当URL为nil,重新reload
在进程被终止回调中,重新reload
// 此方法适用iOS9.0以上 - (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0){ //reload } 复制代码
野指针:指向一个已删除的对象 或 受限内存区域的指针;目前此类问题很少了,主要来自两个方面
assign(unsafe_unretain)
修饰的 delegate
和target-action
修改成了weak,内存被回收的时候,这些指针设为nil,这也大幅度减少了野指针的出现。目前绝大部分App都是iOS 9起步,野指针少了很多,但是工程中依然会有野指针问题,本质还是内存使用不当;
Mach Exception
大多数都是野指针的问题,崩溃日志里最多见objc_msgSend
和unrecognized selector sent to
等等。
对于野指针问题,最好能复现,使用Zombie Object
帮助调试,Zombie Object
实现原理就是 hook 住了对象的dealloc
方法,通过调用自己的__dealloc_zombie
方法来把对象进行僵尸化,当这个对象再次收到消息,objc_msgsend
的时候,调用abort()崩溃并打印出调用的方法。。
在iOS 10.0 - 10.1,苹果bug引入nano_free Crash问题,这些Crash发生libsystem_malloc.dylib
中的 nano zone
内的;
libsystem_malloc.dylib
中,对内存的管理有两个实现:nano zone
和scalable zone
。他们分别管理不同大小的内存块:
当时微信团队提出的几种解决思路,最后给的解决方案是不使用nano zone,具体描述
guard zone
。malloc_zone_create
创建的)。
更多见:
善待"大"图(位图大小大于60MB)解码:将原图裁剪成多个小图,然后依次绘制到目标位图context中,具体可见SDWebImage中和关于**decodedAndScaledDownImageWithImage:**的实现;
限制并发解码图片的个数;
UIGraphicsImageRenderer
创建 image 上下文,而不是UIGraphicsBeginImageContextWithOptions
,因为前者的性能更好、更高效,并且支持广色域;合理使用autorealsepool
,降低内存峰值,避免 OOM
复用大内存对象,如UITableViewCell对象;懒加载大的内存对象
imageNamed 和 imageWithContentOfFile 的选择
建议NSData读取文件方式
[NSData dataWithContentsOfFile:path options:NSDataReadingMappedIfSafe error:&error]
;用 NSCache
代替 NSMutableDictionary
, 因为 NSCache
可以自动清理内存,在内存吃紧的时候会更加合理。
使用 NSPurgableData 代替NSData,主要原因如下:
栈内存分配:alloca(size_t)
堆内存分配:calloc VS malloc + memset
calloc(size_t num,size_t size)
分配内存时是虚拟内存,只有在访问的时候才会发生物理页的映射关系;malloc + memset
会产生Dirty Memory
calloc
函数得到的内存空间是经过初始化的,其内容全为0,而malloc
函数得到的内存空间是未初始化的,必须使用memset函数来初始化;weak strong dance
来解决 block 中的循环引用问题;之前对内存问题一些整理
之前对图片解码和图片优化一些总结