阅读 215

iOS-内存管理 (一、理论篇)

前言

关于iOS的内存管理网上已有很多前辈大神提供了很多学习笔记和博客,非常感谢能够从他们那里学到东西,此篇只是根据自身的总结学习。 实践篇:

内存基本知识

  • 1.内存的基本划分:

    1.栈区(heap):由系统去管理。地址从高到低分配。先进后出。会存一些局部变量,函数跳转跳转时现场保护(寄存器值保存于恢复),这些系统都会帮我们自动实现,无需我们干预。所以大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃 。

    2.堆区(stack):需要我们自己管理内存,alloc申请内存release释放内存。创建的对象也都放在这里。 地址是从低到高分配。堆是所有程序共享的内存,当N个这样的内存得不到释放,堆区会被挤爆,程序立马瘫痪。这就是内存泄漏。

    3.全局区/静态区(staic):全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。程序结束后有系统释放。

    4.常量区:常量字符串就是放在这里的,还有const常量。

    5.代码区:存放App代码,App程序会拷贝到这里。

    简单总结 在iOS中数据是存在在堆和栈中的,然而我们的内存管理管理的是堆上的内存,栈上的内存并不需要我们管理。(注意:如果是ARC下,堆上的OC对象其实是系统帮助我们管理了)

    • 基础数据类存储在栈上
    • OC对象存储在堆上
  • 2.引用计数

    引用计数是计算机编程语言中的一种内存管理技术,是指将资源(可以是对象、内存或磁盘空间等等)的被引用次数保存起来,当被引用次数变为零时就将其释放的过程。使用引用计数技术可以实现自动资源管理的目的。同时引用计数还可以指使用引用计数技术回收未使用资源的垃圾回收算法。

    当创建一个对象的实例并在堆上申请内存时,对象的引用计数就为1,在其他对象中需要持有这个对象时,就需要把该对象的引用计数加1,需要释放一个对象时,就将该对象的引用计数减1,直至对象的引用计数为0,对象的内存会被立刻释放。

    在遥远的以前,iOS开发的内存管理是手动处理引用计数,在合适的地方使引用计数-1,直到减为0,内存释放。现在的iOS开发内存管理使用的是ARC,自动管理引用计数,会根据引用计数自动监视对象的生存周期,实现方式是在编译时期自动在已有代码中插入合适的内存管理代码以及在 Runtime 做一些优化。

