iOS的内存管理(1) 一些概念点

前言

通过这段时间的学习,对Objective-C的内存管理知识做一个总结。分享给大家,如有理解错误的地方,还望多指正。
总结从以下几个方面来说明:

  1. 引用计数器
  2. ARC(Automatic Reference Counting):自动引用计数
  3. 循环引用问题
  4. 自动释放池autorelease pool

正文

1 引用计数器

1.理解引用计数器
说到引用计数器,有着iOS开发经验的同行一定知道,Objective-C语言内存管理的核心就是引用计数器。
简单来说,每个OC对象都有一个引用计数器,如果想使某个对象继续存在内存中,那就使其引用计数增加1,如果该对象使用结束,我们不希望它继续存在于内存中,那就使其引用计数减少1(,当该对象的引用计数等于0时候,系统收回该对象的内存。

  1. 引用计数器的工作原理
    NSObject协议下声明了一下三个方法用于操作引用计数器:
  • retain 递增引用计数;
  • release 递减引用计数;
  • autorelease 待稍后清理引用释放池时,再递减引用计数。
    查看当前引用计数的方法是retainCount[obj retainCount]

对象创建出来的时候,其引用计数至少为1。若想让它继续存活,则调用retain方法,若该对象不再被使用,则调用release或者autorelease方法。当引用计数为0时候,该对象占用的内存将会被回收。引用计数器的工作原理大概如此。

2 ARC(Automatic Reference Counting):自动引用计数
  1. 自动引用计数(ARC)的理解
    在ARC出现前,开发者使用引用计数需要记住何时使用retainreleaseautorelease,而ARC的诞生就是为了解决这个问题。ARC省去了开发者在代码中调用retainreleaseautorelease精力,取代了开发者内存管理的工作。
  2. ARC的工作原理
    Xcode的Clang编译器带有一个静态分析器,用于检测程序中引用计数有问题的地方。例如:
if (true) {
   id obj = [[SomeClass alloc] init];
   [obj doSometing];
}

这段代码在MRC环境下就会出问题,因为if条件外obj没有被释放,此处会发生内存泄漏。静态分析器做的就是检测这样的错误。
既然静态分析器可以做到这些,那么可以应用这个功能,提前在程序中加入retainrelease等操作。ARC的工作原理,就是使用了这一功能。
在ARC下,代码经过编译后,会自动为源代码添加上相对应的操作。所以在ARC下,retain,release,autorelease都是不允许被使用的,否则会产生编译错误。

3.ARC的一些tips

  • ARC在调用retain,release,autorelease方法的时候,并不走Objctive-C的消息派发机制,而是直接调用底层的C语言方法。这样做提升了性能,也是retain,release,autorelease方法不能被重写的原因。
  • OC语法有非常严格的命名规则,以下列单词开头的方法名:new alloc copy mutableCopy。若方法返回对象,则ARC不会为返回的对象加上autorelease,否则会在返回对象前为其加上autorelease
  • 除了自动的调用retain,release,autorelease之外,ARC还能够把互相抵消的retain,release,autorelease操作简化。若某个对象上重复多次的进行了‘retain’和‘release’操作,那么ARC有时可以成对的抵消这两个操作。
  • ARC也包含运行期组件。当它检测到某方法返回对象前,为其执行了autorelease操作,之后该对象还要执行retain操作,那ARC就会删除这一对操作。具体方式为,在返回对象前,不直接调用autorelease方法,而是调用objc_autoreleaseReturnValue函数,此函数会检测当前方法返回之后即将要执行的代码,若发现那段代码要在返回对象上执行retain操作,则设立一个flag不再执行原有的autorelease操作。同理,若方法返回一个自动释放的对象,而该对象需要被保留,那么不直接执行retain,而是改为执行objc_retainAutoreleasedReturnValue函数,此函数检测刚才设立的那个flag,若已经设置,则不执行retain操作。
    objc_autoreleaseReturnValueobjc_retainAutoreleasedReturnValue两个函数的实现必须通过查看机器码指令才可以判断,所以是由编译器的开发者完成的。
  • 用以下修饰符修饰变量时的一些语义:
    __strong:默认语义,保留此值;
    __unsafe_unretained:不保留此值;
    __weak:不保留此值,但是变量可以安全使用,如果系统回收了该对象,那么这个变量也会被自动清空;
    __autoreleasing:在方法调用时,使用这个修饰参数值,在方法返回后,该值自动释放。
  • ARC环境下,当对象被回收时,实例变量的回收问题:ARC会使用Objctive-C++的一项特性来清理实例变量。回收Objective-C++对象时,待回收对象会调用所有C++对象的析构函数。编译器如果发现某个对象有C++对象,就会生成名为.cxx_destruct的方法。ARC借助此特性,在该方法中生成清理内存所需要的代码。
  • CoreFoundation对象需要手动管理内存,不归ARC管理,开发者必须自己调动CFRetain/CFRelease
3 循环引用问题
  1. 循环引用的理解
    如果A对象强引用了B对象,而B对象也强引用了A对象,这就是最简单的循环引用,两个对象间的互相引用。当系统要回收对象A时,由于A引用了对象B,所以对象B也需要被释放,而此时B又强引用了A,如此一来,两个对象都不能够释放,继续存活于内存中,就会出现内存泄漏。这就是循环引用的问题。
  2. 循环引用问题的解决
    解决循环引用问题的最佳方式就是 弱引用。用unsafe_unretained或者weak修饰属性。在语义上unsafe_unretainedassign等价,区别于assign用于修饰通用类型的属性,比如int,float结构体等,而unsafe_unretained用于修饰对象。
    例如对象A强引用了对象B,那么对象B如果弱引用了对象A,就不会出现以上的问题。当系统回收对象A时,对象B会被回收,而B对A的弱引用不会造成循环引用,所以不会出现内存泄漏的问题。
    weak等价于unsafe_unretained,它们的不同主要表现在被修饰的属性被释放后的行为不同。当用unsafe_unretained修饰的属性被回收后,该属性任然指向那个被回收的属性,而weak则指向nil。使用weak会使程序更加安全一些。
  3. block使用中的循环引用问题
    这个问题在很多的技术文章中被提到过,这里也做个简单的说明。
    例如:
//DemoViewController.m
@interface DemoViewController ()
@property (nonatomic, copy) void (^testBlock) (void);
@end

@implementation DemoViewController
...
- (void)viewDidLoad {
  [super viewDidLoad];
  [self test];
}

- (void)test {
  self.testBlock = ^(){
    [self doSometing];
  }
}
...

@end

以上这段代码,由于testBlock块中捕获了self,所以testBlock强引用了self,而同时self强引用着testBlock,如此就形成了循环引用,有内存泄漏的风险。
打破这种保留的方式很简单,使用__weak定义一个新的weakSelftestBlock捕获。如下:

//DemoViewController.m

@interface DemoViewController ()
@property (nonatomic, copy) void (^testBlock) (void);
@end

@implementation DemoViewController
...
- (void)viewDidLoad {
  [super viewDidLoad];
  [self test];
}

- (void)test {
  __weak typeof(self) weakSelf = self;
  self.testBlock = ^(){
    [weakSelf doSometing];
  }
}
...
@end

关于block的知识点总结,会在后面整理一份。

4 自动释放池autorelease pool
  1. 自动释放池的认识
    在ARC中,自动释放池(autorelease pool)是一项重要的特性。
    当某个对象调用release时,会理解递减引用计数retainCount。如果换做调用autorelease,则对象会被加入autorelease pool中,当清空自动释放池autorelease pool时,会向其中的对象发送release消息。
  2. 自动释放池的使用
    使用自动释放池的语法如下:
//使用语法
@autoreleasepool{
  //...
}

一般情况下,系统创建的主线程或者GCD机制中的线程,都会默认创建自己的自动释放池,每次执行 事件循环 时,就会将其清空。因此,不需要自己来创建 自动释放池块。
应用程序的入口int main()函数处,就为我们手动创建了应用程序的自动释放池。

int main(int argc, char * argv[]) {
    @autoreleasepool {
        return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
    }
}

所以通常情况下我们无需自己创建自动释放池。当某些临时产生的对象导致应用程序的内存峰值过高时,我们可以通过创建自动释放池,来解决这个问题。
例如:

NSMutableArray *objsArray = [NSMutableArray array];
for (int i = 0; i < 10000; i ++) {
  id obj = [self createSomeObjcWithi:i];
  [objsArray addObject:obj];
}

以上代码就会造成程序的内存突然暴增,而等所有obj对象都释放以后,又突然下降。
此时,增加一个自动释放池代码块即可解决这个问题:

NSMutableArray *objsArray = [NSMutableArray array];
for (int i = 0; i < 10000; i ++) {
  @atuoreleasepool {
    id obj = [self createSomeObjcWithi:i];
    [objsArray addObject:obj];
  }
}

这样一来,应用程序在执行循环时,就会有效降低内存峰值,不像原来那么高。
创建自动释放池本身也会占用一定的内存,所以是否使用自动释放池完全取决于程序本身。

关于自动释放池Draveness大神的自动释放池的前世今生 ---- 深入解析 Autoreleasepool有详细的解析。

总结

内存管理是应用程序的灵魂,虽然在ARC环境下,我们可以尽量少的投入精力在内存管理上,但是了解其中的原理和机制,会让我们在程序出问题时找到有效的解决途径,更是提高自我的一种方式。
当然,内存管理涉及到的也不止文中提到的内容,还有很多需要挖掘的地方。
文章是本人看书学习中的总结,主要用于知识巩固,顺便和大家分享交流,如果有不妥的地方,欢迎指正。

你可能感兴趣的:(iOS的内存管理(1) 一些概念点)