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

2018-03-18  本文已影响16人  WestMiss

前言

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

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

正文

1 引用计数器

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

  1. 引用计数器的工作原理
    NSObject协议下声明了一下三个方法用于操作引用计数器:

对象创建出来的时候,其引用计数至少为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

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

上一篇 下一篇

猜你喜欢

热点阅读