iOS内存管理之开发-实战篇

上一篇文章我介绍了iOS内存管理的底层技术原理。平时不需要我们程序员来过多的关注,尤其进入ARC时代,我们程序员对于内存要做的事变得很简单,但是在我们开发过程中依然可能会出现内存泄漏。

那我们实际开发过程中会遇到哪些内存方面的问题呢?

下面我从两个方面给大家剖析下:

第一个是我们经常用到的property的修饰符。

第二个是我们常提到的循环引用。

property修饰符

修饰符分为三大类:

1、读写性修饰符:readwrite、readonly

2、内存相关修饰符: assign、weak、strong、copy以及目前已经不再使用的retain、release、unsafe_unretained

3、线程安全相关修饰符: atomic、nonatomic

我们只看内存相关的,retain、release、unsafe_unretained在MRC中使用的,我们不需要关注。那看下剩下的assign、weak、strong、copy

assign: 既可以修饰对象,也可以修饰基本类型。而在实际开发中我们只会修饰基本数据类型,为什么呢?因为assign修饰对象会产生悬空指针的问题,修饰的对象释放后,指针不会自动置成nil,此时再向对象发消息程序会崩溃

weak: 只能修饰对象,经常用来修饰delegate,如果修饰基本数据类型会报错:Property with ‘weak’ attribute must be of object type. weak修饰的对象释放后(引用计数变为0),指针会被自动置为nil,之后再向该对象发消息也不会崩溃(向nil发送任何消息都不会崩溃)。weak适用于delegate和block等引用类型,不会导致悬空指针问题,也不会循环引用,非常安全。

strong用来修饰对象,强引用对象,会改变对象 的引用数。

copy 用来修饰NSString,NSArray,NSDictionary。

了解了基础后,我们来看下下面几个问题

为什么assign修饰对象会造成悬空指针而weak不会?

NSString、NSArray,NSDictionary为什么用copy修饰不用strong

NSMutableString、NSMutableArray、NSMutableleDictionary用copy还是strong?

那我们先看下第一个问题,这个就要从我们内存管理上来看,一个对象在释放的时候会遍历弱引用表,来释放所有的弱引用指针,在上边讲解内存管理的时候我们讲到了weak指针会被存到弱引用表,所以weak的指针会在对象释放的时候被释放。而对于用assgin来修饰的指针不会被记录,所以不会再对象释放的时候去释放这个指针就会造成悬空指针。

第二个问题因为NSString、NSArray,NSDictionary有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。

如果我们使用是strong,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性。

copy此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。当属性类型为NSString时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个NSMutableString类的实例。这个类是NSString的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。

第三个问题NSMutableString、NSMutableArray、NSMutableleDictionary我们要用strong来修饰,因为如果用copy修饰的话,copy出来的对象将会是NSString、NSArray、NSDictionary。这样的话我们如果对其进行可变的操作如增删改,则会引起程序的崩溃。

循环引用的常见场景及解决方案

1、block使用不当引起循环引用

2、NSTimer使用不当引起循环引用

3、Delegate修饰符用的不对引起的循环引用

block使用不当引起循环引用

我们看下下面的代码:

success block会持有其函数内所有的变量, 比如self.

可怕的点在于,由于arc下强持有的变量无法被释放,而request方法为对象方法,也就是其被self所持有, 所以引用的过程也就是: self -> request方法 -> success block -> self。

形成了一个强引用的持有链条,当退出页面后,dealloc方法不会被调用.这一组内存就被占用了,所以有些App如果做的很差,用户就会发现自己越用越卡,经常内存溢出甚至导致设备重启,罪魁祸首就是内存泄露过多,当App占用过高的内存,直接被系统kill,亦称闪退。

那我们要怎么解决呢? 看下边的代码:

在这段代码中,我们使用了弱引用,这样我们打破了循环引用的圈,解决了block引起的循环引用。

NSTimer引起的循环引用,看下边代码:

在该代码中,self强持有timer对象,创建timer时self被作为target强持有,造成了循环引入如下图:

那我们首先想到的方法和block一样,我们将用__weak来修饰self是不是就可以了呢?

事实上这样是无效的,无论我们用strong还是weak修改在NSTimer中都会生成一个强引用指针来指向self。

那这样的循环引用我们应该怎么办呢?依然是打断循环链

第一种方案:我们自定义一个TimerWeak对象,然后利用Category的方法会覆盖宿主方法来替换创建NSTimer的主要方法。看下边的代码:

是不是完美的断掉了NSTimer的循环引用,又不会给我们平时的工作带来更多的工作量呢?

在iOS10.0之后,苹果为我们提供了下面的api,使用block的方式来代替selector,如果App只需要支持到10.0之后的用户,那我们NSTimer使用时只需要考虑block引起的循环引用问题。无需做上边的处理。

Delegate修饰符使用错误会引起循环引用。

我们声明delegate的时候都会weak来修饰,其实也是为了解决循环引用。这个大家应该都很熟悉了就不多做介绍了。

参考链接: 

https://www.jianshu.com/p/015132faf9ee

https://segmentfault.com/a/1190000019975967

https://www.jianshu.com/p/a4892d1931f3

参考书籍:Objective-C高级编程:iOS与OS X多线程和内存管理