iOS内存管理机制

这世上,没有谁活得比谁容易,只是有人在呼天抢地,有人在默默努力。

  随着科技的发展,移动设备的内存越来越大,设备的运行速度也越来越快,但是相对于整个应用市场上成千上万的应用容量来说,还是及其有限的。因此,每一个应用所能占用的内存是有限制的。这一专题就是来探讨系统中的内存是如何分配的。

image

一. 计算机的基本知识

1.1)硬件内存区分

  我们的手机、电脑、或者智能设备都有随机存储器RAM(运行内存/主存)和只读存储器ROM(相当于计算机中的硬盘)。RAM是内部存储,ROM是外部存储。我们的CPU直接访问的是RAM,如果想访问外部存储,则数据须先从ROM中将数据调度到RAM中才能被CPU访问。也就是说CPU不能直接从内存卡等硬盘里面读取数据。

1.2)RAM和ROM的特点和区别

  • RAM:运行内存,CPU可以直接访问,访问速度快,价格高。不能够掉电存储,断电会失去数据,不稳定;
  • ROM:存储型内存,CPU不可以直接访问,访问速度慢,价格低。可以掉电存储,稳定;

1.3)RAM和ROM的协同工作

  由于RAM不支持掉电存储,所以App程序一般存储在ROM中。手机里面使用的ROM基本都是Nand Flash(闪存),CPU是不能直接访问的,而是需要文件系统/驱动程序(嵌入式中的EMC)将其读到RAM里面,CPU才可以访问。另外,RAM的速度也比Nand Flash快。

二. 内存分区

  说到内存分区,内存即指的是RAM,可以分为5个区。所有的进程(执行中的程序)都必须占用一定数量的内存(RAM),它或许是用来存放从磁盘(ROM)载入的程序代码,或是存放取自用户输入的数据等等。不过进程对这些内存的管理方式因内存用途不一而不尽相同,有些内存是事先静态分配和统一回收的,而有些却是按需要动态分配和回收的。

2.1)代码区

  代码段是用来存放可执行文件的操作指令(存放函数的二进制代码),也就是说它是可执行程序在内存中的镜像。代码段需要防止在运行时被非法修改,所以只允许读取操作,而不允许写入操作。它是不可写的。

2.2)常量区

  常量存储区,这是一块比较特殊的存储区,里面存放的是数字/字符常量。编译时分配,APP结束时由系统释放。

2.3)全局(静态)区

  编译时分配,APP结束时由系统释放。全局变量和静态变量的存储是放在这一块区域,程序退出后自动释放。

  • 数据区(全局初始化区),数据段用来存放程序中已经初始化的全局变量和静态变量。
  • BSS区(全局未初始化区),BSS段包含了程序中未初始化的全局变量和未初始化的静态变量。

2.4)堆(heap)区

  堆(FIFO)是由程序开发者分配和释放,地址是从低到高分配。用于存放进程运行中被动态分配的内存段,它大小并不固定,可动态扩张或缩减。当进程调用alloc等函数分配内存时,新分配的内存就被动态添加到堆上(堆被扩张);当利用release释放内存时,被释放的内存从堆中被剔除(堆被缩减),因为我们现在iOS基本都使用ARC来管理对象,所以不用我们程序员来管理,但是我们要知道这个对象存储的位置。

2.5)栈(stack)区

  栈(LIFO)是由系统自动分配并释放,地址从高到低分配。用于存放程序临时创建的局部变量,存放函数的参数值,局部变量等。也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味这在数据段中存放变量)。

  另外在函数被调用时,其参数也会被压入发起调用的进程栈中,并且待到调用结束后,函数的返回值也会被存放回栈中。由于栈的先进后出(LIFO)特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上将我们可以把栈看成一个临时数据寄存、交换的内存区。函数跳转时现场保护(寄存器值保存于恢复),这些系统都会帮我们自动实现,无需我们干预。所以大量的局部变量,深递归,函数循环调用都可能耗尽栈内存而造成程序崩溃 。

2.6)内存分区总结

  上述几种内存区域中常量区、数据段、BSS和堆通常是被连续存储的,内存位置上是连续的,而代码段和栈往往会被独立存放。

  栈是向低地址扩展的数据结构,是一块连续内存的区域。堆是向高地址扩展的数据结构,是不连续的内存区域。有人会问堆和栈会不会碰到一起,他们之间间隔很大,绝少有机会能碰到一起,况且堆是链表方式存储。

int age = 27; //全局初始化区(数据区)
NSString *name; //全局未初始化区(BSS区)
static NSString *sName = @"Dely";//全局(静态初始化)区(数据区)

