【Effective Objective-C】—— 内存管理

【Effective Objective-C】—— 内存管理,第1张

第29条:理解引用计数

OC使用引用计数来管理内存,也就是说,每个对象都有个可以递增或递减的计数器,如果某个对象引用他时就会给其引用计数加1,用完了之后,就递减其计数,直至为0,销毁这个对象。ARC实际上也是一种引用计数机制。

1.引用计数工作原理:

在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去。NSObject协议声明了下面三个方法用于 *** 作计数器:

Retain 递增保留计数release 递减保留计数autorelease 待稍后清理“自动释放池”时,再递减保留计数。

其实还有一种查看保留计数的方法叫做retainCount,但是这个方法不太有用,最好还是不要使用的好,后续有说明。

当一个对象的引用计数归零之后,系统会将其占用的内存标记为“可重用”,也就是说其他的对象可以使用这块内存了。下面是一些图文讲解,可能更好的理解:
并且你如果一直向前回溯的话,你最终会发现一个“根对象”,在Mac OS X应用程序中,此对象就是NSApplication对象,而在iOS应用程序中,则是UIApplication对象,两者都是应用程序启动时创建的单例。

通常我们都使用alloc方法来创建一个对象,给对象一个继续存活下去的意愿,但是使用alloc方法创建的对象不一定其引用计数创建出来就是1,我们只能说明保留计数至少为1。并且我们如果在对象的引用计数为0的情况下,即其“可复用”的情况下使用这个变量的话,程序就会崩溃。但是若是将其放到了“自动释放池”中程序就可能不会崩溃了。所以我们为了避免这种情况的发生,我们通常就将对象释放完后将其置空,这种指针通常称为“悬挂指针”。

2.属性存取方法中的内存管理:

对象也可以保留别的对象,这一般通过访问“属性”来实现,而访问属性时,会用到相关实例变量的获取方法及设置方法。若属性为strong关系,则设置的属性会保留。一般的设置方法为:

- (void)setFoo:(id)foo {
    [foo retain];
    [_foo release];
    _foo = foo;
}

此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。通俗解释就是先将传进来的新值保留住,然后将之前_foo指针指向的旧值释放掉,那么因为旧值引用计数为0了,它就会将其标记可复用,那么再让这个_foo指针指向新值,这样就实现了值的设置。

3.自动释放池:

调用release会立刻递减对象的保留计数,而有时可以不调用它,改为使用autorelease,此方法会在稍后递减计数,通常是在下一次“事件循环”时递减,不过也可能执行得更早些。
例如下面这段代码:

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

此时返回的str对象其保留计数比期望的要多1,因为alloc会令保留计数加1,而又没有与之对应的释放 *** 作,这就会有很大的影响,但是在何处释放就又是问题了,因为其还的返回,返回完后又获取不到这个str,也不能在返回之后释放,所以此时就用到了autorelease,在其返回后保留一段时间再释放。

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

实际上,释放 *** 作会在清空最外层的自动释放池时执行,除非你有自己的自动释放池,否则这个时机指的就是当前线程的下一次事件循环。
通过上述可见,autorelease能延长对象生命期,使其在跨越方法调用边界后依然可以存活一段时间。

4.保留环:

保留环其实就是因为对象之间的互相引用出现的问题,这会导致内存泄漏,因为循环中的对象其保留计数不会降为0。
我们要解决保留环,通常采用“弱引用”来解决此问题,或者从外界命令循环中的某个对象不在保留另一个对象。

5.要点: 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放 *** 作分别会递增及递减保留计数。 第30条:以ARC简化引用计数

通常我们在书写代码的时候就很容易出现没有释放对象而导致的内存泄漏的问题,就比如说一个函数中你创建了一个临时的变量,然而在函数结束的时候没有将其释放掉,那么这块内存就泄漏了。但是使用ARC就可以避免这个问题,因为他会在函数的最后自己添加一个释放 *** 作,但是使用ARC的时候一定要记住,引用计数实际上还是要执行的,只不过保留与释放 *** 作现在是由ARC自动为你添加。

因为ARC会自动执行保留和释放 *** 作,所以在ARC环境下使用retainreleaseautoreleasedealloc方法是非法的。直接调用这四种的任何一种都会产生编译错误,因为ARC要分析何处应该自动调用内存管理方法,所以如果手工调用的话,就会干扰其工作。

实际上,ARC在调用这些方法时,并不通过不同的OC消息派发机制,而是直接调用其底层C语言版本。这样做性能更好,因为保留及释放 *** 作需要频繁执行,所以直接调用底层函数能节省很多CPU周期。

1.使用ARC时必须遵循的方法命名:

若方法名以下列词语开头,则其返回的对象归调用者所有:

allocnewcopymutableCopy

意思就是调用这四种方法的那段代码要负责释放方法所返回的对象。也就是说,这些对象的保留计数是正值,而调用了这四种方法的那段代码要将其中一次保留 *** 作抵消。若方法名不以上述四个词语开头,则表示其所返回的对象并不归调用者所有。这种情况下,返回的对象会自动释放,也就是使用了autorelease

