iOS开发快速进阶与实战
上QQ阅读APP看书,第一时间看更新

2.1 内存分区

本节所说的是内存,并非是内存管理,是其他系统以及编程语言都有提及的内存分区,是对于编程语言来说比较宽泛的内存说明。

通常我们将内存分区划分为以下几大块。

(1)栈区;

(2)堆区;

(3)全局区;

(4)常量区;

(5)代码区。

我们知道任何一个程序在运行的时候实际是运行在内存中的,这个内存也就是我们通常所说的主存,也叫运行内存,也叫RAM(Random Access Memory),是可以直接与CPU进行交换数据的内部存储器。内存读取速度很快,所以作为操作系统运行程序的区域。不同的分区保存不同的值,值可以为指针,可以为对象,可以为二进制代码,可以为数字等,每个分区有自己的功能,它们一起协作为系统提供更好的任务划分,如图2-1所示。

图2-1 内存分区位置

下面来详细说明每个分区的功能。

栈区(stack):栈区是由系统来自动分配释放,是一个栈的数据结构,存储函数的参数、局部变量、引用。

堆区(heap):堆区是由开发者“手动管理”或者程序结束时由系统全部回收,是一种树状的数据结构,一般用于存储由malloc、new等方式创建的对象。在iOS开发中,大多数关于内存管理方面的问题也多出自此:多是一些开发者没有及时回收内存,或者内存溢出以及泄漏等问题。

全局区(静态存储区):用于存放全局变量和静态变量,存储方式是:未经初始化的全局变量和静态变量存放在一个区域,初始化后的全局变量和静态变量在另一个区域。回收方式也是等进程结束由系统回收。

文字常量区:主要存储基本数据类型的值,以及常量,同样是进程结束后由系统回收。

代码区:存储要执行函数的二进制代码,如果需要执行就加载到该区域中。

下面通过一小段代码,来详解一下。

打印结果如下。

系统的内存分区是按照一定的逻辑来实现的,但并不是绝对的,例如,图2-1中表示堆区中是由低地址向高地址扩展的,obj的地址理应比w1的要低一些,实际上却比w1要高,这是因为堆并不是连续的内存区域,是系统用树来实现的,过程中会考虑空闲区域,例中w1分配的区域过小,所以可能分配在obj之前,如果给w1分配的空间大一些,则w1的地址就会在obj之后了。

这里应该再注意以下几个特例。

1.字符串类型

多个直接声明的相同字符串在内存中只占用一份内存,例如:

     NSLog(@"hello1: %p", @"Hello");
     NSLog(@"hello2: %p", @"Hello");

打印出来的结果是:

     hello1: 0x100001168
     hello2: 0x100001168

这个变量的地址是在常量区中存储,虽然声明的是两个字符串,看似应该开辟两端内存,但通过打印可以看出实际上是同一块内存,这是可以理解的,因为这是同一个固定的字符串,在编译期就确定了的,不会更改,是一个不可变量,因此引用同一份内存并没有什么问题,如果需要在此字符串上进行修改也是另外开辟一段内存。

2.block类型

关于block的内容在本书中有详细的介绍,这里只做简单的说明。

block声明的时候是在栈中的,但赋值给变量的时候会复制到堆中。

     //声明一个block
     NSLog(@"block in stack: %p", ^(){});
//将一个block赋值给一个变量 id block = ^(){}; NSLog(@"block in heap: %p", block);

前面介绍了关于block的内存分区,下面有一份示例代码。

打印结果如下。

     a=3, *p=3
     a=3, *p=1

这是关于block作用的一个非常典型的例子,可以很明确地看到block对外部变量的作用。两个方法中,都声明了一个int类型的a,赋值为1,接着声明一个int指针p指向a。在第一个方法中,直接调用了一个block,由于block还没有被赋值,所以这时block还没有复制到堆中,所以对于a来说也没有发生复制,与p指向的为同一内容。所以经过两次加1后,两者都为3;而对于第二个方法,将这个block赋值给一个临时变量,此时根据之前我们所说的内容,发生了复制,__block修饰的a也复制为一份新的内容,但p依然指向之前的内容,此时p的指向和a已经不是同一内容了,所以∗p依然为1,而a经过两次加1后,变为3。

本节小结

(1)程序运行所使用的内存分类包括栈、堆、常量、全局和代码区等;

(2)了解日常开发者所用的对象和变量对应于存在哪一种内存;

(3)了解block对象在内存中的存储和复制形式,以及对外部变量的持有情况。