@implementation ViewController 
- (void)viewDidLoad {  
		[super viewDidLoad];  
    Int tmpAge;//栈  
    NSString * tmpName = @"Dely";//栈  
    NSString * number = @"123456"; //123456\0在常量区,number在栈上。  
    NSMutableArray * array = [NSMutableArray arrayWithCapacity:1];//分配而来的8字节的区域就在堆中,array在栈中,指向堆区的地址。  
    NSInteger total = [self getTotalNumber:1 number2:1];
}

// 当ViewDidLoad代码块一过,tmpAge、tmpName 、number 、* array指针都会被系统编译器自动回收。而OC
// 对象不会被系统回收,因为它存放在堆里面,堆里面的内存是动态存储的,所以需要程序员手动回收内存。 

-(NSInteger)getTotalNumber:(NSInteger)number1 number2:(NSInteger)number2{  
    return number1 + number2;//number1和number2 栈区
}

@end 

三. 堆和栈的区别

3.1)申请方式和回收方式

  • 栈区(stack):由编译器自动分配和释放。

  • 堆区(heap):由程序开发者分配和释放。

3.2)申请后的系统响应

  • 栈:每一个函数在执行的时候都会向操作系统索要资源,栈区就是函数运行时的内存,栈区中的变量由编译器负责分配和释放,内存随着函数的运行分配,随着函数的结束而释放,由系统自动完成。

  注意:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。

  • 堆:首先应该知道操作系统有一个记录空闲内存地址的链表。当系统收到程序的申请时,会遍历该链表,寻找第一个空间大于所申请空间的堆结点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中

3.3)申请大小的限制

  • 栈:栈是向低地址扩展的数据结构,是一块连续的内存的区域。是栈顶的地址和栈的最大容量是系统预先规定好的,栈的大小是2M(也有的说是1M,总之是一个编译时就确定的常数 ) ,如果申请的空间超过栈的剩余空间时,将提示overflow。因此,能从栈获得的空间较小。

  • 堆:堆是向高地址扩展的数据结构,是不连续的内存区域。这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址。堆的大小受限于计算机系统中有效的虚拟内存。由此可见,堆获得的空间比较灵活,也比较大。

3.4)申请效率的比较

  • 栈区(stack):由系统自动分配,速度较快。但程序员是无法控制的。

  • 堆区(heap):是由alloc分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来最方便。

3.5)分配方式的比较

  • 栈区(stack):有2种分配方式:静态分配和动态分配。静态分配是编译器完成的,比如局部变量的分配。动态分配由alloc函数进行分配,但是栈的动态分配和堆是不同的,他的动态分配是由编译器进行释放,无需我们手工实现。

  • 堆区(heap):堆都是动态分配的,没有静态分配的堆。

3.6)分配效率的比较

  • 栈区(stack):栈是操作系统提供的数据结构,计算机会在底层对栈提供支持:分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。

  • 堆区(heap):堆则是C/C++函数库提供的,它的机制是很复杂的,例如为了分配一块内存,库函数会按照一定的算法(具体的算法可以参考数据结构/操作系统)在堆内存中搜索可用的足够大小的空间,如果没有足够大小的空间(可能是由于内存碎片太多),就有可能调用系统功能去增加程序数据段的内存空间,这样就有机会分到足够大小的内存,然后进行返回。显然,堆的效率比栈要低得多。

四. 内存分配的引入

4.1)什么行为会增加App的内存占用?

① 创建一个OC对象。

② 定义一个变量。

③ 调用一个函数和方法。

4.2)内存管理范围

  任何继承了NSObject的对象,其它非对象类型不需要管理。简单来说,只有OC对象需要内存管理,非OC对象类型不需要内存管理,比如基本数据类型。

4.3)内存管理原因

  由内存管理范围,我们是不是就有疑问了?为什么OC对象需要进行内存管理,而其它非对象类型比如基本数据类型就不需要进行内存管理了呢?只有OC对象才需要进行内存管理的本质原因是什么?

  由于移动设备或者PC设备的内存大小是有限制,所以任何应用都需要进行内存管理。

  因为OC的对象在内存中是以堆(heap)的方式分配空间的,而堆(heap)是由程序开发者释放的,就是release。也就是说OC对象是存储在堆(heap)里面的,堆内存需要程序开发者手动回收。而非OC对象一般存放在栈里面,栈内存会被系统自动回收。堆内存是动态分配的,所以也需要程序开发者手动添加内存,回收内存。

五. Objective-C内存管理

