标签:堆栈、内存管理、ARC、@property 属性、@synthesize 属性合成、@autoreleasepool 自动释放池;
3.1 栈
当程序执行某个方法(或函数)时,会从内存中名为栈(stack)的区域分配一块内存空间,这块内存空间称为帧( frame)。帧负责保存程序在方法内声明的变量的值。在方法内声明的变量称为局部变量(local variable)。
当某个应用启动并运行 main
函数时,它的帧会被保存在栈的底部。当 main
调用另一个方法(或函数)时,这个方法(或函数)的帧会压入栈的顶部。被调用的方法还可以再调用其他方法,依此类推,最终会在栈中形成一个塔状的帧序列。当被调用的方法(或函数)结束时,程序会将其帧从栈顶“弹出”并释放。如果同一个方法再次被调用,则应用会创建一个全新的帧,并将其压入栈的顶部。
3.2 堆
堆(heap)是指内存中的另一块区域,和栈是分开的。为这两类内存区域分别取名堆和栈,是为了能够形象地描述这两个概念。
栈会按照后进先出的规则保存一组帧,而堆则包含了大量无序的活动对象,需要通过指针来保存这些对象在堆中的地址。
当应用向某个类发送 alloc
消息时,系统会从堆中分配出一块内存,其大小足够存放相应对象的全部实例变量。
ARC 和内存管理
编写 iOS 应用时,需要通过 ARC 管理内存,也就是自动引用计数。
3.3 指针变量与对象所有权
指针变量暗含了对其所指向的对象的所有权(ownership)。
- 当某个方法(或函数)有一个指向某个对象的局部变量时,可以称该变量拥有该变量所指向的对象。
- 当某个对象有一个指向其他对象的实例变量时,可以称该对象拥有该实例变量所指向的对象。
3.4 强引用与弱引用
只要指针变量指向了某个对象,那么该对象就会多一个拥有者,并且不会被程序释放。这种指针特性称为强引用(strong reference)。
程序也可以选择让指针变量不影响其指向对象的拥有者个数。这种不会改变对象拥有者个数的指针特性称为弱引用( weak reference)。
使用 __weak
关键字,可以将某个变量声明为具有弱引用特性。
大部分强引用循环问题都可以为其确定一个父-子关系。通常情况下,父对象应该使用具有强引用特性的指针,指向子对象。而子对象则应该使用具有弱引用特性的指针,指回父对象。这样就可以避免强引用循环问题。
具有弱引用特性的指针指向的对象被释放后,指针会自动设置为 nil
。
3.5 属性
通过属性,也可以为类声明实例变量并实现相应的存取方法,而且更简便。
声明一个属性,等于隐含地为相应名称的实例变量声明一对存取方法。
属性的特性
属性的特性用于描述相应存取方法的行为。
@property (nonatomic, readwrite, strong) NSString *itemName;
多线程特性
多线程特性(Multi-threading attribute)有两种可选类型:nonatomic
和 atomic
。
大多数 Objective-C 程序员会将这个特性设置为 nonatomic
。
因为 nonatomic
不是默认类型,所以在声明属性时,必须明确地写出 nonatomic
。
读/写特性
读/写特性(Read/write attribute)也有两种可选类型:readwrite
和 readonly
。
编译器会为具有 readwrite
特性的属性生成存方法和取方法,如果是 readonly
类型,则只会生成取方法。
内存管理特性
内存管理特性(Memory management attribute)有四种可选类型: strong
、weak
、copy
和
unsafe_unretained
。这些类型决定相应的实例变量将如何引用对象。
对于不指向任何对象的属性(例如int valuelnDollars
),不需要做内存管理,这时应该选用 unsafe_unretained
,它表示存取方法会直接为实例变量赋值。 Apple 引入ARC 之前曾经使用 assign
表示这种类型。
unsafe_unretained
中的 “unsafe(不安全)” 可能会误导读者。该类型的“不安全”是相对于弱引用而言的。与弱引用不同, unsafe_unretained
类型的指针指向的对象被销毁时,指针不会自动设置为 nil,而是成为空指针,因此不安全。但是当处理非对象属性(non -object-)时,是不会出现空指针问题的。
unsafe_unretained
是非对象属性的默认值,所以 valuelnDollars
属性不用明确写出该类型。
通常情况下,当某个属性是指向其他对象的指针,而且该对象的类有可修改的子类(例如 NSString/NSMutableString 或 NSArray/NSMutableArray)时,应该将该属性的内存管理特性设置为 copy
。
声明为 copy
的原因:如果属性指向的对象的类有可修改的子类,那么该属性可能会指向可修改的子类对象,同时,该对象可能会被其他拥有者修改。因此,最好先复制该对象,然后再将属性指向复制后的对象(编写具有防御性的代码)。
自定义属性的存取方法
可以在实现文件中编写自定义的存取方法,覆盖属性默认实现的方法。
⚠️ 如果既覆盖了存方法,也覆盖了取方法(或者为只读属性覆盖了取方法),那么编译器就不会再自动创建
相应的实例变量了。如果需要实例变量,就必须明确声明。
3.6 属性合成
属性会自动生成存取方法,也会自动声明和创建实例变量。
在头文件中声明属性时,只会生成存取方法的声明。为了让属性生成实例变量并实现存取方法,该属性必须被合成 synthesized)。通常情况下,编译器会自动合成属性并生成默认的实例变量和存取方法。如果需要自定义属性的合成方式,可以在实现文件中使用 @synthesize
指令:
示例:见源码中的 Person 类。
3.7 Autorelease 池与 ARC 历史
Clang 静态分析器(Clang static analyzer)。
当对象收到 autorelease
消息时,某个自动释放池会成为该对象的临时拥有者。
自动释放池解决的问题:某个方法创建了一个新的对象,但是创建方又不需要成为该对象的拥有者。为了能返回新创建的对象,同时避免提前释放问题,就可以向新创建的对象发送 autorelease
消息。便捷方法借助自动释放池,将新创建的对象返回给调用方,又不产生内存管理问题。便捷方法的代码示例如下:
+ (BNRItem *)someItem {
BNRItem *item = [[[BNRItem alloc] init] autorelease];
return item;
}
‼️ ARC 下禁止使用 autorelease
特性!
@autoreleasepool
指令后面跟一对花括号,可以创建一个自动释放池。
在 @autoreleasepool
中,如果某个方法返回一个新创建的对象,而该方法的方法名不包含 alloc
和 init
,那么这个新创建的对象通常会被放入相应的自动释放池。在应用执行完某个@autoreleasepool
中的程序段后,该自动释放池中的所有对象都会失去一个拥有者,代码如下:
@autoreleasepool {
// 从 someitem 方法得到一个 BNRItem 对象,该方法的方法名没有包含 alloc 或 copy
BNRItem *item = [BNRItem someitem];
}// 自动释放池被销毁,item 变量所指向的对象会被释放。
iOS 应用会自动创建一个默认的自动释放池,所以开发者无需关心这个问题,但是了解 @autoreleasepool
的作用有益无害。