ARC除了自动调用“保留”和“释放”方法外,其也可以优化 *** 作,比如两个在一起的保留和释放,它就会将这一对直接移除,不执行这两个代码。

ARC环境下编译代码时,必须考虑“向后兼容性”,以兼容那些不使用ARC的代码,其实ARC的简化 *** 作是因为其调用的特殊函数,它会把autorelease方法改为调用objc_autoreleaseReturnValue函数,把retain方法改为objc_retainAutoreleaseReturnValue函数。
objc_autoreleaseReturnValue函数究竟如何检测方法调用者是否会立刻保留对象呢?这要根据处理器来定。

2.变量的内存管理语义:

ARC也会处理局部变量与实例变量的内存管理。通常情况下,每个变量都是指向对象的强引用。

就用set方法来说,不使用ARC而自己设置set方法就需要这样写:

- (void)setObject:(id)object {
    [_object release];
    _object = [object retain];
}

但是这样写会出现问题,如果新值和实例变量已有的值相同了,它再执行release就会将其释放其保留计数若降为0,后来再进行retain保留 *** 作,程序就会报错,而使用ARC仅仅需要这样就够了:

- (void)setObject:(id)object {
    _object = object;
}

ARC会用一种安全的方式来设置:先保留新值,再释放旧值,最后设置实例变量。

在应用程序中,可用下列修饰符来改变局部变量与实例变量的语义:


我们通常会给局部变量加上修饰符,用以打破由“块”所引入的“保留环”。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身,那么就可能导致“保留环”。

3.ARC如何清理实例变量:

要管理其内存,ARC就必须在“回收分配给对象的内存”是生产必要的清理代码。ARC环境下,dealloc方法可以这样来写:
因为ARC会自动生成回收对象时所执行的代码,所以通常无需再编写dealloc方法。这能减少项目源代码的大小,而且可以省去其中一些样板代码。

4.覆写内存管理方法:

不使用ARC时,可以覆写内存管理方法。比方说,在实现单例类的时候,因为单例不可释放,所以我们经常覆写release方法,将其替换为“空 *** 作”。切忌,在ARC环境下千万不可以!!!

5.要点: 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的许多“样板代码”。ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放” *** 作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放” *** 作。由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC将此确定为开发者必须遵守的规则。ARC只负责管理OC对象的内存。尤其要注意CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。 第31条:在dealloc方法中只释放引用并解除监听

对象在经历其生命周期后,最终会为系统所回收,这时就要执行dealloc方法了。但是我们千万不要自己随便调用dealloc方法,除了当你接触监听和通知的时候,因为你一旦调用dealloc之后,对象就不再有效了,后续方法调用均是无效的。

那么应该在dealloc方法中做些什么呢?
主要就是释放对象所拥有的引用,也就是把所有OC对象都释放掉,ARC会通过自动生成的.cxx_destruct方法在dealloc中为你自动添加这些释放代码。

但是自己添加的检测事件还有通知都需要自己手动来清除:

如果手动管理引用计数而不使用ARC的话,那么最后还需调用“[super dealloc]”。

在开销较大或者系统内稀缺的资源dealloc可不会释放引用,所以我们通常的做法就是:实现另外一个方法,当应用程序用完资源对象后,就调用此方法。 其实使用清理方法而非dealloc方法还有一个原因:就是系统并不保证每个创建出来的对象dealloc都会执行。


如果对象管理着某些资源,那么在dealloc中也要调用“清理方法”,以防开发者忘了清理这些资源。

并且,编写dealloc方法时还需注意,不要在里边随便调用其他方法。因为这会导致很多问题,且经常使应用程序崩溃,因为那些任务执行完毕后,要回调此对象,告诉对象任务已完成,而此时如果对象已摧毁,那么回调 *** 作就会出错。

dealloc里也不要调用属性的存取方法,因为有人可能会覆写这些方法,并于其中做一些无法在回收阶段安全执行的 *** 作。

要点: 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”或“NSNotificationCenter”等通知,不要做其他事情。如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法。执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了。 第32条:编写“异常安全代码”时留意内存管理问题

OC中的错误模型表明,异常只应在发生严重错误后抛出,虽说如此,不过有时仍然需要编写代码来捕获并处理异常。

在说明之前先的了解三个概念:@try 代码块存放可能出现异常的代码,@catch 代码块 异常处理逻辑,@finally 代码块回收资源。

try块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非catch块能处理此问题,否则对象所占内存就将泄漏。就比如下面这个:

如果程序在doSomethingThatMayThrow中出错误了,导致程序抛出异常,那么没有执行释放指令,这块内存就泄漏了,所以可以这样改:

所以说,如果手动管理引用计数,而且必须捕获异常,那么要设法保证所编代码能把对象正确清理干净。若使用ARC且必须捕获异常,则需打开编译器的-fobjc-arc-exceptions标志。