5.1)Objective-C内存管理相关术语

  什么是内存管理?是指软件运行时对计算机内存资源的分配和使用的技术。其最主要的目的是如何高效,快速的分配,并且在适当的时候释放和回收内存资源。

  引用计数:OC中每个对象都有一个与之对应的整数,叫引用计数。Objective-C的内存管理本质是通过引用计数实现的。

  MRC(manual reference counting):即手动引用计数,在iOS5之前内存是由开发者自己手动管理的,写完代码需要合理插入retainrelease,保证内存不会泄露,程序可以正常运行。

  ARC(automatic reference counting):即自动引用计数,2011年WWDC大会iOS5提出了自动引用计数(ARC),内存的管理由系统进行接管,开发者只需要关注业务逻辑实现,大大提高了开发效率。

5.2)什么是引用计数?

5.2.1)引用计数解释

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

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

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

5.2.2)文艺解释

  当这个这个世界上最后一个人都忘记你时,就迎来了终极死亡。类比于引用计数,就是每有一个人记得你时你的引用计数加1,每有一个人忘记你时,你的引用计数减1,当所有人都忘记你时,你就消失了,也就是从内存中释放了。

  如果再深一层,包含我们后面要介绍的ARC中的强引用和弱引用的话,那这个记住的含义就不一样了。强引用就是你挚爱的亲人,朋友等对你比较重要的人记得你,你的引用计数才加1。

  而弱引用就是那种路人,一面之缘的人,他们只是对你有一个印象,他们记得你是没有用的,你的引用计数不会加1。当你挚爱的人都忘记你时,你的引用计数归零,你就从这个世界上消失了,而这些路人只是感觉到自己记忆中忽然少了些什么而已。

5.2.3)代码测试

  我们创建一个工程,在Build Phases里设置AppDelegate的Compiler Flags-fno-objc-arc来开启手动管理引用计数的模式。

- (void)noObjcArc {
    NSObject *object = [[NSObject alloc] init];
    NSLog(@"引用计数=%lu 对象内存(堆)=%p object指针内存地址(栈)=%p", (unsigned long)[object retainCount], object, &object);

    self.property = object;
    NSLog(@"引用计数=%lu 对象内存(堆)=%p object指针内存地址(栈)=%p property指针内存地址=%p", (unsigned long)[object retainCount], object, &object, &_property);
    
    [object release];
    NSLog(@"引用计数=%lu 对象内存(堆)=%p object指针内存地址(栈)=%p property指针内存地址=%p", (unsigned long)[object retainCount], object, &object, &_property);
}

​ 输出结果:

引用计数=1 对象内存(堆)=0x600001dc84d0 object指针内存地址(栈)=0x7ffeeceb0808
引用计数=2 对象内存(堆)=0x600001dc84d0 object指针内存地址(栈)=0x7ffeeceb0808 property指针内存地址=0x600001fe9378
引用计数=1 对象内存(堆)=0x600001dc84d0 object指针内存地址(栈)=0x7ffeeceb0808 property指针内存地址=0x600001fe9378

  我们看到object持有对象引用计数+1为1,然后self.property又持有了对象,引用计数再+1为2,然后我们主动释放object,引用计数-1变为1。我们能看到[object release]释放后指向对象的指针仍就被保留在object这个变量中,只是对象的引用计数-1了而已。

  对应的内存上的分配如下图所示:

image

5.3)自动释放池

5.3.1)AutoreleasePool的原理

  自动释放池是OC中的一种内存自动回收机制,它可以延迟加入AutoreleasePool中的变量release的时机,即当我们创建了一个对象,并把他加入到了自动释放池中时,他不会立即被释放,会等到一次runloop结束或者作用域超出{}或者超出[pool release]之后再被释放。

  系统有一个现成的自动内存管理池,他会随着每一个mainRunloop的结束而释放其中的对像;自动释放池也可以手动创建,他可以让pool中的对象在执行完代码后马上被释放,可以起到优化内存,防止内存溢出的效果(如视频针图片的切换时、创建大量临时对象时等)。

  autorelease 只是一个标记,表明会延迟自动释放,当一个autorelease对象超出自己的作用域后,会被添加到离他最近的autorelease pool中,当pool开始倾倒的时候,会向池里面所有的对象发送一次release方法,释放pool中所有的对象。

