文章目錄
  1. 1. 1. 内存区域分布
  2. 2. 2. iOS 引用计数内存管理策略
  3. 3. 3. iOS开发中的内存管理四个黄金法则
  4. 4. 4. 有关引用计数的方法:
  5. 5. 5. iOS中的变量标识符 & 属性标识符
  6. 6. 6. ARC规则
  7. 7. 7. 内存相关常见问题
    1. 7.1. 7.1 内存泄漏
  8. 8. 8. 内存泄漏的排查方法
  9. 9. 9. AutoreleasePool 与 RunLoop的关系
  10. 10. 10. weak-strong dance
1. 内存区域分布

堆操作:
操作系统中有一个存放堆内空闲存储块地址和大小的链表,当程序员申请空间的时候,系统就会遍历整个链表,找到第一个比申请空间大的空闲块节点,系统会将该空闲块从空闲链表中删除,分配给程序,由于申请的空间不一定与找到的空闲块大小相同,多出来剩余的空闲区会被系统重新添加到空闲链表中。当我需要删除对象时,便会根据指针纪录的地址,将这一块区域重新加入到链表中

栈操作:
栈区的内存是系统自动申请的而且是有序的。我们在申请栈空间时就只能在栈的顶部进行申请,当程序执行某个方法(或者函数)时,会从内存中栈(stack)的区域分配出一块内存空间,这个内存空间被称之为帧(frame)用来储存程序在这个方法内声明的变量的值。当应用启动并运行 main 函数时,它的帧会被存在栈的底部。当 main 继续调用另外一个方法时,这个方法的帧又会继续被压入栈的顶部。被调用的方法还可以再调用其他方法,以此类推,会有帧继续被压入栈顶,在被调用的方法结束后,程序会将其帧从栈顶释放。

2. iOS 引用计数内存管理策略

引用计数是一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。从而实现资源自动管理的目的。它的做法是:当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,对象的引用计数为0时对象的内存会被立刻释放。

* 当程序调用方法名以alloc、new、copy、mutableCopy开头的方法来创建对象时,该对象的引用计数加1,这种情况我们将拥有所创建的这个对象。
* 当有一个新的指针指向这个对象时(或者调用retain方法时),我们将其引用计数加 1,接收到此调用的对象通常保证在他接收到retain所在的方法中保持有效。
* 除了以alloc、new、copy、mutableCopy 开头的方式创建对象外,其他方式创建的对象都是会被添加到AutoReleasePool,该对象的引用计数不会+1,这种情况下我们不用负责释放对象。
* 当某个指针不再指向这个对象时(或者调用release方法时),我们将其引用计数减 1
* 当对象的引用计数变为 0 时,说明这个对象不再被任何指针指向了,这个时候我们就可以将对象销毁,回收内存。这时候会调用该对象的dealloc方法。

* 对于数组类型其引用计数是会自动的相应变化的:
1. 当一个对象被添加进数组时 ,对象的引用计数也会相应的增加。
2. 数组移除指定的对象或者时所有对象,其被移除的对象会 release
3. 当数组销毁时,所有对象均会 release。
3. iOS开发中的内存管理四个黄金法则
* 自己生成的对象,自己持有
* 非自己生成的对象,自己也能持有
* 不再需要自己持有的对象的时候,释放
* 非自己持有的对象无法释放
4. 有关引用计数的方法:
* —retain:将该对象的引用计数器加1,从而持有该对象,但是并不拥有对象的释放权利。
* —release:将该对象的引用计数器减1,注意只有在计数为0的时候才会释放,而不是说一旦release就释放。
* —autorelease:调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,引用计数减1。
* —retainCount:返回该对象的引用计数的值。
* dealloc: 当一个对象一个拥有者都没有的话,dealloc就会被自动调用,dealloc方法的角色是释放对象自己的内存,并且销毁他所拥有的资源,包括所有对象变量的拥有权。
5. iOS中的变量标识符 & 属性标识符

变量标识符

__strong                持有强引用的变量在超出其作用域时被废弃,随着强引用的失效,引用的对象会随之释放,从另一个角度讲只要还有一个强指针指向某个对象,这个对象就会一直存活
__weak 弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生,如果对象没有被其他对象强引用,弱引用会被置为 nil,弱引用的实现原理是这样:
系统对于每一个有弱引用的对象,都维护一个表来记录它所有的弱引用的指针地址。这样,当一个对象的引用计数为 0 时,系统就通过这张表,找到所有的弱引用指针,
继而把它们都置成 nil
__unsafe_unretained 它和__weak有点类似,只不过在没有被其他对象强引用的时候它不会被置为 nil。如果它引用的对象被回收掉了,该指针就变成了野指针。
__unsafe_unretained修饰符的变量不属于编译器的内存管理对象,赋值时即不获得强引用也不获得弱引用。

__autoreleasing 替代autorelease方法

