【iOS-MRC】

文章目录

  • 前言
  • 1. 什么是内存管理
    • 1.1 iOS内存分区
  • 2. MRC 手动管理内存(Manual Reference Counting)
    • 2.1 引用计数器
        • 2.1.1引用计数器的特点
    • 2.2 引用计数器操作
    • 2.3 dealloc 方法
    • 2.4 野指针和空指针
    • 2.5 内存管理的思考方式
        • 2.5.1 自己生成的对象自己持有
        • 2.5.2 非自己生成的对象自己也可以持有
        • 2.5.3 不再需要自己持有的对象就将其释放
        • 2.5.4 非自己持有的对象自己无法释放。
    • 2.6 @property 参数
    • 2.7 自动释放池
        • 2.7.1 autorelease 的原理实质
        • 2.7.2 autorelease 的注意事项
        • 2.7.3 autorelease 误区
  • 总结

前言

ARC和MRC是密不可分,ARC是编译器帮助我们自动进行内存管理的模式,在很早之前需要我们手动管理内存和引用计数,这就是MRC模式。

OC(Objective-C)的MRC(Manual Reference Counting)是一种手动内存管理模式,它是在ARC(Automatic Reference Counting)之前的一种内存管理模式。在MRC模式下,开发者需要手动管理对象的内存分配和释放,通过增加和减少对象的引用计数来进行内存管理。

1. 什么是内存管理

程序在运行的过程中,往往涉及到创建对象、定义变量、调用函数或方法,而这些行为都会增加程序的内存占用。

而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的。

当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要再使用的对象、变量等。

如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验。

所以,我们需要对 「内存」 进行合理的分配内存、清除内存,回收不需要再使用的对象。从而保证程序的稳定性。

在 iOS 中,我们通常将内存分为五大部分

1.1 iOS内存分区

内存指的就是RAM(random access memory),在iOS里面内存分配区域主要分为五个区:栈区(系统管理的地方)、堆区(程序员控制的地方)、静态区(全局区)、常量区、代码区。

  • 代码区:用于存放程序的代码,即 CPU 执行的机器指令,并且是只读的。

  • 全局区 / 静态区:它主要存放静态数据、全局数据和常量。分为未初始化全局区(BSS 段)、初始化全局区:(数据段)。程序结束后由系统释放。

    • 数据段:用于存放可执行文件中已经初始化的全局变量,也就是用来存放静态分配的变量和全局变量。
    • BSS 段:用于存放程序中未初始化的全局变量。
    • chat也解释了他们的区别:在程序执行过程中,需要为程序中的变量和常量分配内存空间。在可执行文件中,存放这些变量和常量的内存区域被划分为多个段(section),其中包括数据段(data segment)、BSS段和代码段(code segment)等。
    • 数据段和BSS段是用来存放程序中的变量和常量的。其中,数据段存放已经初始化的全局变量和静态变量,而BSS段则存放未初始化的全局变量和静态变量。
    • 数据段在可执行文件中被标记为.data段,而BSS段则在可执行文件中被标记为.bss段。在程序运行时,数据段和BSS段都被映射到进程的内存空间中,以供程序运行时访问。

需要注意的是,BSS段中存放的变量都被初始化为0或NULL,因此在程序中无需显式地初始化这些变量。而数据段中的变量在定义时需要显式地赋值

总的来说,数据段和BSS段都是用来存放程序中的变量和常量的内存区域。其中,数据段存放已经初始化的变量,而BSS段存放未初始化的变量。

  • 常量区:用于存储已经初始化的常量。程序结束后由系统释放。
  • 栈区(Stack): 用于存放程序临时创建的变量、存放函数的参数值、局部变量等。由编译器自动分配释放。
  • 堆区(Heap): 用于存放进程运行中被动态分配的内存段。它大小不固定,可动态扩张和缩减。由程序员分配和释放。程序员需要手动调用 malloc()new 来分配内存,并且需要手动调用 free() 或 delete 来释放内存
    只有堆区存放的数据需要由程序员分配和释放。

