内存管理

Wednesday, January 31, 2018

理解引用计数

  • Retain 递增保留计数
  • release 递减保留计数
  • autorelease 待稍后清理『自动释放池』,再递减保留计数

自动释放池

  • 此方法通常会在下一次事件循环时递减,不过也可能执行得更早一些。
  • 此特性在方法中返回对象时特别有用。
- (NSString *)stringValue {
            NSString *str = [[NSString alloc] initWithFormat: @"I am this: %@", self];
            return str;
    }
  • 此时返回的 str 其保留计数要比预想的多1,因为调用 alloc 会保留计数,而又没有对应的释放操作。但是不能在方法内释放 str,否则方法还没返回,该对象就被回收了。这里就可以用『autorelease』,它会稍后释放对象。释放操作会在最外层的自动释放池执行,除非有自己的自动释放池,否则这个时机就是当前线程的下一次事件循环。
 - (NSString *)stringValue {
            NSString *str = [[NSString alloc] initWithFormat: @"I am this: %@", self];
            return [str autorelease];
    }

保留环

  • 保留环将会导致内存泄露,因为循环中的对象引用计数不会降为0。
  • 在垃圾收集环境中,会将这种孤岛情况全部回收走,但是在引用计数框架中,则需 要靠弱引用来解决这个问题。

ARC 简化引用计数

  • 自动引用计数的思路是用静态分析器根据需要,预先加入适当的保留或者释放操作以避免这些问题。
  • 使用 ARC 时引用计数实际还是要执行的,只不过由 ARC 自动为你添加。
  • 在 ARC 下调用内存管理方法是非法的。
  • ARC 调用这些方法时,并不通过普通的 Objective-C 的消息派发机制,而是直接调用其底层的 C 语言版本。

使用 ARC 时必须遵循的方法命名规则

  1. alloc
  2. new
  3. copy
  4. mutableCopy
  • 若方法名以上述词语开头,则返回的对象归调用者所有,意思是调用上述方法的那段代码要负责释放方法所返回的对象。
  • 如果还有其他对象保留了此对象,并对齐调用了 autorelease,那么保留计数的值可能比1大,这也是 retainCount 不准确的原因之一。
+ (EOCPerson *)newPerson {
        EOCPerson *person = [[EOCPerson alloc] init];
        return person;
        //需要调用者执行 release
    }
    
    + (EOCPerson *)somePerson {
        EOCPerson *person = [[EOCPerson alloc] init];
        return person;
        //会调用[person autorelease];
    }
  • ARC 除了自动调用保留和释放方法外,还能够把能够相互抵消掉的 retain,release,autorelease 移除掉。
  • ARC 可以在运行期检测到 autorelease 及其随后的 retain 这一多余的动作。ARC 为了优化代码,在方法中返回自动释放的对象时,不直接调用 autorelease,而是改为调用 objc_autoreleaseReturanValue。此函数会检视返回之后将要执行的代码,如果发现有执行 retain 操作,则设置全局数据结构中的一个标志位,而不执行 autorelease。与之相似的是,若调用方法的代码要保留此对象,则不直接执行 retain,而是执行 objc_retainAutoreleaseReturnValue 函数,此函数检查标志位,若已经标记,则不执行 retain 操作。
//演示 ARC 优化过程
    + (EOCPerson *)personWithName:(NSString *)name {
        EOCPerson *person = [[EOCPerson alloc] init];
        person.name = name;
        objc_autoreleaseReturnValue(perosn);
    }
    
    //使用上述方法
    EOCPerson *tmp = [EOCPerson personWithName: @"Matt Galloway"];
    _myPerson = objc_retainAutoreleasedReturnValue(tmp)
    

变量的内存管理语义

  • ARC 也会处理局部变量和实例变量的内存管理,默认每个变量都是指向对象的强引用。
  • ARC 设置引用计数为了安全,会先保留新值,再释放旧值,最后设置实例变量。
  • 局部变量修饰符:
    1. __strong: 默认语义,保留此值
    2. __unsafe_unretained: 不保留此值,但不安全,等到再次使用可能已被清空。
    3. __weak:不保留此值,可以安全使用,对象如果被回收,变量也会被清空。
    4. __autoreleasing:把对象按引用传递给方法时,使用这个修饰符,此值会在方 法返回时自动释放。

ARC 如何清理实例变量

  • ARC 会在 dealloc 中插入这些代码
  • 非 Objective-C 对象,比如 CoreFoundation 中的对象是由 malloc()分配在堆中的内存,那么仍然需要清理。
 - (void)dealloc {
        CFRelease(_coreFoundationObject);
        free(_heapALLocatedMemoryBlob);
    }
    </code>

在 dealloc 方法中只释放引用并解除监听

  • 如果手动管理引用计数,在 dealloc 方法中记得调用[super dealloc]
  • 系统并不保证每个创建的对象的 dealloc 都会执行,应用程序中止后,资源会释放给系统,如果一定要清理某些对象,可以在 applicationWillTerminate 方法里清理。
  • 如果对象管理着某些资源,那么在 dealloc 中也要调用清理方法,以防开发者忘了清理这些资源。
  • 不建议在 dealloc 里面调用任何方法。
  • 在 dealloc 里也不应该调用属性的存取方法,因为有人可能覆写了这些方法。
  • 不应在 dealloc 里执行异步方法。

以弱引用避免保留环

  • 避免保留环的最佳方式是弱引用

以自动释放池块降低内存峰值

  • 创建自动释放池的语法如下
@autoreleasepool {
    
}
  • 一般无需担心自动释放池创建问题,GCD 中的线程,默认都有自动释放池,每次执行事件循环(event loop)时就会将其清空。
  • 通常只有在 main 函数才需要创建自动释放池
  • 可以通过嵌套自动释放的方式降低内存峰值
    for (int i = 0; i < 100000; i++) {
        [self doSomethingWihtInt: i];
    }
  • 上述代码,如果 doSomething 方法只是创建临时对象,那么对象可能会在自动释放池里。可能调用完方法就不再使用了,等待自动释放池释放。然而自动释放池要等到下一次事件循环才会清空。意味着在执行 for 循环时,内存占用会不断增大。如果循环长度无法预知,这个问题更加明显。此时使用把循环内的代码包裹在@autoreleasepool 里,那么循环中自动释放的对象就会在这个池,而不是线程的池里。
  • 要根据实际情况评估是否要创建自动释放池,因为创建释放池也是有开销的。

用『僵尸对象』调试内存管理问题

  • 启用这项调试功能后,运行期系统会把所有已经回收的实例转化成特殊的『僵尸对象』,而不会真正的回收他们。
  • 设置方法是在环境变量里将 NSZombieEnabled 设为 YES。
  • 系统会修改 isa 指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类可以响应所有的选择子,方式是打印一条包含消息内容及其接收者的消息。然后终止程序。

不要去使用 retainCount

  • ARC 已经不允许用。
  • retainCount 返回的保留计数只是某个给定的时间点上的值,未考虑到系统会稍后把自动释放池清空,因而不会将后续的释放操作从返回值里减去。
  • retainCount 永远不会为0。
  • 单例对象的保留计数绝对不会变。
Objective-CiOS

Block与GCD要点

协议与分类