风行的博客

iOS 内存管理

内存管理 - 指程序在运行时申请内存,并在使用完后释放内存的过程

内存管理不当造成的主要问题便是内存泄漏和过度释放,虽然 ARC 使我们可以不去关注内存管理上的一些细节问题,但掌握一些相关知识还是很有必要的。


一些概念

  • MRC:manual reference counting,自己编写内存管理代码(retain、release、autorelease…)

  • ARC:automatic reference counting,编译器会在编译阶段为代码加上优化过的内存管理代码,这样就可以让我们不必花费大量时间在内存管理上面,可以将更多的精力放在业务代码上。

  • 内存泄漏:不再使用的对象内存没有释放掉,将导致内存占用无限增长,即便是使用 ARC,也会因为循环引用问题而造成内存泄露,并且还要注意与 CoreFoundation 对象进行桥接时要手动释放内存。

  • 内存过度释放:释放了仍在使用中的对象,将导致应用崩溃


内存管理规则

内存管理是建立在对象的拥有关系上的,当拥有对象后就要负责释放它,并且不要释放非自己持有的对象,具体规则如下:

  • 拥有对象所有权
    • 通过 alloc/new/copy/mutableCopy 创建对象
    • 在某些场景里避免一个对象被移除,可以对它进行 retain
1
2
3
Student * stu1 = [[Student alloc] init];

Student * stu2 = [stu1 retain];
  • 放弃对象拥有权
    • 立即释放:给对象发送一个 release 消息
    • 延迟释放:给对象发送一个 autorelease 消息
1
2
3
4
5
+ (Student *)studentWithName:(NSString *)name {
    Student *stu = [[Student alloc] initWithName:name];

    return [stu autorelease];
}
  • 实现 dealloc 方法来释放对象自身内存与它所持有的资源,此方法由系统在该对象被销毁时自动调用
1
2
3
4
5
6
- (void)dealloc {
  [_firstName release];
  [_lastName release];

  [super dealloc]; // 必须先释放自己占有的资源再通过此行代码释放自己
}


ARC 带来的变化

  • 不能够自己调用 retain/release/autorelease,由编译器自动插入
  • dealloc 方法中不能调用 [super dealloc] ,由系统去调用并释放实例变量和 assocate 对象,weak 对象也是在这时被设置为 nil,我们只需要释放一些资源,如通知、KVO 等


引用计数

内存管理规则中的对象所有权是通过引用计数来实现,除了常量以外,每个对象都有一个引用计数。

  • 创建对象时,计数为 1
  • 给对象发送 retain 消息时,计数加 1
  • 给对象发送 release 消息时,计数减 1
  • 给对象发送 autorelease 消息时,计数在当前自动释放池代码块结束时减 1
  • 当对象的计数为 0 时将被销毁


属性修饰符

MRC 中包括 assign/copy/retain

  • assign:表示在 setter 中仅是简单的赋值,不改变引用计数,一般用来修饰基本类型和 delegate 属性
1
2
3
4
5
6
7
8
@property (nonatomic, assign) NSInteger count;

@property (nonatomic, assign) id delegate; // 避免引用循环,但要在适当时候设置为 nil

// 对应 setter 方法
- (void)setCount:(NSInteger)count {
  _count = count;
}
  • copy:表示在 setter 中将参数进行内存 copy 后再进行赋值,一般用于不可变字符串、字典、Block
1
2
3
4
5
6
7
8
@property (nonatomic, copy) NSString *userName;

// 对应 setter 方法
- (void)setUserName:(NSString *)userName {
  id tempName = [userName copy];
  [_userName release];
  _userName = tempName;
}
  • retain:表示在 setter 中将参数对象 retain 后再进行赋值,一般用于可变字符串、可变字典及其他对象
1
2
3
4
5
6
7
8
@property (nonatomic, retain) NSMutableString *userName;

// 对应 setter 方法
- (void)setUserName:(NSString *)userName {
  [userName retain];
  [_userName release];
  _userName = userName;
}


ARC 中包括 assign/weak/unsafe_unretained/copy/strong

  • assign:同 MRC 中的 assign 一样,只是不再用来修饰 delegate 对象
  • weak:可以避免循环引用,用来修饰对象,但在 setter 中是简单赋值,不改变引用计数,和 assign 的区别在于属性被销毁后会被设置为 nil,所以能在继续使用该属性时避免程序崩溃,一般用来修饰 delegate 对象和 IBOutlet 对象。
  • unsafe_unretained:和 weak 相似,区别在于被销毁时不会置为 nil (unsafe),它主要是为了兼容 4.0 系统而存在(iOS4 以及之前没有 weak),由于 weak 会对性能有一点影响,因此对性能要求很高的地方可以考虑使用 unsafe_unretained 替换 weak
  • copy:同 MRC 中的 copy 一样
  • strong:同 MRC 中的 retain 一样