堆区存放的,主要是继承了 NSObject 的对象,需要由程序员进行分配和释放。其他非对象类型(int、char、float、double、struct、enum 等)则存放在栈区,由系统进行分配和释放。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        int a = 10; // 栈
        int b = 20; // 栈
        // p : 栈
        // Person 对象(计数器 == 1): 堆
        NSObject *obj = [[NSObject alloc] init];
    }
    // 经过上面代码后, 栈里面的变量 a、b、p 都会被回收
    // 但是堆里面的 Person 对象还会留在内存中,因为它是计数器依然是 1
    return 0;
}

在Objective-C中,对象通常被创建在堆上,而不是栈上。因此,变量 p 存储在栈上,而对象 Person 存储在堆上。

在这段代码中,[NSObject alloc] 会在堆上分配一个 NSObject 对象的内存,并返回该对象的地址。然后,init 方法会初始化该对象,并返回指向该对象的指针。这个指针被赋值给变量 obj,也就是说,obj 存储了该对象在堆上的地址。

因为在Objective-C中,对象的内存需要在程序运行时动态分配和释放,而栈空间是由编译器静态分配的,不能动态调整大小。所以,将对象存储在堆上是更为合适的选择。

2. MRC 手动管理内存(Manual Reference Counting)

2.1 引用计数器

这个就是理解为一个对象被多少人所引用,多少就是引用计数器表达的东西

从字面意义上,可以把引用计数器理解为「对象被引用的次数」,也可以理解为: 「有多少人正在用这个对象」。

系统根据引用计数器的机制来判断对象是否需要被回收。在每次 编译器运行 迭代结束后,都会检查对象的引用计数器,如果引用计数器等于 0,则说明该对象没有地方继续使用它了,可以将其释放掉

2.1.1引用计数器的特点

  • 每个 OC 对象都有自己的引用计数器。
    • 任何一个对象,刚创建的时候,初始的引用计数为 1。
    • 即使用 alloc、new 或者 copy 创建一个对象时,对象的引用计数器默认就是 1。
  • 当没有任何人使用这个对象时,系统才会回收这个对象。也就是说:
    • 当对象的引用计数器为 0 时,对象占用的内存就会被系统回收。
    • 如果对象的引用计数器不为 0 时,那么在整个程序运行过程,它占用的内存就不可能被回收(除非整个程序已经退出)。

2.2 引用计数器操作

为保证对象的存在,每当创建引用到对象需要给对象发送一条 retain 消息,可以使引用计数器值 +1 ( retain 方法返回对象本身)。

当不再需要对象时,通过给对象发送一条 release 消息,可以使引用计数器值 -1。

给对象发送 retainCount 消息,可以获得当前的引用计数器值。

当对象的引用计数为 0 时,系统就知道这个对象不再需要使用了,所以可以释放它的内存,通过给对象发送 dealloc 消息发起这个过程。

需要注意的是:release 并不代表销毁 / 回收对象,仅仅是将计数器 -1。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // 只要创建一个对象默认引用计数器的值就是 1。
        NSObject *p = [[NSObject alloc] init];
        NSLog(@"retainCount = %lu", [p retainCount]); // 打印 1
 
        // 只要给对象发送一个 retain 消息, 对象的引用计数器就会 +1。
        [p retain];
 
        NSLog(@"retainCount = %lu", [p retainCount]); // 打印 2
        // 通过指针变量 p,给 p 指向的对象发送一条 release 消息。
        // 只要对象接收到 release 消息, 引用计数器就会 -1。
        // 只要对象的引用计数器为 0, 系统就会释放对象。
 
        [p release];
        // 需要注意的是: release 并不代表销毁 / 回收对象, 仅仅是将计数器 -1。
        NSLog(@"retainCount = %lu", [p retainCount]); // 1
 
        [p release]; // 0
        NSLog(@"--------");
    }
//    [p setAge:20];    // 此时对象已经被释放 
    return 0;
}
2023-03-28 20:38:47.192790+0800 MRC学习[20085:649748] retainCount = 1
2023-03-28 20:38:47.193075+0800 MRC学习[20085:649748] retainCount = 2
2023-03-28 20:38:47.193090+0800 MRC学习[20085:649748] retainCount = 1
2023-03-28 20:38:47.193101+0800 MRC学习[20085:649748] --------
Program ended with exit code: 0

2.3 dealloc 方法

  • 当一个对象的引用计数器值为 0 时,这个对象即将被销毁,其占用的内存被系统回收。
  • 对象即将被销毁时系统会自动给对象发送一条 dealloc 消息(因此,从 dealloc 方法有没有被调用,就可以判断出对象是否被销毁)
  • 在MRC里一旦重写dealloc方法,必须调用 [super dealloc],并且放在最后面调用。

