彭序猿

耕读传家,学为好人

你好,我是彭序猿,目前在iOS开发的道路上探索,这里会写点关于iOS开发blog,还有一些生活上的琐碎事儿。


我们做技术的,不会耍什么心眼,从来都是你看我顺眼,我看你顺眼,那我们就是好基友~

Effective Objective-C 2.0 总结(五)

内存管理

[TOC]

第 29 条:理解引用计数

引用计数工作原理

  1. Objective-C 语言使用引用计数来管理内存,每个对象都有个可以递增递减的计数器,用以表示当前有多少个事物想令此对象继续存活下去。
  2. NSObject 协议声明下面三个方法用于操作计数器,以递增或递减其值:
    • retain 递增保留计数
    • release 递减保留计数
    • autorelease 待稍后清理 “自动释放池” 时,再递减保留计数
  3. 在调用release 之后,对象所占的内存可能会被回收,这样子在调用对象的方法就可能使程序崩溃,这里 “可能” 的意思是对象所占的内存在 “解除分配” (deallocated)之后,只是放回 “可用内存池”(avaiable pool)。若果执行方法时尚未覆写对象,那么对象仍然有效。
  4. 为避免在不经意间使用无效对象,一般在调用完release 之后都会清空指针,保证不会出现可能指向无效对象的指针,这种指针通常被称为 “悬挂指针”(dangling pointer)。

自动释放池

  1. 调用release 会立刻递减对象的保留计数(这里可能会令系统回收此对象),调用autorelease 方法,是在稍后递减计数,通常是在下一次 “事件循环” 时递减。

  2. 此特性很有用,尤其是在返回对象时更应该用它

   - (NSString *)stringValue {
    NSString *str = [[NSString alloc] 
                initWithFormat:@"I am this %@",self];
    return str;
   }

这里返回的str 对象的保留计数会比期望值多1,因为调用alloc 会令保留计数+1,这里又没有对应的释放操作,这样子就意味着调用者要负责处理这多出来的保留操作。在这个方法又不能释放str,否则还没等方法返回,str 这个对象就被释放了。这里应该用autorelease ,它会在稍后释放对象,保证这里可以保证调用者可以先用这个str 对象。

  1. autorelease 能延长对象声明周期,使其在跨越方法调用边界后依然可以存活一段时间。

保留环

  1. 呈环状相互引用的多个对象,相互持有,这将导致内存泄漏,这里循环中的对象其保留计数不会降为0。
  2. 通常采用 “弱引用” 来解决此问题,或者从外界命令某个对象不再保留另外一个对象来打破保留环,从而避免内存泄漏。
  • 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
  • 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。

第 30 条:以 ARC 简化引用计数

  1. 内存泄漏:没有正确的释放已经不再使用的内存。
  2. 自用引用计数预先加入适当的保留或释放操作来避免内存泄漏,使用ARC 时,引用计数实际上还是要执行的,只是保留与释放操作是由ARC 自动添加的。
  3. ARC 会自动执行retain、release、autorelease、dealloc等操作,所以在ARC 下调用这些内存管理方法是非法的。因为ARC 会分析何处应该自动调用内存管理方法,所以我们再手动调用的话,会干扰其工作。
  4. 实际上,ARC 在调用这些方法时,并不是普通的Objective-C 消息派发机制,而是直接调用其底层的C 语言函数,这样子性能会更好。

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

  1. 将内存管理语义在方法名中表示出来,若方法名以下列词语开头,则返回的对象归调用者所有:
    • alloc
    • new
    • copy
    • mutableCopy
  2. 将内存管理交由编译器和运行期组件来做,可以使代码得到多种优化。

变量的内存管理语义

  1. ARC 也会处理局部变量与实例变量的内存管理。
  2. 我们通常会给局部变量加上修饰符来打破 “块”(block)所引入的 “保留环”(retain cycle)。

ARC 如何清理实例变量

  1. 对实例变量进行内存管理,必须在 “回收分配给对象的内存” 时生成必要的清理代码。凡事具备强引用的变量,都必须释放,ARC 会在dealloc 方法中插入这些代码。
  2. ARC 会借用Objective-C++ 的一项特性来生成清理代码,在回收对象时,待回收对象会调用所有C++ 对象的析构函数,编译器如果发现某个对象里含有C++ 对象,就会生成名为.cxx_desteuct 的方法,ARC 借助此特性,在该方法中生成清理内存所需的代码。
  3. 对于非Objective-C 的对象,然后需要我们手动清理。CFRelease();

覆写内存管理方法

  1. 非ARC 时可以覆写内存管理方法,在ARC 下禁止覆写内存管理方法,会干扰到ARC 分析对象生命周期的工作。
  • 有ARC 之后,程序员就无需担心内存管理问题了。使用ARC 来编程,可省去类中的许多 “样板代码”。
  • ARC 管理对象生命周期的办法基本上是:在适合的地方插入 “保留” 及 “释放” 操作。在ARC 环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行 “保留”及 “释放” 操作。
  • ARC 只负责管理Objective-C 对象的内存。尤其要注意:CoreFoundation 对象不归ARC 管理,开发者必须适时调用CFRetain/CFRelease。