属性标识符

@property (assign/retain/strong/weak/unsafe_unretained/copy) PropertyType* propertyType

* assign 表明 setter 仅仅是一个简单的赋值操作,没有持有不持有这一说,通常用于基本的数值类型
* strong 表明属性定义一个持有者关系。当给属性设定一个新值的时候,首先对旧值进行 release ,对新值进行retain 然后进行赋值操作。
* weak 表明属性定义了一个持有者关系。当给属性设定一个新值的时候,这个值不会进行 retain,旧值也不会进行 release, 而是进行类似 assign 的操作。
不过当属性指向的对象被销毁时,该属性会被置为nil
* unsafe_unretained 的语义和 assign 类似,不过是用于对象类型的,表示一个非拥有(unretained)的,同时也不会在对象被销毁时置为nil的(unsafe)关系。
* copy 类似于 strong,不过在赋值时进行 copy 操作而不是 retain 操作。通常在需要保留某个不可变对象,并且防止它被意外改变时使用。

概括得讲:
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
* 不能使用NSAllocateObject/NSDeallocateObject
* 须遵守内存管理的方法命名规则
* 不要显式调用dealloc
* 使用@autoreleasepool块替代NSAutoreleasePool
* 不能使用NSZone
* 对象型变量不能作为C语言结构体(struct、union)的成员: 要把对象类型添加到结构体成员中,可以强制转换为void *或是附加__unsafe_unretained修饰符。
7. 内存相关常见问题

内存问题有两种:

  • 释放得太早,还在使用中就释放:
如果某个对象有至少一个拥有者,那么就必须保留不能释放,否则的话其他对象或者方法仍然有指向这个对象的指针沦为野指针(空指针)。这称之为过早释放,这是十分危险的,因为当野指针指向的内存区域再次被某个新的对象使用时,野指针上的操作便会破坏这个新对象造成文件丢失或者崩溃。
  • 释放得太晚,已经不用了但是还没释放:
如果某个对象失去了拥有者(变成没有拥有者)那么应该将其释放掉,否则没有拥有者的对象会被孤立而程序找不到,并且始终占用着一块内存,导致内存泄漏
7.1 内存泄漏

ARC内存泄露常见场景:

  • 对象型变量作为C语言结构体,或者联合体(struct、union)的成员
struct Data {
NSMutableArray __unsafe_unretained *array;
};

__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。如果管理时不注意赋值对象的所有者,便可能遭遇内存泄露或者程序崩溃。

  • 循环引用

循环引用常见有如下几种情况:

  1. 两个对象互相持有对象,这个可以设置弱引用解决,最常见的是block,但是需要注意并非所有的block都需要使用weak来打破循环引用,如果self没有持有block就不会造成循环引用。而有些地方之所以使用了__weak,是为了在[self dealloc]之后就不再执行了。

解决方案 1:在block外部对弱化self,在block内部强化已经弱化的weakSelf

@interface Test: NSObject {
id __weak obj_;
}

- (void)setObject:(id __strong)obj;
block持有self对象,这个要在block块外面和里面设置弱引用和强引用。

__weak __typeof(self) wself = self;
obj.block = ^{
__strong __typeof(wself) sself = wself;
[sself updateSomeThing];
}

解决方法 2: 通过将对象在block中设置为nil,但是这种需要注意的是block一定要被执行

__block TestObject *object = [[TestObject alloc] init…];
object.completionHandler = ^(NSInteger result) {
[object testMethod];
object = nil;
};
  1. NSTimer的target持有self
self.timmer = [NSTimer scheduledTimerWithTimeInterval:1.0 
target:self
selector:@selector(updateTime:)
userInfo:nil
repeats:YES];

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;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[wself commentAnimation];
});

__weak __typeof(self) wself = self;
timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 5 * NSEC_PER_SEC, 1 * NSEC_PER_SEC);
dispatch_source_set_event_handler(timer, ^{
[wself commentAnimation];
});

dispatch_resume(timer);
  1. 代理delegate

代理在一般情况下,需要使用weak修饰,我们常见的delegate 一般会是VC的属性,被VC持有,同时我们会将VC相关的属性作为delegate从而导致循环引用。
解决方案:delegate属性使用weak修饰

  1. 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被释放。

文章目錄
  1. 1. 1. 内存区域分布
  2. 2. 2. iOS 引用计数内存管理策略
  3. 3. 3. iOS开发中的内存管理四个黄金法则
  4. 4. 4. 有关引用计数的方法:
  5. 5. 5. iOS中的变量标识符 & 属性标识符
  6. 6. 6. ARC规则
  7. 7. 7. 内存相关常见问题
    1. 7.1. 7.1 内存泄漏
  8. 8. 8. 内存泄漏的排查方法
  9. 9. 9. AutoreleasePool 与 RunLoop的关系
  10. 10. 10. weak-strong dance