2.4 野指针和空指针

只要一个对象被释放了,我们就称这个对象为「僵尸对象(不能再使用的对象)」。

当一个指针指向一个僵尸对象(不能再使用的对象),我们就称这个指针为「野指针」。
只要给一个野指针发送消息就会报错(EXC_BAD_ACCESS 错误)。

// 野指针
int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *p = [[NSObject alloc] init]; // 执行完引用计数为 1。
 
        [p release]; // 执行完引用计数为 0,实例对象被释放。
        [p release]; // 此时,p 就变成了野指针,再给野指针 p 发送消息就会报错。
    }
    return 0;
}
  • 为了避免给野指针发送消息会报错,一般情况下,当一个对象被释放后我们会将这个对象的指针设置为空指针。
  • 空指针:
    没有指向存储空间的指针(里面存的是 nil, 也就是 0)。

给空指针发消息是没有任何反应的。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        NSObject *p = [[NSObject alloc] init]; // 执行完引用计数为 1。
 
        [p release]; // 执行完引用计数为 0,实例对象被释放。
        p = nil;
        [p release]; // 此时,p 就变空指针,发消息也不会报错
    }
    return 0;
}


2.5 内存管理的思考方式

内存管理的思考方式有四个基本法则,这四个基本很早之前就学过,再次复习说明真的很重要

  • 自己生成的对象自己持有
  • 非自己生成的对象自己也可以持有
  • 不再需要自己持有的对象就将其释放
  • 非自己持有的对象自己无法释放。

2.5.1 自己生成的对象自己持有

  • 通过 alloc、new、copy 或 mutableCopy 方法创建并持有对象。
  • 当自己持有的对象不再被需要时,必须调用 release 或 autorelease 方法释放对象。
id obj = [[NSObject alloc] init];   // 自己创建的对象,自己持有
[obj release];

而由各类实现的 copyWithZone: 方法和 mutableCopyWithZone: 方法将生成并持有对象的副本。

由以下四种方法名称开头的方法名,也将自己生成并持有对象:

  • allocMyObject
  • newMyObject
  • copyMyObject
  • mutableCopyMyObject

2.5.2 非自己生成的对象自己也可以持有

  • 除了用上面方法(alloc / new / copy / mutableCopy 方法)所取得的的对象,因为非自己生成并持有,所以自己不是该对象的持有者。
  • 通过调用 retain 方法,即便是非自己创建的对象,自己也能持有对象。
  • 同样当自己持有的对象不再被需要时,必须调用 release 方法来释放对象。
//取得的对象存在但不持有
id obj = [NSMutableArray array];
//持有该对象
[obj retain];

2.5.3 不再需要自己持有的对象就将其释放

不再需要自己持有的对象就将其使用release释放。可以释放自己生成的但自己持有对象,也可以释放非自己生成的对象但是自己的持有的对象

//自己持有对象
id obj = [[NSObject alloc] init];
//释放对象
//指向对象的指针仍然被保留在变量obj中,貌似可以访问,但对象一经释放绝对不可访问
[obj release];


id obj = [NSMutableArray array];
//持有该对象
[obj retain];

[obj release];


2.5.4 非自己持有的对象自己无法释放。

如果不是自己持有的对象一定不能进行释放,倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        //非自己持有的对象无法释放,crash
        id obj = [NSMutableArray array];
        [obj release];
    }
    return 0;
}

2.6 @property 参数

基于MRC情况下
在成员变量前加上 @property,系统就会自动帮我们生成基本的 setter / getter 方法,但是不会生成内存管理相关的代码。

@property (nonatomic) int val;


同样如果在 property 后边加上 assign,系统也不会帮我们生成 setter 方法内存管理的代码,仅仅只会生成普通的 getter / setter 方法,默认什么都不写就是 assign。

@property(nonatomic, assign) int val;


如果在 property 后边加上 retain,系统就会自动帮我们生成 getter / setter 方法内存管理的代码,但是仍需要我们自己重写 dealloc 方法。

@property(nonatomic, retain) Room *room;


2.7 自动释放池