5.3.2)自动释放池的创建和销毁

  • MRC环境下:

    // MRC下 AutoreleasePool
    // 创建一个自动释放池
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    // do something
    id obj = [[NSMutableArray alloc] init];
    // 调autorelease方法将对象加入到自动释放池
    [obj autorelease];
    // 手动释放自动释放池执行完这行代码是,自动释放池会对加入他中的对象做一次release操作
    [pool release];
    // 自动释放池销毁时机:[pool release]代码执行完后.
    
  • ARC环境下

    @autoreleasepool {
        // 在这个{}之内的变量默认被添加到自动释放池
        id obj = [[NSMutableArray alloc] init];
    } // 出了这个括号,p被释放
    

5.3.3)自动释放池的使用场景

  • 如果你写了一个创建了很多临时变量的循环。你可能在下次循环之前使用一个自动释放池来释放那些变量。在循环中使用自动释放池来减少应用程序的峰值内存占用。

    for (int i = 0; i < 1000; ++i) {
        @autoreleasepool{
            NSString *str = @"Hello World";
            NSLog(@"%@", str);
        }
    }
    
    //在循环中创建了大量的临时对象NSString,在方法没有走完的情况下,每次创建的对象是不会释放的,所以我们用自动释放池,在每次循环开始的时候把临时变量NSString放到池里,等每次循环结束的时候倾倒池子,从而每次释放释放NSString临时变量。
    
  • 如果你创建了一个二级线程。你必须创建你的自动释放池在线程一开始执行的时候,否则,你的应用将会内存泄漏。

5.4)MRC手动管理引用计数

  MRC内存管理原则,谁申请,谁释放,遇到alloc/copy/retain等都需要添加release或autorelease。

5.4.1)对象操作

  在MRC中增加的引用计数都是需要自己手动释放的,所以我们需要知道哪些方式会引起引用计数发生变化,如下表所示:

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

5.4.2)四个法则

  • 自己生成的对象,自己持有;

    // 1.自己生成并持有该对象
    NSObject *obj1 = [[NSObject alloc] init];
    NSObject *obj2 = [NSObject new];
    
  • 非自己生成的对象,自己也能持有;

    // 2.持有非自己生成的对象
    id obj3 = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
    [obj3 retain]; // 自己持有对象
    
  • 不在需要自己持有对象的时候,释放;

    // 3.不在需要自己持有对象的时候,释放
    NSObject *obj4 = [[NSObject alloc] init]; // 此时持有对象
    [obj4 release]; // 释放对象
    // 注意:指向对象的指针仍就被保留在obj4这个变量中,但对象已经释放,不可访问
    
  • 非自己持有的对象无需释放;

    // 4.非自己持有的对象无法释放
    id obj = [NSArray array]; // 非自己生成的对象,且该对象存在,但自己不持有
    [obj release]; // ~~~此时将运行时crash 或编译器报error~~~ 非 ARC 下,调用该方法会导致编译器报 issues。此操作的行为是未定义的,可能会导致运行时 crash 或者其它未知行为
    

5.4.3 非自己生成的对象,且该对象存在,但自己不持有

  其中关于非自己生成的对象,且该对象存在,但自己不持有是如何实现的呢?这个特性是使用autorelease来实现的,示例代码如下:

- (id) getAObjNotRetain {    
    id obj = [[NSObject alloc] init]; // 自己持有对象    
    [obj autorelease]; // 取得的对象存在,但自己不持有该对象    
    return obj;
}

  使用autorelease方法可以使取得的对象存在,但自己不持有对象。autorelease 使得对象在超出生命周期后能正确的被释放(通过调用release方法)。在调用 release 后,对象会被立即释放,而调用 autorelease 后,对象不会被立即释放,而是注册到 autoreleasepool 中,经过一段时间后 pool结束,此时调用release方法,对象被释放。

  像[NSMutableArray array] [NSArray array]都可以取得对象都不持有的对象,这些方法都是通过autorelease实现的。

5.5)ARC自动管理引用计数

5.5.1)ARC介绍

  ARC其实也是基于引用计数,只是编译器在编译时期自动在已有代码中插入合适的内存管理代码(包括 retain、release、copy、autorelease、autoreleasepool)以及在 Runtime 做一些优化。

  现在的iOS开发基本都是基于ARC的,所以开发人员大部分情况都是不需要考虑内存管理的,因为编译器已经帮你做了。为什么说是大部分呢,因为底层的 Core Foundation 对象由于不在 ARC 的管理下,所以需要自己维护这些对象的引用计数。

  还有就算循环引起的互相之间强引用,引用计数永远不会减到0,所以需要自己主动断开循环引用,使引用计数能够减少。