第 31 条:在 dealloc 方法中只释放引用并解除监听

  1. 对象在经历生命周期后,最终会为系统回收,这时候就要执行dealloc 方法。每个对象生命周期内,此方法只会调用一次,也就是保留计数为0 的时候,绝对不能自己调用dealloc 方法,运行期会在适当的时候调用,一旦调用,对象就不再有效了,后续的方法调用均是无效的。
  2. dealloc 方法主要是释放对象所拥有的引用,也就是把Objective-C 对象都释放掉,ARC 会通过自动生成的.cxx_desteuct 方法,在dealloc 中为你自动添加这些释放代码。但是其他非Objective-C 对象就需要自己手动释放了。
  3. dealloc 方法通常还需要把原来配置过的观测行为都清理掉,例如通知等。
  4. 对于开销较大或者系统内稀缺的资源不应该等到dealloc 才清理(文件描述符、套接字、大块内存等),因为dealloc 并不会在特定的时机调用,因为有可能还有别的对象持有它。应该自己实现一个方法,当应用程序用完资源对象后,就调用此方法,这样子对象的生命周期就更加明确了。
  5. 调用dealloc 方法的那个线程会执行 “最终的释放操作”,令对象保留计数为0,而某些方法必须在特定的线程调用,若在dealloc 中调用那么方法,无法保证当前的线程就是那个方法所需的线程。在dealloc 里尽量不要去调用方法,包括属性的存取方法,因为在这些方法可能会被覆写,并在其中做一些无法在回收阶段安全执行的操作。
  • 在dealloc 方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的 “键值观测”(KVO)或NSNotification 等通知,不要做其他事情。
  • 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close 方法。
  • 执行异步任务的方法不应在dealloc 里调用;只有在正常状态下执行的那些方法也不应在dealloc 里调用,因为此时对象已处于回收的状态。

第 32 条:编写 “异常安全代码” 时留意内存管理问题

  1. 纯C 中没有异常,C++与Objective-C 都支持异常,在运行期系统中C++与Objective-C 异常相互兼容,也就是说,从其中一门语言里抛出的异常能用另外一门语言所编写的 “异常处理程序” 来捕获。
  2. Objective-C 错误模型表明,异常只应发生严重错误后抛出,发生异常如何管理内存很重要,在try 块中保留某个对象的,但是在释放它之前抛出异常了,这时候就无法正常释放了,这时候需要借助@finally 块来保证释放对象的代码一定会执行,且只执行一次。
  3. 在ARC 不会自动生成处理异常中的代码,因为这样子需要加入大量的样板代码,以便追踪待清理的对象,从而在抛出异常时将其释放。可以这段代码会严重运行期的性能,还会增加应用程序的大小。
  4. 可以通过-fobjc-arc-exceptions 这个编译编织来开启这个功能,但是这个功能不应该作为生成这种安全处理异常所用的附加代码,应该是让代码处于Objective-C++模式。
  • 捕获异常时,一定要注意将try 块内创建的对象清理干净。
  • 在默认情况下,ARC 不生成安全处理异常所需的清理代码。开启编译标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。

第 33 条:以弱引用避免保留环

  1. 几个对象都已某种方式互相引用,从而形成 “环”,这种情况通常会泄漏内存,因为没有东西引用环中对象,这样子环里的对象互相引用,不会被系统回收。
  2. 避免保留环的最佳方式就是弱引用,来表示 “非拥有关系”,unsafe_unretained、weak 修饰都是可以达到的。unsafe_unretained 表示属性值可能不安全,有可能系统把属性所指的对象回收了,但是这个属性依然指向那块地址,那么再调用它的方法可能会使程序崩溃,用weak 修饰的时候,在所指对象被回收的时候,会将属性的指针置为nil。
  3. 一般来说,如果不拥有某对象,就不要保留它,这条规则对collection 例外,collection 虽然不直接拥有其内容,但是它要代表自己所属的那个对象来保留这些元素。
  • 将某些引用设为weak,可避免出现 “保留环”。
  • weak 引用可以自动清空,也可以不自动清空。自动清空是随着ARC 而引入的新特性,由运行期系统来实现。在具备自动清空功能的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。