Objective-C 提供了 autorelease 方法方便我们控制对于对象的释放时机的把握

  • autorelease 是一种支持引用计数的内存管理方式,只要给对象发送一条 autorelease 消息,会将对象放到一个自动释放池中,当自动释放池被销毁时,会对池子里面的「所有对象」做一次 release 操作。
  • autorelease 方法会返回对象本身,且调用完 autorelease 方法后,对象的计数器不变
void testRetainCount() {
    NSObject *p = [NSObject new];
    p = [p autorelease];
    NSLog(@"count = %lu", [p retainCount]); // 计数还为 1
}

2.7.1 autorelease 的原理实质

autorelease 实际上只是把对 release 的调用延迟了,对于每一个 autorelease,系统只是把该对象放入了当前的 autorelease pool 中,当该 pool 被释放时,该 pool 中的所有对象会被调用 release 方法。

autorelease 是Objective-C中的一种内存管理方式,它使用了自动释放池来延迟对象的释放时间,以避免过早地释放对象导致程序出错。autorelease方法实际上是将对象添加到当前自动释放池中,当自动释放池被销毁时,其中的所有对象都会被释放。

自动释放池是NSAutoreleasePool类的一个实例,它是一个存储autorelease对象的容器。每当一个对象被调用autorelease方法时,它会被添加到最近的自动释放池中。当自动释放池被销毁时,其中的所有对象都会被释放。在Cocoa应用程序中,主循环(RunLoop)会自动创建和释放自动释放池。

2.7.2 autorelease 的注意事项

在iOS开发中,由于ARC(自动引用计数)的出现,程序员不再需要手动管理内存,因此autorelease方法的使用也会相应减少。但在某些情况下,仍然需要手动使用autorelease方法来控制对象的释放时间,比如在使用GCD时,需要将block对象添加到自动释放池中,以避免出现内存泄漏的问题。

并不是放到自动释放池代码中,都会自动加入到自动释放池

@autoreleasepool {
    // 因为没有调用 autorelease 方法,所以对象没有加入到自动释放池
    Person *p = [[Person alloc] init];
    [p run];
}


autorelease 是一个方法,只有在自动释放池中调用才有效。

@autoreleasepool {
}
// 没有与之对应的自动释放池, 只有在自动释放池中调用autorelease才会放到释放池
Person *p = [[[Person alloc] init] autorelease];
[p run];
 
// 正确写法
@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
 }
 
// 正确写法
Person *p = [[Person alloc] init];
@autoreleasepool {
    [p autorelease];
}


2.7.3 autorelease 误区

不要连续调用 autorelease。

@autoreleasepool {
 // 错误写法, 过度释放
    Person *p = [[[[Person alloc] init] autorelease] autorelease];
 }

调用 autorelease 后又调用 release(错误)。

@autoreleasepool {
    Person *p = [[[Person alloc] init] autorelease];
    [p release]; // 错误写法, 过度释放
}


总结

iOS MRC是Objective-C中的手动内存管理机制,即需要程序员手动管理对象的引用计数,通过retain、release、autorelease等方法来控制对象的生命周期。以下是使用MRC进行内存管理的一些基本规则和注意事项:

  • 对象的所有权:程序员需要手动管理对象的所有权,即在需要使用对象时对其进行retain操作,在不需要使用对象时对其进行release操作。
  • 引用计数:每个对象都有一个引用计数,用于记录有多少个指针指向该对象。当引用计数为0时,对象会被释放。
  • 循环引用:当两个对象相互引用时,就会形成循环引用。为避免循环引用,可以使用weak或者assign修饰符来声明指向对象的弱引用。
  • autorelease池:使用autorelease方法可以将对象添加到自动释放池中,延迟对象的释放时间。当自动释放池被释放时,其中的对象会被自动释放。
  • 避免野指针:在对象被释放之后,程序员需要将指向该对象的指针设置为nil,以避免野指针的出现。
  • 内存管理规则:对于一个对象,每次调用retain方法都需要对应一次release方法,每次调用alloc、copy或者new方法都需要对应一次release方法或者autorelease方法。

MRC虽然需要程序员手动管理内存,但是也有其优点,例如可以精确控制对象的生命周期,可以避免一些ARC中的内存管理问题。不过,在实际开发中,为了提高开发效率和代码质量,建议使用ARC进行内存管理。

你可能感兴趣的:(ios,objective-c,xcode)