iOS 内存管理总结
1. 内存区域分布
堆操作:
操作系统中有一个存放堆内空闲存储块地址和大小的链表,当程序员申请空间的时候,系统就会遍历整个链表,找到第一个比申请空间大的空闲块节点,系统会将该空闲块从空闲链表中删除,分配给程序,由于申请的空间不一定与找到的空闲块大小相同,多出来剩余的空闲区会被系统重新添加到空闲链表中。当我需要删除对象时,便会根据指针纪录的地址,将这一块区域重新加入到链表中
栈操作:
栈区的内存是系统自动申请的而且是有序的。我们在申请栈空间时就只能在栈的顶部进行申请,当程序执行某个方法(或者函数)时,会从内存中栈(stack)的区域分配出一块内存空间,这个内存空间被称之为帧(frame)用来储存程序在这个方法内声明的变量的值。当应用启动并运行 main 函数时,它的帧会被存在栈的底部。当 main 继续调用另外一个方法时,这个方法的帧又会继续被压入栈的顶部。被调用的方法还可以再调用其他方法,以此类推,会有帧继续被压入栈顶,在被调用的方法结束后,程序会将其帧从栈顶释放。
2. iOS 引用计数内存管理策略
引用计数是一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。从而实现资源自动管理的目的。它的做法是:当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,对象的引用计数为0时对象的内存会被立刻释放。
* 当程序调用方法名以alloc、new、copy、mutableCopy开头的方法来创建对象时,该对象的引用计数加1,这种情况我们将拥有所创建的这个对象。 |
3. iOS开发中的内存管理四个黄金法则
* 自己生成的对象,自己持有 |
4. 有关引用计数的方法:
* —retain:将该对象的引用计数器加1,从而持有该对象,但是并不拥有对象的释放权利。 |
5. iOS中的变量标识符 & 属性标识符
变量标识符
__strong 持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放,从另一个角度讲只要还有一个强指针指向某个对象,这个对象就会一直存活 |
属性标识符
@property (assign/retain/strong/weak/unsafe_unretained/copy) PropertyType* propertyType |
概括得讲:
strong 和 copy都会持有对象,一个是持有对象的本身,一个是持有对象的副本。
weak,unsafe_unretained 更像一个旁观者,它们不会对数据的引用计数起到任何的改变,它看着对象被持有,被销毁却无能为力,只不过weak会在对象被销毁的时候会将其置为nil。而unsafe_unretained不会,unsafe_unretained 在开发中用得比较少, 如果对性能有极高的要求方可以考虑使用 unsafe_unretained 替换 weak,因为weak 其实对性能还是有影响的,只不过少量使用的时候是不会察觉到的,但是在类似YYModel这种序列化,反序列化库如果大量使用weak,肯定会对性能有较大的影响,weak的最主要作用就是解决循环引用的问题。这个会在后面做介绍,其实这个已经在Block总结的时候已经介绍过了。
6. ARC规则
与Java 中 GC 不同,ARC 是编译器特性,而不是基于运行时的,ARC 背后的原理是依赖编译器的静态分析能力,通过在编译时找出合理的插入引用计数管理代码,而不是实时监控与回收内存。
需要注意的是ARC 所做的事情并不仅仅局限于在编译期找到合适的位置帮你插入合适的 release 等等这样的内存管理方法,其在运行时期也做了一些优化,比如:
- 合并对称的引用计数操作。比如将 +1/-1/+1/-1 直接置为 0.
- 巧妙地跳过某些情况下 autorelease 机制的调用。
当返回值被返回之后,紧接着就需要被 retain 的时候,没有必要进行 autorelease + retain,直接什么都不要做就好了。
ARC 打开的情况下有如下限制:
* 不能使用retain/release/retainCount/autorelease |
7. 内存相关常见问题
内存问题有两种:
- 释放得太早,还在使用中就释放:
如果某个对象有至少一个拥有者,那么就必须保留不能释放,否则的话其他对象或者方法仍然有指向这个对象的指针沦为野指针(空指针)。这称之为过早释放,这是十分危险的,因为当野指针指向的内存区域再次被某个新的对象使用时,野指针上的操作便会破坏这个新对象造成文件丢失或者崩溃。 |
- 释放得太晚,已经不用了但是还没释放:
如果某个对象失去了拥有者(变成没有拥有者)那么应该将其释放掉,否则没有拥有者的对象会被孤立而程序找不到,并且始终占用着一块内存,导致内存泄漏 |
7.1 内存泄漏
ARC内存泄露常见场景:
- 对象型变量作为C语言结构体,或者联合体(struct、union)的成员
struct Data { |
__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便可能遭遇内存泄露或者程序崩溃。
- 循环引用
循环引用常见有如下几种情况:
- 两个对象互相持有对象,这个可以设置弱引用解决,最常见的是block,但是需要注意并非所有的block都需要使用weak来打破循环引用,如果self没有持有block就不会造成循环引用。而有些地方之所以使用了__weak,是为了在[self dealloc]之后就不再执行了。
解决方案 1:在block外部对弱化self,在block内部强化已经弱化的weakSelf
@interface Test: NSObject { |
解决方法 2: 通过将对象在block中设置为nil,但是这种需要注意的是block一定要被执行
__block TestObject *object = [[TestObject alloc] init…]; |
- NSTimer的target持有self
self.timmer = [NSTimer scheduledTimerWithTimeInterval:1.0 |
NSTimer会造成循环引用,timer会强引用target即self,一般self又会持有timer作为属性,这样就造成了循环引用。
如果timer只作为局部变量,不把timer作为属性同样释放不了,因为在加入runloop的操作中,timer被强引用。而timer作为局部变量,是无法执行invalidate的,所以在timer被invalidate之前,self也就不会被释放。
解决方案:在恰当时机调用[timer invalidate]即可,这个需要根据业务来自己决定,但是放在dealloc中调用是无效的,因为循环引用的情况下dealloc是不会被调用的,所以[timer invalidate]也就不会被调用。
还有下面几种定时相关的情形也需要注意:
__weak __typeof(self) wself = self; |
__weak __typeof(self) wself = self; |
- 代理delegate
代理在一般情况下,需要使用weak修饰,我们常见的delegate 一般会是VC的属性,被VC持有,同时我们会将VC相关的属性作为delegate从而导致循环引用。
解决方案:delegate属性使用weak修饰
- NSNotification
使用block的方式增加notification,引用了self,在删除notification之前,self不会被释放
解决方案:在block内部使用弱引用解决
- 对象被单例持有
我们在单例里面设置一个对象的属性,因为单例是不会释放的,所以单例会有一直持有这个对象的引用。
[Instanse shared].obj = self; |
- CF类型内存
注意以creat,copy作为关键字的函数都是需要释放内存的.
8. 内存泄漏的排查方法
- 静态分析方法(Analyze)
- 动态分析方法(Instrument工具库里的Leaks,Allocations)
- 在可疑对象的dealloc方法中添加log进行查看
- 使用三方开源库:
MLeaksFinder
PLeakSniffer
FBRetainCycleDetector
FBAllocationTracker
FBMemoryProfiler
介绍FBRetainCycleDetector,FBAllocationTracker,FBMemoryProfiler的文章
9. AutoreleasePool 与 RunLoop的关系
主线程的AutoreleasePool会在RunLoop进入的时候重新建立一个,在RunLoop退出休眠状态的时候也会进行释放后重新建立一个。在退出RunLoop的时候释放AutoreleasePool,具体见RunLoop总结
10. weak-strong dance
在7.1 介绍内存泄漏类型时候提到循环引用的一种解决方案是在block外部对弱化self,在block内部强化已经弱化的weakSelf,这也就是这里所说的 weak-strong dance,block外部对弱化self是为了避免循环引用,而在block内部强化已经弱化的weakSelf是为了避免外部_weak导致在运行block的时候self被释放。