美文网首页
iOS的内存管理(1) 一些概念点

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

作者: WestMiss | 来源:发表于2018-03-18 20:23 被阅读16次

    前言

    通过这段时间的学习,对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) 一些概念点

          本文链接:https://www.haomeiwen.com/subject/xybkqftx.html