5.5.2)所有权修饰符

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

  其中有以下4种所有权修饰符(声明变量修饰符):

  • __strong:强引用,持有所指向对象的所有权,无修饰符情况下的默认值。如需强制释放,可置nil。

  • __weak:弱引用,不持有所指向对象的所有权,引用指向的对象内存被回收之后,引用本身会置nil,避免野指针。

  • __unsafe_unretaied:这个修饰符主要是为了在ARC刚发布时兼容iOS4以及版本更低的系统,因为这些版本没有弱引用机制。

  • __autoreleasing:自动释放对象的引用,一般用于传递参数。

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

  • strong对应的所有权类型是 __strong

  • copy 对应的所有权类型是 __strong

  • retain 对应的所有权类型是 __strong

  • weak对应的所有权类型是 __weak

  • unsafe_unretained对应的所有权类型是__unsafe_unretained

  • assign 对应的所有权类型是 __unsafe_unretained

  关系对照图:

image

关于属性修饰符详细参考文章:https://www.cnblogs.com/hubert-style/p/15045430.html

(1)__strong

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

  示例:我们常用的定时器

// 所有权修饰符 __strong
NSTimer *timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];

// 默认所有权,相当于
NSTimer * __strong timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(runTimer) userInfo:nil repeats:NO];

// 当不需要使用时,强制销毁定时器
[timer invalidate];
timer = nil;

  原理解析1:对象通过alloc、new、copy、mutableCopy生成

{
    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、mutableCopy外方法产生的情况:

{
    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)array
{
    return [[NSMutableArray 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中而仅仅返回一个对象。达到了一种最优效果。如下图:

image

(2)__weak

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

  __weak 的几个使用场景:

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

  原理:

{
    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,该函数的动作如下:

  • 从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修饰的变量时,先将其赋给一个strong变量,然后进行访问;

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

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

(3)__unsafe_unretained

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

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

(4)__autoreleasing

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

@autoeleasepool {
    // 如果看了上面__strong的原理,就知道实际上对象已经注册到自动释放池里面了 
    id __autoreleasing obj = [[NSObject alloc] init];
}

  编译器转换上述代码如下:

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];
}

  编译器转换上述代码如下:

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中。

5.6)循环引用

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

  最容易产生循环引用的两种情况就是DelegateBlock。所以我们就引入了弱引用这种概念,即弱引用虽然持有对象,但是并不增加引用计数,这样就避免了循环引用的产生。也就是我们上面所说的所有权修饰符__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秒后没被释放,就会中断言。

5.7)Core Foundation 对象的内存管理

  底层的 Core Foundation 对象,在创建时大多以 XxxCreateWithXxx 这样的方式创建,例如:

// 创建一个 CFStringRef 对象
CFStringRef str= CFStringCreateWithCString(kCFAllocatorDefault, “hello world", kCFStringEncodingUTF8);

// 创建一个 CTFontRef 对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

  对于这些对象的引用计数的修改,要相应的使用 CFRetain 和 CFRelease 方法。如下所示:

// 创建一个 CTFontRef 对象
CTFontRef fontRef = CTFontCreateWithName((CFStringRef)@"ArialMT", fontSize, NULL);

// 引用计数加 1
CFRetain(fontRef);

// 引用计数减 1
CFRelease(fontRef);

  对于 CFRetainCFRelease 两个方法,读者可以直观地认为,这与 Objective-C 对象的 retainrelease 方法等价。

  所以对于底层 Core Foundation 对象,我们只需要延续以前手工管理引用计数的办法即可。

  除此之外,还有另外一个问题需要解决。在 ARC 下,我们有时需要将一个 Core Foundation 对象转换成一个 Objective-C 对象,这个时候我们需要告诉编译器,转换过程中的引用计数需要做如何的调整。这就引入了bridge相关的关键字,以下是这些关键字的说明:

  • __bridge: 只做类型转换,不修改相关对象的引用计数,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
  • __bridge_retained:类型转换后,将相关对象的引用计数加 1,原来的 Core Foundation 对象在不用时,需要调用 CFRelease 方法。
  • __bridge_transfer:类型转换后,将该对象的引用计数交给 ARC 管理,Core Foundation 对象在不用时,不再需要调用 CFRelease 方法。

5.8)总结常见内存泄漏

(1)僵尸对象和野指针

  • 僵尸对象:内存被回收的对象;
  • 野指针:指向僵尸对象的指针叫野指针,向野指针发送消息会导致崩溃;

(2)循环引用;

(3)循环中对象占用内存大;

(3)无线循环;

(4)系统内存警告;

posted @ 2021-10-13 15:36  背包の技术  阅读(1229)  评论(0编辑  收藏  举报