文章目录
- 前言
- 29.理解引用计数
- 引用计数的工作原理
- 关闭ARC模式
- 引用计数的增减
- 理解一下引用计数存在
- 属性存取方法中的内存管理
- 自动释放池
- 保留环
- 要点
- 30.以ARC简化引用计数
- 使用ARC时必须遵循的方法和命名规则-
- 变量的内存管理语法意义
- ARC如何清理实例变量
- 覆写内存管理的方法
- 要点
- 31.在dealloc方法中只释放引用并解除监听
- dealloc使用场景
- 对象管理着某些资源的时候在dealloc也要调用清理方法
- 注意
- dealloc不要调用属性的存取方法
- 要点
- 32.编写异常安全代码时留意内存管理问题
- @try @catch @finally
- 发生异常时如何管理内存是个值得研究的问题
- 要点
- 33.以弱引用避免保留环
- 保留环导致内存泄漏
- 学会避免保留环的出现
- weak 和unsafe_unretained区别
- 要点
- 34.以”自动释放池块“降低内存峰值
- 内存峰值
- 要点
- 35.用僵尸对象调试内存管理问题
- 僵尸对象开启
- 僵尸对象工作原理
- 要点
- 36.不要使用retainCount
- 被弃用的retainCount
- 要点
- 总结
前言
- 新年从iOS内存管理开始学习。OC是一门面向对象的语言,而面向对象语言里内存管理是一个重要的概念,想用一门语言写出内存使用效率高的代码就得掌握其中的细节,在OC引入ARC模式之后内存管理的事情几乎都是由编译器来决定的,这使我们学习内存管理变得简单许多
29.理解引用计数
- OC语言使用引用计数来管理内存,也就是说每个对象都有个可以递增或者递减的计数器,如果想使某个对象继续存活之需要递增其引用计数,用完之后递减其引用计数即可,计数变为0,就意味着对象没人关注。
- 值得注意的是在ARC的模式下所有和引用计数相关的方法都是无法编译的,接下来也是主要讲述ARC里不能直接调用的方法(切换成MRC即可)
引用计数的工作原理
- 在引用计数架构下,对象有个计数器,用以表示当前有多少个事物想令此对象继续存活下去,俗称引用计数。
-
NSObject
协议声明了下面三个方法用于操作计数器:
-
-
- Retain 递增保留计数
-
-
-
- release 递减保留计数
-
-
-
- autorelease 待稍后清理“自动释放池”时,再递减保留计数。
-
- 查看保留计数的方法叫做retainCount,这个方法不太有用,随后会解释
关闭ARC模式
- 在buliding settings 里的Automatic Reference Counting设置为NO
引用计数的增减
- 对象创建的时候引用计数为1,若是想让对象继续存活则retain,否则release或者autorelease方法,当一个对象的引用计数归零之后,系统会将其占用的内存标记为“可重用”,也就是说其他的对象可以使用这块内存了
理解一下引用计数存在
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2023];
[array addObject:number];
[number release];
[array release];
- 创建数组之后,把number加入其中的时候,系统会为number retain一次,也就是number的引用计数为2,接下来不需要number对象的时候我们释放了它,在这个例子里能知道number的引用计数现在还为1,但是换成别的的时候我们或许不知道numebr对象已经不存在了但是数组仍然引用这number,这样就会造成crash。所以过早的释放对象使程序变得难以调试
- 为了避免在不经意间使用了无效对象,一般relase之后都会清空指针,这样能保证不出现悬空指针
NSMutableArray *array = [[NSMutableArray alloc] init];
NSNumber *number = [[NSNumber alloc] initWithInt:2023];
[array addObject:number];
[number release];
number = nil;
属性存取方法中的内存管理
- 属性存取方法也存在内存管理模式,对象也可以保留别的对象,这一般通过访问属性来实现,而对于实例变量中的属性为strong关系的时候设置的值会保留
@property (nonatomic, strong)NSString *foo;
- (void)setFoo:(id)foo {
[foo retain];
[_foo release];
_foo = foo;
}
- 此方法将保留新值并释放旧值,然后更新实例变量,令其指向新值。顺序很重要,我们不能先释放旧的值,在之前讲过万一脂肪两个值指向同一对象那么有可能在release之后对象就被销毁了,可能存在悬空指针的现象
自动释放池
- OC的引用计数框架里面自动释放池是一项重要的特性,相比于release,自动释放可以防止我们直接把对销毁从而避免了悬空指针的出现
autorelease。
autorelease
通常是在下一次事件循环的时候进行递减引用计数,也就是可以保证此次进行的事件完整的进行下去-
autorelease
的特性通常在方法返回对象的时候使用,当我们不需要令方法调用者手工保留值的时候。
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this:%@", self];
return str;
}
-
- 此时返回的str对象其保留值比期望多1 因为alloc会➕1,然后并没有进行释放,意味着调用者可能背负的操作更多,但又不能在返回前就release str,所以代码可以这样写
- (NSString *)stringValue {
NSString *str = [[NSString alloc] initWithFormat:@"I am this:%@", self];
return [str autorelease];
}
-
- 所以此时就用到了
autorelease
,在其返回后保留一段时间再释放 当返回过去的str对象必然是存活的,也就是autorelease
能延长对象生命周期,使其在跨越方法调用边界之后可以存活一段时间
- 所以此时就用到了
保留环
- 对于引用计数我们通常苦恼的就是互相引用出现保留环的问题。这将导致内存泄漏,因为循环对象中的对象其保留计数不会为0,所以至少存在某个对象引用着它,
- 也就是说在这个循环里所有的对象保留计数都是1.这种情况被认定为孤岛现象,为了避免引用环的出现通常使用weak关键字来修饰某个属性,从而打破保留环,这种还是需要根据实际情况而定
要点
- 引用计数机制通过可以递增递减的计数器来管理内存。对象创建好之后,其保留计数至少为1。若保留计数为正,则对象继续存活。当保留计数降为0时,对象就被销毁了。
- 在对象生命期中,其余对象通过引用来保留或释放此对象。保留与释放操作分别会递增及递减保留计数。
30.以ARC简化引用计数
ARC(Automatic Reference Counting)是指内存管理中对引用采取自动计数的计数
- ARC虽然是系统自动管理了引用计数,但是引用计数还是在实际运行的,我们不需要做保留和释放操作,在ARC调用这些方法的时候并不通过普通的OC消息派发机制,而是直接调用底层的C语言版本,这样做性能更好,因为保留及释放操作需要频繁的执行,所以直接调用底层函数便于节省CPU周期,ARC不是直接调用retain函数,而是去调用和地产对应的objc_retain函数,这也是不能覆写retain等等引用计数函数的原因
-在ARC下不能调用内存管理的方法有 -
retain
-
release
-
autonrelease
-
dealloc
使用ARC时必须遵循的方法和命名规则-
在OC里,若方法名以下列词语开头,则其返回的对象归调用者所有
- alloc
- new
- copy
- mutableCopy
-
- 归调用者所有:调用上述代码的时候要负责释放方法返回的对象。需要抵消调用对象产生的retain操作。
- 若方法名不以以上四个词语开头,则表示其返回的对象不属于调用者所有,返回的对象会自动释放。
变量的内存管理语法意义
- ARC也会处理局部变量和实例变量的内存管理,默认情况喜爱每个变量都是指代对象的强引用,一定要理解的事实例变量的真正意思是什么。对于某些代码来说,语义和手动内存管理存在一定差异。例如在编写setter方法的时候,不用ARC模式是这样写
- (void)setObject:(id)object {
[_object release];
_object = [object retain];
}
- 这样写的问题是新值和实例变量的值是一样的,只有当前对象还在用引用这个值的时候,那么设置方法中的释放操作会令值保留计数变为0,那么就会被系统回收从而产生crash,接下来retain操作则是错误的,使用ARC的时候不会存在这种流失的现象
- (void)setObject:(id)object {
_object = object;
}
- ARC会用一种安全的方式来设置,先保留新值,再释放旧值,最后设置实例变量
-
- 在编写程序的时候我们可以使用下列修饰符来改变局部变量和实例变量的语义
- 在编写程序的时候我们可以使用下列修饰符来改变局部变量和实例变量的语义
- 对于上述修饰符的使用可以在我们想令实例变量的语义和不使用ARC的时候是相同的话,可以如下修饰
@interface ViewController () {
id __weak _weakObject;
id __unsafe_unretained _unsafeUnretainedObject;
}
- 注意上面的写法都不会在设置实例变量的时候保留值。
- 我们经常会给局部变量加上修饰符打破由块所引入的保留环。block在随后会了解到。块会自动保留其所捕获的全部对象,而如果这其中有某个对象又保留了块本身就可能产生保留环,可以使用__weak修饰符打破它
ARC如何清理实例变量
- 刚才说过ARC也负责对实例变量进行内存管理,当使用手动管理内存计数的时候需要如下dealloc方法
- (void)dealloc {
[_foo release];
[super dealloc];
}
- 而在ARC模式下不需要手动dealloc方法(使用通知传值的时候需要使用dealloc方法解除监听),因为ARC会自动清理内存所需要干的事情。
- 对于非OC对象,比如某些malloc分配在栈或者堆上的内存是需要我们去清理的,尽管在ARC模式下我们只是不需要再去调用超类的dealloc方法,其他都是一样的。
覆写内存管理的方法
- 不使用ARC时,可以覆写内存管理方法。比方说,在实现单例类的时候,因为单例不可释放,所以我们经常覆写release方法,将其替换为“空操作”
要点
- 有ARC之后,程序员就无须担心内存管理问题了。使用ARC来编程,可省去类中的许多“样板代码”。
- ARC管理对象生命期的办法基本上就是:在合适的地方插入“保留”及“释放”操作。在ARC环境下,变量的内存管理语义可以通过修饰符指明,而原来则需要手工执行“保留”及“释放”操作。
- 由方法所返回的对象,其内存管理语义总是通过方法名来体现。ARC将此确定为开发者必须遵守的规则。
- ARC只负责管理OC对象的内存。尤其要注意CoreFoundation对象不归ARC管理,开发者必须适时调用CFRetain/CFRelease。
- 自动引用计数的详细部分也需要在这里记录一下,用来复习OC内存管理之引用计数
31.在dealloc方法中只释放引用并解除监听
- ARC模式下系统帮助我们做了许多的自动内存管理,对象在经历了生命周期结束之后最致命会为系统所回收,这个时候就需要执行dealloc方法了,需要注意
-
- dealloc方法仅执行一次,当引用计数降为0的时候会执行,具体什么时候无法保证
-
- 我们决不能自己调用dealloc方法,当调用之后对象就会失效,后续的方法坚决无效!
dealloc使用场景
- 我们能在dealloc里面做的是主要释放对象所拥有的引用,ARC会通过自动生成的.cxx_destruct方法。在dealloc中为你自动添加这些释放的代码,当然对于非OC于心的CoreFundation对象就是需要手动释放
- dealloc方法通常用来释放监听,在平时使用并学习通知传值的时候最常用的就是释放监听,避免后续产生崩溃
-
- 如果是手动管理内存计数的话就还需要添加超类dealloc
[super dealloc];
- 如果是手动管理内存计数的话就还需要添加超类dealloc
对象管理着某些资源的时候在dealloc也要调用清理方法
注意
- 调用dealloc方法的那个线程会执行最终的释放操作,令对象的保留计数为0,某些方法必须在特定的线程调用才行,若是在dealloc里调用了那些方法,则无法保证其当前线程就是那些方法需要的线程,所以在运行期系统已经改动了对象的内部数据结构
dealloc不要调用属性的存取方法
- dealloc不要调用属性的存取方法,有人可能覆写这些方法并于其中做一些无法在回收阶段安全执行的操作,此外属性也可能属于键值观测KVO机制下,有可能造成运行期系统的状态失调发生崩溃
要点
- 在dealloc方法里,应该做的事情就是释放指向其他对象的引用,并取消原来订阅的“键值观测”或“NSNotificationCenter”等通知,不要做其他事情。
- 如果对象持有文件描述符等系统资源,那么应该专门编写一个方法来释放此种资源。这样的类要和其使用者约定:用完资源后必须调用close方法。
- 执行异步任务的方法不应在dealloc里调用;只能在正常状态下执行的那些方法也不应在dealloc里调用,因为此时对象已处于正在回收的状态了。
32.编写异常安全代码时留意内存管理问题
- 书上一上来就抛出@try@ catch等关键词,这里查询了网上资料作辅助
- 这些关键词是在抛出异常的时候出现的
@try @catch @finally
@try
{
代码块1(可能出现异常的语句)
//执行的代码,其中可能有异常。一旦发现异常,则立即跳到catch执行。否则不会执行catch里面的内容
}@catch(Exception e)
{
代码块2(发生异常时进行处理)
//除非try里面执行代码发生了异常,否则这里的代码不会执行
}@finally
{
代码块3(始终要进行处理的语句)
//不管什么情况都会执行,包括try catch 里面用了return ,可以理解为只要执行了try或者catch,就一定会执行 finally
}-
- catch是抓取代码块1中的异常
-
- 代码块2是出异常后的处理
-
- 代码块3是不管出不出异常都会执行,如果代1或代2中有return,代3会在return后执行
-
- 总结:
1、不管有木有出现异常,finally块中代码都会执行;
2、当try和catch中有return时,finally仍然会执行;
3、finally是在return后面的表达式运算后执行的(此时并没有返回运算后的值,而是先把要返回的值保存起来,管finally中的代码怎么样,返回的值都不会改变,任然是之前保存的值),所以函数返回值是在finally执行前确定的;
4、finally中最好不要包含return,否则程序会提前退出,返回值不是try或catch中保存的返回值。
这个异常捕获机制就是哪里用到就写到那里
- 总结:
发生异常时如何管理内存是个值得研究的问题
- 如下代码
- 在
@try
块中,如果先保留了某个对象,然后在释放它之前又抛出了异常,那么,除非catch块能处理此问题,否则对象所占内存就将泄漏。解决办法就是使用@fially块
。无论是否抛出异常其中的代码都会保证运行,并且只运行一次。代码做如下改变即可
- 在平常我还是打开了全局断点,为了方便随时查找错误,如果是手动管理计数,必须捕获异常的同时还要设法保证把代码清理干净。
要点
- 捕获异常时,一定要注意将try块内所创立的对象清理干净。
- 在默认情况下,ARC不生成安全处理异常所需的清理代码。开启编译器标志后,可生成这种代码,不过会导致应用程序变大,而且会降低运行效率。
33.以弱引用避免保留环
- 保留环这个词出现了很多次了,究极原因是对象之间的相互引用造成的
- 最简单的保留环可以由两个对象构成,它们互相引用对方
- 对于如下的代码可以看出引用环的存在
保留环导致内存泄漏
- 保留环会导致内存泄漏,如果只剩一个引用还指向保留环的实例,而现在这个又把引用一处,那么整个环就会泄漏,也就是说无法继续访问其中的任何对象了
学会避免保留环的出现
- 避免保留环的最佳方式就是弱引用。这种引用通常用来表示“非拥有关系“,将属性声明为unsafe_unretained即可,当然注意unsafe_unretained修饰的属性同assgin特质等价
- OC还有一项与ARC相伴的运行期的特性,叫弱引用weak,与unsafe_unretained作用完全相同,在现在的ARC下基本取代了unsafe_unretained关键字。对于weak只要系统回收了属性,属性值自然置nil。
weak 和unsafe_unretained区别
- 一般来讲不拥有某对象就不需要保留它。weak在开发中多用于协议
要点
- 将某些引用设为weak,可避免出现“保留环”。
- weak引用可以自动清空,也可以不自动清空。自动清空是随着ARC而引入的新特性,由运行期系统来实现。在具备自动清空的弱引用上,可以随意读取其数据,因为这种引用不会指向已经回收过的对象。
34.以”自动释放池块“降低内存峰值
- OC对象的生命周期取决于其引用计数,在OC的引用计数里面存在自动释放池。
- 自动释放池的存在使应用程序在执行循环的时候内存峰值会降低。
内存峰值
- 内存峰值是指应用程序在某个特定时间段内的最大内存用量。而自动释放池可以减少这个峰值,因为系统会在块的末尾把某些对象回收掉,而刚才提到的那种临时对象就在回收之列。
- 自动释放池机制就像栈一样,系统创建好自动释放池之后就将其堆入栈中,而清空自动释放池的过程就是把其从栈弹出,在对象上指向自动释放操作,就是将其放入栈顶的池里。
- 但是尽管自动释放池块的开销不太大,但毕竟还是有的貌似有尽量不要建立额外的自动释放池。@autoreleasepool语法还有个好处:每个自动释放池均有其范围,可以避免无意间误用了那些在清空池后以为系统所回收的对象。
要点
- 自动释放池排布在栈中,对象收到autorelease消息后,系统将其放入最顶端的池里。
- 合理运用自动释放池,可降低应用程序的内存峰值。
- @autoreleasepool这种新式写法能创建出更为轻便的自动释放池。
35.用僵尸对象调试内存管理问题
- Cocoa提供了一个名为僵尸对象的功能,启用这项功能之后运行期系统会把所有已经回收的实例对转化为特殊的僵尸对象,而不会真正的回收。僵尸对象虽然不能重新使用,但是当我们给僵尸对象发送消息的时候会抛出异常,还能准确的说明了发送过来的信息是如何
僵尸对象开启
- 当给僵尸对象发送消息之后应用程序会终止并发送如下消息
僵尸对象工作原理
-
在系统即将回收对象的时候如果发现通过环境变量开启了僵尸对象功能那么就会执行一个附加步骤,把对象转化为僵尸对象而不是回收对象
-
僵尸对象的打印效果可以明确指出僵尸对象收到的选择子及其原来所属的类,其中还包含了接受消息的僵尸对象所对应的指针值,这个消息在适当的时候用处很大
要点
- 系统在回收对象时,可以不将其真的回收,而是把它转化为僵尸对象。通过环境变量NSZombieEnable可开启此功能。
- 系统会修改对象的
isa
指针,令其指向特殊的僵尸类,从而使该对象变为僵尸对象。僵尸类能够响应所有的选择子,相应方式为:打印一条包含消息内容及其接收者的消息,然后终止应用程序。
36.不要使用retainCount
被弃用的retainCount
- OC的每一个对象都有一个计数器,并且计数器的值表明还有多少个其他对象想令此对象继续存活。
- NSObject协议存在一个方法可以查询当前的保留计数
ARC forbids implementation of 'retainCount'
// ARC forbids implementation of 'retainCount'
//- (NSUInteger)retainCount {
//
//}
-
- 在ARC模式下该方法已经被弃用了,首要原因还是它返回的保留计数只是某个对象在某个具体时间的引用计数,完全没有特殊的参考意义,现在不仅存在自动释放池,还有ARC模式的自动管理引用计数,那么retainCOunt完全不能反映真实的引用计数。
要点
- 对象的保留计数看似有用,实则不然,因为任何给定时间点上的“绝对保留计数”都无法反映对象生命期的全貌。
- 引入ARC之后,retainCount方法就正式废止了,在ARC下调用该方法会导致编译器报错。
总结
- 内存管理虽然大部分内容讲的是关于MRC情况下的手动管理方法和原理,但其中不乏很多值得学习的思路,在MRC如何更好的管理内存,在ARC下什么该用什么不该用,都是值得学习的地方,内存管理很重要!