要点: 捕获异常时,一定要注意将try块内所创立的对象清理干净。在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。 第33条:以弱引用避免保留环

对象图里经常会出现一种情况,就是几个对象都以某种方式互相引用,从而形成“环”。就像这样:

会导致两个类的相互引用:
这样的保留环就会导致内存泄漏,避免保留环的最佳方式就是弱引用。这种引用经常用来表示“非拥有关系”。我们只需将属性声明为unsafe_unretained即可。
这样就不会生成保留环了,但是unsafe_unretained它可能会导致属性值不安全,而且不归此实例所拥有。但是OC中还有一个属性修饰符它就避免了这种情况,那就是weak

1.unsafe_unretained和weak的区别:


使用weak而非unsafe_unretained引用可以令代码更安全。应用程序也许会显示出错误的数据,但不会直接崩溃。这么做显然比令终端用户直接看到程序退出要好。不过无论如何,只要在所指对象已经彻底销毁后还继续使用弱引用,那就已然是个bug。

一般来说,如果不拥有某对象,那就不要保留它。这条规则对collection例外。

2.要点: 将某些引用设为weak,可避免出现“保留环”。weak引用可以自动清空,也可以不自动清空。自动清空是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。 第34条:以“自动释放池块”降低内存峰值

OC对象的生命期取决于其引用计数。释放对象有两种方式:一种是调用release方法,使其保留计数立即递减;另一种是调用autorelease方法,将其加入“自动释放池”中。

位于自动释放池范围的对象,将在此范围末尾处收到release消息,并且自动释放池可嵌套。将自动释放池嵌套的好处是:可以借此控制应用程序的内存峰值,使其不至过高。

用一段代码来说明:

@autoreleasepool {
    for (int i = 0; i < 10000; i++) {
        [self doSomethingWithInt:i];
    }
}

这段代码中的函数可能会创建很多的临时变量,但是你并没有及时的把它释放掉,它就会一直堆积占用你的内存,直到这个循环结束,但是你如果在循环其中再加入一个自动释放池就不会出现这种情况了。

@autoreleasepool {
    for (int i = 0; i < 10000; i++) {
        @autoreleasepool {
            [self doSomethingWithInt:i];
        }
    }
}

内存峰值是指应用程序在某个特定时段内的最大内存用量。

自动释放池机制就像“栈”一样。系统创建好自动释放池之后就将其推入栈中,而清空自动释放池,则相当于将其从栈中d出。在对象上执行自动释放 *** 作,就等于将其放入栈顶的那个池里。

但是尽管自动释放池块的开销不太大,但毕竟还是有的貌似有尽量不要建立额外的自动释放池。@autoreleasepool语法还有个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后以为系统所回收的对象。

要点: 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。合理运用自动释放池,可降低应用程序的内存峰值。@autoreleasepool这种新式写法能创建出更为轻便的自动释放池。 第35条:用“僵尸对象”调试内存管理问题

Cocoa提供的“僵尸对象”这个功能对程序员来说实在是太便捷了,启用这个功能之后,运行期系统会把所有已经回收的实例转化成特殊的“僵尸对象”,而不会真正回收他们。这种对象所在的核心内存无法重用,因此不可能遭到覆写。僵尸对象收到消息后,会抛出异常,其中准确说明了发送过来的消息,并描述了回收之前的那个对象。僵尸对象是调试内存管理问题的最佳方式。

NSZombieEnabled环境变量设为YES,即可开启此功能。

给僵尸对象发送消息后,控制台会打印消息,而应用程序则会终止。打印出来的消息就像这样:
也可以在Xcode中打开:

僵尸对象的工作原理是什么呢?
系统在即将回收对象时,如果发现通过环境变量启用了僵尸对象功能,那么还将执行一个附加步骤。这一步就是把对象转化为僵尸对象,而不彻底回收。

那么这个僵尸类是怎么产生的呢?
他其实是在运行期生成的,当首次碰到一个类的对象要变成僵尸对象时,它就会创建这么一个类。僵尸类是从名为_NSZombie_的模版里复制出来的,这些僵尸类没有多少事情可做,只是一个标记,又因为它将原类的方法都拷贝了,所以它会响应原类的方法,不过它会报错提醒程序员。

要点: 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnable可开启此功能。系统会修改对象的isa指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,相应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。 第36条:不要使用retainCount

NSObject协议中定义了下列方法,用于查询对象当前的保留计数:

- (NSUInteger)retainCount;

但是我们不要使用!!!因为其所返回的保留计数只是某个给定时间点上的值,而且就算我们知道某对象的保留计数又有什么用,不如把代码做好,保留几次就记得释放几次岂不是更好。
所以,没事就别用这个方法,对我们来说没有用!!!还会让你更头大!!!

要点: 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”都无法反映对象生命期的全貌。引入ARC之后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/web/996868.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-05-21
下一篇 2022-05-21

发表评论

登录后才能评论

评论列表(0条)

保存