第 34 条:以 “自动释放池块” 降低内存峰值

  1. 释放对象有两种方式:
  • 一种是调用release 方法,使其保留计数立即递减
  • 一种是调用autorelease 方法,将对象放入 “自动释放池” 中,自动释放池用于存放那些需要稍后某个时刻释放的对象,清空(drain)自动释放池时,系统会向其中的对象发送release 消息。
  1. 创建自动释放池,系统会自动创建一些线程,这些线程默认都有自动释放池,每次执行 “事件循环”时,都会将其清空。自动释放池于左边花括号创建,并于对应的右花括号自动清空。位于自动释放池范围内的对象,会在末尾处受到release 消息。
   @autoreleasepool {
   //...
   }
  1. 内存峰值:是指应用程序在某个特定时段内的最大内存用量。

  2. 对象有可能会放在自动释放池里面,需要等到线程执行下一次事件循环才会清空,这里会导致应用程序所占内存会持续增加,等到临时对象释放的时候,内存用量又会突然下降。我们现在就想把这个内存峰值给降低下来。

  3. 可以增加一个自动释放池来解决这个问题:这样子对象就会加入到这个释放池,而不是线程的主池中,每次循环都创建和释放这个释放池。

   for (int i = 0;i < 100000;i++){
    @autorelease{
        NSObject *object = [NSObject new];
    }
   }
  1. 自动释放池机制就像 “栈” 一样。系统创建好自动释放池之后,就将其推入栈中,而清空自动释放池,则相当于将其从栈中弹出。在对象上执行自动释放操作,就等于将其放入栈顶的那个池。

  2. 对于是否需要用池来优化效率,这个得考虑清楚来,因为自动释放池的创建还是有一丢丢开销的,所以尽量不要建立额外的自动释放池。

  • 自动释放池排布在栈中,对象收到autorelease 消息后,系统将其放入到最顶端的池里。
  • 合理运用自动释放池,可降低应用程序的内存峰值。
  • @autoreleasepool 这种新式写法能创建出更为轻便的自动释放池。

第 35 条:用 “僵尸对象” 调试内存管理问题

  1. 向已回收的对象发送消息是不安全的,是否崩溃这个是看对象所占的内存有没有为其他内容所覆写。
  2. Cocoa 提供 “僵尸对象”(Zombie Object)这个非常方便的功能,开启后,运行期系统会把已经回收的实例转换成特殊的 “僵尸对象”,而不会真正回收它们。这个对象所在的核心内无法重用,因此不可能遭到覆写,僵尸对象收到消息后,会抛出异常。
  3. 使用:Xcode Scheme 中的Enable Zombie Objects 选项,打开会将NSZombieEnabled 环境变量设成YES。
  4. 系统在即将回收时,会执行一个附加步骤,将对象转换成僵尸对象,而不彻底回收。僵尸类是从名为_NSZombie_ 的模版类复制出来的。_NSZombie_ 类并未实现任何方法,此类没有超类,因此跟NSObject 一样,也是一个 "根类",该类只有一个实例变量,叫做isa,所以发给他的消息都要经过 “完整的消息转发机制” 。
  5. 在完整的消息转发机制中,___forwarding___ 是核心,检查接受消息的对象所属的类名,若是_NSZombie_ ,则表示消息接受者是僵尸对象,需要特殊处理。
  • 系统在回收对象时,可以不将其真的回收,而是把它转化成僵尸对象。通过环境变量NSZombieEnabled 可开启此功能。
  • 系统会修改对象的isa 指针,令其指向特殊的僵尸类,从而使该对象变成僵尸对象。僵尸类能够响应所有的选择子,响应方式为:打印一条包含消息内容及其接受者的消息,然后终止应用程序。

第 36 条:不要使用 retainCount

  1. 每个对象都有一个计数器,其表明还有多少个其他对象想令此对象继续存活。在ARC retainCount 这个方法已经废弃了,但是在非ARC 中也不应该调用这个方法,因为这个保留计数只是返回某个时间点的值,并不会联系上下文给出真正有用的值。
  2. retainCount 可能永远不返回0,因为系统有时候会优化对象的释放行为,在保留计数为1的时候就把它回收了。
  3. 不应该依靠保留计数的具体址来编码。
  • 对象的保留计数看似有用,实则不然,因为任何给定时间点上的 “绝对保留计数”(absolute retain count)都无法反映对象生命期的全貌。
  • 引入ARC 之后,retainCount 方式就正式废止了,在ARC 下调用方法会导致编译器报错。
最近的文章

Effective Objective-C 2.0 总结(七)

系统框架第 47 条:熟悉系统框架框架:将一系列代码封装成动态库,并在其中放入描述其接口的头文件。平时我们第三方框架用的是静态库,因为iOS 应用程序不允许其中包含动态库。Foundation、CoreFoundation 框架平时用的比较多,“无缝桥接” 可以将这两种框架的对象平滑转换。还有很多系统库我们平时都应该尽量去用它们,而不是重新实现它们已经实现的功能:CFNetworkCoreAudioAVFoundationCoreDataCoreText许多系统框架都可以直接使用。其中最重...…

继续阅读
更早的文章

Effective Objective-C 2.0 总结(四)

协议与分类[TOC]第 23 条:通过委托与数据源协议进行对象间通信Objective-C 可以使用 “委托模式”(Delegate pattern)的编程设计模式来实现对象间的通信:定义一套接口,某对象若想接受另一个对象的委托,则需遵从此接口,以便成为其 “委托对象”(delegate)。Objective-C 一般利用 “协议” 机制来实现此模式。定义协议: @protocol EOCNetworkingFetcherDelegate @optional - (void)n...…

继续阅读