runtime 是如何将 weak 对象设置为 nil?

weak 对象会被放入到一个 hash 表中,并用它指向的对象内存地址作为 key,所有指向它的 weak 指针以数组的形式作为 value,当此对象的 dealloc 方法被调用时,会用这个 key 将指向它的 weak 指针数组找出来,并将它们置为 nil,最后再从 weak hash 表中删除这条数据。

列出几种有问题的写法

1
2
3
@property (nonatomic, strong) NSString *str;

// 当源字符串是 NSMutableString 类型时,strong 是浅拷贝,copy 才是深拷贝,所以 str 会随着源字符串的修改而变化
1
2
3
@property (nonatomic, copy) NSMutableString *str;

// 当源字符串是 NSString 类型时,不管用 strong 还是 copy 都是浅拷贝,所以这里 str 指向的仍然是 NSString 对象,当用 str 调用 NSMutableString 类的 insert 等方法时会报错"找不到该方法"
1
2
3
@property (nonatomic, assign) id delegate;

// MRC 下需要自己在 dealloc 中将 delegate 设置为 nil, ARC 下需要用 weak 修饰 delegate 属性
1
2
3
@property (nonatomic, copy) NSString *newString;

// newString属性对应的 getter 也叫 newString,ARC下编译器不允许方法名以 alloc/init/new/copy/mutableCopy 开头,它会根据方法以什么开头来决定内存管理方式


AutoreleasePool

当不再使用一个对象时应该将其释放,但是在某些情况下,我们很难理清一个对象什么时候不再使用,Objc 提供的自动释放池可以解决这个问题,只需要给这种对象发送 autorelease 消息,就会将该对象放到池子里,当池子被清理时,会给池里所有的对象发送 release 消息。

  • 每个 Runloop 在迭代时都会创建自动释放池,并在迭代后释放池子。如果是我们自己创建的池子,会在出了 @autoreleasepool 的大括号后进行清理。通常我们不用自己去创建池子,但是遇到循环次数较大时会导致内存占用不断增长,这时需要我们自己创建自动释放池。
1
2
3
4
5
6
7
for (int i = 0; i < 1000000; i ++) {
  @autoreleasepool {
      NSString *str = [NSString stringWithFormat:@"%d", i];

      NSLog(@"%@", str);
  }
}

PS: NSArray 的 enumerateObjectUsingBlock…. 中也有一个 AutoreleasePool

  • 每个线程在维护自己的自动释放池时都会有一个或多个 AutoreleasePoolPage 对象,每个 Page 对象会开辟4096字节内存(虚拟内存一页的大小),它们之间以双向链表的形式组合而成,Page 对象通过 next 指针实现用栈的结构形式存储 autorelease 对象,next 指针会被初始化在栈底,当有 autorelease 对象入栈时,next 便会指向下一地址,当 Page 空间被占满便指向栈顶,这时如果再添加 autorelease 对象,便会交给新建的 Page 对象存储,并连接链表。

  • 每添加一个 @autoreleasepool { … } 相当于实现下面代码

1
2
3
4
5
6
7
// 会在现有 AutoreleasePoolPage 对象中添加一个哨兵对象(nil)用来标记位置,主要用于在释放池子时标记哪些 autorelease 对象需要释放
void *context = objc_autoreleasePoolPush();

...

// 给晚于哨兵对象后加入的所有 autorelease 对象发送 release 消息,并修改 next 指针,可以跨 Page(所以得用双向链表组合 Page 对象)
objc_autoreleasePoolPop(context);

PS:在添加 autorelease 对象时,如果发现线程没有 AutoreleasePoolPage 则会创建新的,所以不用担心子线程中没开启 Runloop 导致的内存泄露问题。

AutoreleasePoolPage 结构如下

1
2
3
4
5
6
7
8
9
class AutoreleasePoolPage {
    magic_t const magic;
    id *next; // 指向栈顶最新进来的 autorelease 对象的下一个位置
    pthread_t const thread; // 当前线程
    AutoreleasePoolPage * const parent; // 上一个 Page
    AutoreleasePoolPage *child; // 下一个 Page
    uint32_t const depth;
    uint32_t hiwat;
};

通过下面这张图可以更好的理解这些细节