风行的博客

iOS 内存管理

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

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


一些概念

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

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

  • 内存泄漏:不再使用的对象内存没有释放掉,将导致内存占用无限增长

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


内存管理规则

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

  • 拥有对象所有权
    • 通过 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
9
10
@property (nonatomic, copy) NSString *str;

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

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

      _userName = [userName retain];
  }
}


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 一样

列出几种有问题的写法

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 开头,它会根据方法以什么开头来决定内存管理方式


Autorelease Pool

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

  • 自动释放池的创建和释放:当主线程的 RunLoop 检测到事件并启动后便会创建自动释放池,当事件处理完,RunLoop 会释放池子并继续休眠。如果是子线程,那么不仅它的 RunLoop 需要我们去启动,还需要我们为它创建一个自动释放池,否则会造成内存池露。 我们自己创建的池子,会在出了 @autoreleasepool 的大括号后进行清理。
1
2
3
4
5
6
7
8
9
// MRC
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
...
[pool release];

// ARC
@autoreleasepool {
  ...
}
  • 当循环次数较大或者事件处理占用内存较大时,会导致内存占用不断增长,这时需要我们自己创建自动释放池
1
2
3
4
5
6
7
for (int i = 0; i < 1000000; i ++) {
  @autoreleasepool {
      NSString *str = [NSString stringWithFormat:@"%d", i];

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