说起内存管理,看似老生常谈,而真正掌握内存管理的核心其实并不简单。ARC/MRR以及“谁分配谁就负责释放”这种基本原则是很重要的,但不是本文要讨论的重点。之前本人还没在小站发过相关的文章,本篇文章中,我本人是想结合实际开发和调试中遇到的一些细节问题,来谈谈iOS的内存管理内在机制和调试方法。
上一篇文章已经是4月份的了,时间飞快又过去了好久,小站5月份没有文章更新,罪过罪过。最近小站的站长我又转换到新团队新岗位,在支付宝做客户端开发感受颇多,不过身在一个技术流团队,工作很有挑战,自己感觉很充实、很“幸福”。iOS开发当中的内存管理,可深可浅,一般应用程序开发过程当中可能并不需要关注太多,如果不是来到支付宝,也许就不会有这么多心得来整理此文。
关于内存,我准备分为内存管理的基本原则、原理和调试方法、实际问题几部分整理。那么接下来我就和大家一起复习和稍微深入一下iOS的内存管理的原理和原则。
0. 概述
内存,简单来说就是内部存储,复杂来说要从冯·诺依曼计算机结构说起。冯·诺依曼结构,也称做普林斯顿结构,目前和哈佛结构相对,指出了计算机由运算器、控制器、存储器、输入和输出设备几大部件组成。如今我们个人用的机器估计都是这个套路,而且运算器和控制器都合在一起,就是CPU,中央处理器。那么内存就是CPU能直接读写访问数据的地方(寄存器是在CPU内的,不算哈),有些朋友说谁谁谁的iPhone内存16G、64G,我只能说这个理解方法仅限于存储部件放在手机里(内)了,严格来讲这算“外存”,我们要讨论的不是这个。
冯·诺依曼结构还说了,内存是用来存啥的呢?指令+数据!(哈佛的恐怕就不一样了)对于我们开发者来说,指令基本就是代码逻辑,至于数据么变量常量肯定都算是的了。
内存有多大?不大,现今主流的个人机器也就几G的样子。iPhone? 统统1G。
我们操作系统都是运行在内存之上的,1G好像不算大,所以为了支持多进程,也为了支持大程序,抽象的虚拟存储的概念诞生了。
简要的概念先陈述到这,下面详细说。哦,对了,ARC和MRR我还是得提一下,这个要是真不知道还真的自己先去了解一下去。
1. 通用内存基本原理
说iOS的内存,有必要先看看一般的计算机都是怎么干的,iPhone也是计算机,通用的道理一样要遵循。这里提两方面:虚存的概念,内存内容的大致分布。
虚拟存储系统。刚刚提到了,物理内存就那么大点,但是还要跑多个程序,还要接受消耗很大内存的程序,这怎么办?凉拌。搞计算机的人都是很聪明的,在操作系统层面做了物理地址和逻辑地址之间的映射转换,当然处理器硬件上也做了支持。一个程序在运行时,实际要用到的指令和数据都是很有限的,不可能从头到尾同时用。那么对于一个程序来说,假装自己有非常大的空间,实际上只要有条理的把暂时要用到的部分放进物理内存供CPU访问就好,这样第二个问题解决了。那既然每个程序(进程)只用一小块,那整个物理内存就可以分给多个程序(进程)用了,第一个问题也迎刃而解。当然,这样做的前提是,数据和指令的动态进出,用完了的暂时不用的踢出内存,需要用的及时加载进来。这个具体的实现方式就多种多样了,很多实现方式是在外存中开了个交换区供换入换出,但iOS可略有不同。
内存的大致分布。不久以前,我发了一篇文章整理了Mach-O文件的格式分析,里面很复杂地放了好多东西,包括我们Build打包时的代码和数据。而Mach-O文件正是我们开发内容的一个静态展现形式,要想在运行的时候看样子,就得看这文件里包含的东西是怎么放进内存的。Objective-C是基于C的,不放看下C程序进程的内存分布:
一个运行时进程的典型内存分布
最简单来说分为两大部分:指令+数据。再细分一点,五部分:代码(指令),初始化数据区,未初始化数据区,堆,栈。
代码(指令,text)就不用说了,最静态的,就是只读的东西;
初始化数据,简单理解就是有初始值的变量、常量;
未初始化数据,只声明未给值的变量,运行前统统为0,之所以单独分出来,估计是性能考虑,因为这些东西都是0,没必要放在程序包里,也不用copy;
栈,程序运行记录,每个线程,也就是每个执行序列各有一个(看crash log最容易理解),都是编译的时候能确定好的,还有一个特点就是这里面的数据可以不用指针,也不会丢;
堆,最灵活的内存区,用途多多,动态分配和释放,编译时不能提前确定,我们的Objective-C对象都是这么来的,都存在这里,通常堆中的对象都是以指针来访问的,指针从线程栈中来,但不独属于某个线程,堆也是对复杂的运行时处理的基础支持,还有就是ARC还是MRR、“谁分配谁释放”说的都是堆上对象的管理;
其实,这个内存中的布局方式大部分操作系统中的大部分进程都是类似的。Objective-C的程序包对运行时有着复杂的支持和内容划分,但也都是在这个大的框架下进行的。
2. iOS的内存管理
其实,iOS的内存管理和其它操作系统大同小异。这里按照苹果文档所述,重点对堆内存分配整理下。
首先,iOS和其它系统一样,内存分页,每页4K。多个页构成一个region统一管理,负责管理的对象是VM object,其中包含了pager、size、resident pages等诸多属性。
不管是Objective-C的[NSObject alloc],还是C代码的对内存分配,最终重任都会落到malloc库上,释放也是如此,最终都将使用malloc库中的free()。
malloc库中有很多malloc的同族函数可以动态分配内存,会结合参数在free
pages中进行最适分配。如果分配的内存比较大,可以直接使用vm_allocate,得到一个VM对象(与Linux类似),这个在实际使用前不分配物理内存。malloc的内部实现都是开源的,感兴趣的可以去了解去看。
此外,对于malloc,还有一个Zone的概念(貌似与Linux的概念不完全相同),可以简单理解为一组free
page单元,可以统一管理操作。默认情况,在第一次调用malloc时,系统会生成一个default
zone,后续的默认分配在此进行。比如,malloc_zone_xxx()函数都是对特定的zone进行分配操作,执行zone->xxx()。
最后强调一下iOS特别需要注意的点:
当前的主流iPhone实际物理内存都不超过1G,可以说不算大。不过和Android机比起来,我不得不为苹果的设计称赞,1G空间利用得如此高效,性能不差,也控制了发热。
那么在这仅有的1G内存中,iOS的操作系统更是抛弃了不必要的复杂——系统层面不支持App内存页换出。当内存吃紧时,对于可以重新载入的只读数据来说,直接清理掉,而对于可写的数据,只能通过App自己去管理维护。内存紧张时,iOS会向App发起memory
warning,不配合释放足够内存者,杀!
App调试时的物理内存情况
上图是使用Activity
Monitor调试时的一个截图,可以看到在尽量不释放自身内存的情况下(为了bug调试特意这么做的),支付宝钱包的内存可以做到502M物理内存占用。再稍微高一点点,系统就会连前台运行的App一起Kill掉。留下一个Unknown的log。
3. 其它
基本的原理就简要整理到此,如下是一些参考:
What and where are the stack and heap?