iOS-内存管理-理论篇

前言

关于iOS的内存管理网上已有不少前辈大神提供了不少学习笔记和博客,很是感谢可以从他们那里学到东西,此篇只是根据自身的总结学习。算法

内存基本知识

  • 1.内存的基本划分:

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

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

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

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

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

    简单总结 在iOS中数据是存在在堆和栈中的,然而咱们的内存管理管理的是堆上的内存,栈上的内存并不须要咱们管理。工具

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

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

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

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

内存管理基本知识

  • 一、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 类型和对象类型默认的全部权修饰符。

    原理:

    一、对象是经过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。

    二、对象是经过除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;。

    原理:

    一、对象是经过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中。

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

    在MRC中增长的引用计数都是须要本身手动释放的,因此咱们须要知道哪些方式会引发引用计数+1

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

    四个法则

    • 本身生成的对象,本身持有
    • 非本身生成的对象,本身也能持有。
    • 不在须要本身持有对象的时候,释放。
    • 非本身持有的对象无需释放。
  • 三、循环引用

    什么是循环引用?循环引用就是在两个对象互相之间强引用了,引用计数都加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秒后没被释放,就会进入断言。

参考文档

www.jianshu.com/p/c448a4b4c…

www.jianshu.com/p/c3344193c…

www.jianshu.com/p/40ec2cae2…

相关文章
相关标签/搜索