内存管理基本知识

  • 1、ARC自动管理内存(Automatic Reference Counting)

    Automatic Reference Counting,自动引用计数,即 ARC,WWDC2011 和 iOS5 所引用的最大变革和最激动人心的变化。ARC 是新的 LLVM3.0 编译器的一项特性,使用 ARC,可以说是一举解决了广大 iOS 开发者所憎恶的手动内存管理的麻烦。使用 ARC 后,系统会检测出何时需要保持对象,何时需要自动释放对象,何时需要释放对象,编译器会管理好对象的内存,会在合适的地方插入retain 、 release 、 autorelease ,通过生成正确的代码,去自动释放或者保持对象。

    所有权修饰符: Objective-C编程中为了处理对象,可将变量类型定义为id类型或各种对象类型。 ARC中id类型和对象类其类型必须附加所有权修饰符。

    其中有以下4种所有权修饰符:

    • __strong
    • __weak
    • __unsafe_unretaied
    • __autoreleasing

    所有权修饰符和属性的修饰符对应关系如下所示:

    • assign 对应的所有权类型是 __unsafe_unretained
    • copy 对应的所有权类型是 __strong
    • retain 对应的所有权类型是 __strong
    • strong 对应的所有权类型是 __strong
    • unsafe_unretained对应的所有权类型是__unsafe_unretained
    • weak 对应的所有权类型是 __weak

    __strong

    __strong 表示强引用,对应定义 property 时用到的 strong。当对象没有任何一个强引用指向它时,它才会被释放。如果在声明引用时不加修饰符,那么引用将默认是强引用。当需要释放强引用指向的对象时,需要保证所有指向对象强引用置为 nil。__strong 修饰符是 id 类型和对象类型默认的所有权修饰符。

    原理:

    1、对象是通过alloc方法产生的情况

    {
     id __strong obj = [[NSObject alloc] init];
    }
    复制代码
    //编译器的模拟代码
    id obj = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    // 出作用域的时候调用
    objc_release(obj);
    复制代码

    虽然ARC有效时不能使用release方法,但由此可知编译器自动插入了release。

    2、对象是通过除alloc、new、copy、multyCopy外方法产生的情况

    {
     id __strong obj = [NSMutableArray array];
    }
    复制代码

    结果与之前稍有不同:

    //编译器的模拟代码
    id obj = objc_msgSend(NSMutableArray,@selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_release(obj);
    复制代码

    objc_retainAutoreleasedReturnValue函数主要用于优化程序的运行。它是用于持有(retain)对象的函数,它持有的对象应为返回注册在autoreleasePool中对象的方法,或是函数的返回值。像该源码这样,在调用array类方法之后,由编译器插入该函数。

    这种objc_retainAutoreleasedReturnValue函数是成对存在的,与之对应的函数是objc_autoreleaseReturnValue。它用于array类方法返回对象的实现上。下面看看NSMutableArray类的array方法通过编译器进行了怎样的转换:

    {
     id __strong obj = [[NSObject alloc] init];
    }
    复制代码
    //编译器的模拟代码
    + (id)array
    {
       id obj = objc_msgSend(NSMutableArray,@selector(alloc));
       objc_msgSend(obj,@selector(init));
       // 代替我们调用了autorelease方法
       return objc_autoreleaseReturnValue(obj);
    }
    复制代码

    我们可以看见调用了objc_autoreleaseReturnValue函数且这个函数会返回注册到自动释放池的对象,但是,这个函数有个特点,它会查看调用方的命令执行列表,如果发现接下来会调用objc_retainAutoreleasedReturnValue则不会将返回的对象注册到autoreleasePool中而仅仅返回一个对象。达到了一种最优效果。如下图:

    __weak

    __weak 表示弱引用,对应定义 property 时用到的 weak。弱引用不会影响对象的释放,而当对象被释放时,所有指向它的弱引用都会自定被置为 nil,这样可以防止野指针。使用__weak修饰的变量,即是使用注册到autoreleasePool中的对象。__weak 最常见的一个作用就是用来避免循环循环。需要注意的是,__weak 修饰符只能用于 iOS5 以上的版本,在 iOS4 及更低的版本中使用 __unsafe_unretained 修饰符来代替。

    __weak 的几个使用场景:

    • 在 Delegate 关系中防止循环引用。
    • 在 Block 中防止循环引用。
    • 用来修饰指向由 Interface Builder 创建的控件。比如:@property (weak, nonatomic) IBOutlet UIButton *testButton;。

    原理:

    1、对象是通过alloc方法产生的情况

    {
     id __weak obj = [[NSObject alloc] init];
    }
    复制代码
    //编译器转换后的模拟代码
    id obj;
    id tmp = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(tmp,@selector(init));
    objc_initweak(&obj,tmp);
    objc_release(tmp);
    objc_destroyWeak(&object);
    复制代码

    对于__weak内存管理也借助了类似于引用计数表的散列表,它通过对象的内存地址做为key,而对应的__weak修饰符变量的地址作为value注册到weak表中,在上述代码中objc_initweak就是完成这部分操作,而objc_destroyWeak则是销毁该对象对应的value。当指向的对象被销毁时,会通过其内存地址,去weak表中查找对应的__weak修饰符变量,将其从weak表中删除。所以,weak在修饰只是让weak表增加了记录没有引起引用计数表的变化。

    对象通过objc_release释放对象内存的动作如下:

    • objc_release
    • 因为引用计数为0所以执行dealloc
    • _objc_rootDealloc
    • objc_dispose
    • objc_destructInstance
    • objc_clear_deallocating

    而在对象被废弃时最后调用了objc_clear_deallocating,该函数的动作如下:

    对象通过objc_release释放对象内存的动作如下:

    • 从weak表中获取已废弃对象内存地址对应的所有记录
    • 将已废弃对象内存地址对应的记录中所有以weak修饰的变量都置为nil
    • 从weak表删除已废弃对象内存地址对应的记录
    • 根据已废弃对象内存地址从引用计数表中找到对应记录删除
    • 据此可以解释为什么对象被销毁时对应的weak指针变量全部都置为nil,同时,也看出来销毁weak步骤较多,如果大量使用weak的话会增加CPU的负荷

    还需要确认一点是:使用__weak修饰符的变量,即是使用注册到autoreleasePool中的对象。

    {
       id __weak obj1 = obj; 
       NSLog(@"obj2-%@",obj1);
    }
    复制代码
    //编译器转换上述代码如下:
    id obj1; 
    objc_initweak(&obj1,obj);
    id tmp = objc_loadWeakRetained(&obj1);
    objc_autorelease(tmp);
    NSLog(@"%@",tmp);
    objc_destroyWeak(&obj1);
    复制代码

    objc_loadWeakRetained函数获取附有__weak修饰符变量所引用的对象并retain, objc_autorelease函数将对象放入autoreleasePool中,据此当我们访问weak修饰指针指向的对象时,实际上是访问注册到自动释放池的对象。因此,如果大量使用weak的话,在我们去访问weak修饰的对象时,会有大量对象注册到自动释放池,这会影响程序的性能,同时也可能对象已经被释放。

    为什么访问weak修饰的对象就会访问注册到自动释放池的对象呢?

    因为weak不会引起对象的引用计数器变化,因此,该对象在运行过程中很有可能会被释放。所以,需要将对象注册到自动释放池中并在autoreleasePool销毁时释放对象占用的内存。

    解决方案:要访问weak修饰的变量时,先将其赋给一个strong变量,然后进行访问。

    __unsafe_unretained

    ARC 是在 iOS5 引入的,而 __unsafe_unretained 这个修饰符主要是为了在ARC刚发布时兼容iOS4以及版本更低的系统,因为这些版本没有弱引用机制。这个修饰符在定义property时对应的是unsafe_unretained。__unsafe_unretained 修饰的指针纯粹只是指向对象,没有任何额外的操作,不会去持有对象使得对象的 retainCount +1。而在指向的对象被释放时依然原原本本地指向原来的对象地址,不会被自动置为 nil,所以成为了野指针,非常不安全。

    __unsafe_unretained的应用场景: 在 ARC 环境下但是要兼容 iOS4.x 的版本,用__unsafe_unretained 替代 __weak 解决强循环循环的问题。

    __autoreleasing

    将对象赋值给附有__autoreleasing修饰符的变量等同于MRC时调用对象的autorelease方法。

    @autoeleasepool {
       // 如果看了上面__strong的原理,就知道实际上对象已经注册到自动释放池里面了
       id __autoreleasing obj = [[NSObject alloc] init];
    }
    复制代码
    //编译器转换上述代码如下1:
    id pool = objc_autoreleasePoolPush();  
    id obj = objc_msgSend(NSObject,@selector(alloc));
    objc_msgSend(obj,@selector(init));
    objc_autorelease(obj);
    objc_autoreleasePoolPop(pool);
    @autoreleasepool {
       id __autoreleasing obj = [NSMutableArray array];
    }
    复制代码
    //编译器转换上述代码如下2:
    id pool = objc_autoreleasePoolPush();  
    id obj = objc_msgSend(NSMutableArray,@selector(array));
    objc_retainAutoreleasedReturnValue(obj);
    objc_autorelease(obj);
    objc_autoreleasePoolPop(pool);
    复制代码

    上面两种方式,虽然第二种持有对象的方法从alloc方法变为了objc_retainAutoreleasedReturnValue函数,但一样都是通过objc_autorelease,注册到autoreleasePool中。

  • 2、MRC 手动管理内存(Manual Reference Counting)-简单介绍

    在MRC中增加的引用计数都是需要自己手动释放的,所以我们需要知道哪些方式会引起引用计数+1

    对象操作 OC中对应的方法 引用计数的变化
    生成并持有对象 alloc/new/copy/mutableCopy +1
    持有对象 retain +1
    释放对象 release -1
    废弃对象 dealloc 归0

    四个法则

    • 自己生成的对象,自己持有
    • 非自己生成的对象,自己也能持有。
    • 不在需要自己持有对象的时候,释放。
    • 非自己持有的对象无需释放。
  • 3、循环引用

    什么是循环引用?循环引用就是在两个对象互相之间强引用了,引用计数都加1了,我们前面说过,只有当引用计数减为0时对象才释放。但是这两个的引用计数都依赖于对方,所以也就导致了永远无法释放

    最容易产生循环引用的两种情况就是Delegate和Block。所以我们就引入了弱引用这种概念,即弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。也就是我们上面所说的所有权修饰符__weak的作用。关于原理在__weak部分也有描述,简单的描述就是每一个拥有弱引用的对象都有一张表来保存弱引用的指针地址,但是这个弱引用并不会使对象引用计数加1,所以当这个对象的引用计数变为0时,系统就通过这张表,找到所有的弱引用指针把它们都置成nil

    所以在ARC中做内存管理主要就是发现这些内存泄漏,关于内存泄漏Instrument为我们提供了 Allocations/Leaks 这样的工具用来检测。

    类似内存泄漏工具MLeaksFinder的原理,具体原理如下:

    我们知道,当一个 UIViewController 被 pop 或 dismiss 后,该 UIViewController 包括它的 view,view 的 subviews 等等将很快被释放(除非你把它设计成单例,或者持有它的强引用,但一般很少这样做)。于是,我们只需在一个 ViewController 被 pop 或 dismiss 一小段时间后,看看该 UIViewController,它的 view,view 的 subviews 等等是否还存在。

    具体的方法是,为基类 NSObject 添加一个方法 -willDealloc 方法,该方法的作用是,先用一个弱指针指向 self,并在一小段时间(3秒)后,通过这个弱指针调用 -assertNotDealloc,而 -assertNotDealloc 主要作用是直接中断言。

    - (BOOL)willDealloc {
       __weak id weakSelf = self;
       dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
           [weakSelf assertNotDealloc];
       });
       return YES;
    }
    - (void)assertNotDealloc {
       NSAssert(NO, @“”);
    }
    复制代码

    这样,当我们认为某个对象应该要被释放了,在释放前调用这个方法,如果3秒后它被释放成功,weakSelf 就指向 nil,不会调用到 -assertNotDealloc 方法,也就不会中断言,如果它没被释放(泄露了),-assertNotDealloc 就会被调用中断言。这样,当一个 UIViewController 被 pop 或 dismiss 时(我们认为它应该要被释放了),我们遍历该 UIViewController 上的所有 view,依次调 -willDealloc,若3秒后没被释放,就会进入断言。

后记:

具体代码实践请移步

iOS-内存管理(二、理论实践1)

iOS-内存管理(三、理论实践2)

参考文档