ARC和MRC是密不可分,ARC是编译器帮助我们自动进行内存管理的模式,在很早之前需要我们手动管理内存和引用计数,这就是MRC模式。
OC(Objective-C)
的MRC(Manual Reference Counting)
是一种手动内存管理模式,它是在ARC(Automatic Reference Counting)
之前的一种内存管理模式。在MRC模式下,开发者需要手动管理对象的内存分配和释放,通过增加和减少对象的引用计数来进行内存管理。
程序在运行的过程中,往往涉及到创建对象、定义变量、调用函数或方法,而这些行为都会增加程序的内存占用。
而一个移动设备的内存是有限的,每个软件所能占用的内存也是有限的。
当程序所占用的内存较多时,系统就会发出内存警告,这时就得回收一些不需要再使用的内存空间。比如回收一些不需要再使用的对象、变量等。
如果程序占用内存过大,系统可能会强制关闭程序,造成程序崩溃、闪退现象,影响用户体验。
所以,我们需要对 「内存」
进行合理的分配内存、清除内存,回收不需要再使用的对象。从而保证程序的稳定性。
在 iOS 中,我们通常将内存分为五大部分
内存指的就是RAM(random access memory),在iOS里面内存分配区域主要分为五个区:栈区(系统管理的地方)、堆区(程序员控制的地方)、静态区(全局区)、常量区、代码区。
代码区:用于存放程序的代码,即 CPU 执行的机器指令,并且是只读的。
全局区 / 静态区:它主要存放静态数据、全局数据和常量。分为未初始化全局区(BSS 段)、初始化全局区:(数据段)。程序结束后由系统释放。
需要注意的是,BSS段中存放的变量都被初始化为0或NULL,因此在程序中无需显式地初始化这些变量。而数据段中的变量在定义时需要显式地赋值。
总的来说,数据段和BSS段都是用来存放程序中的变量和常量的内存区域。其中,数据段存放已经初始化的变量,而BSS段存放未初始化的变量。
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中,对象的内存需要在程序运行时动态分配和释放,而栈空间是由编译器静态分配的,不能动态调整大小。所以,将对象存储在堆上是更为合适的选择。
这个就是理解为一个对象被多少人所引用,多少就是引用计数器表达的东西
从字面意义上,可以把引用计数器理解为「对象被引用的次数」,也可以理解为: 「有多少人正在用这个对象」。
系统根据引用计数器的机制来判断对象是否需要被回收。在每次 编译器运行 迭代结束后,都会检查对象的引用计数器,如果引用计数器等于 0,则说明该对象没有地方继续使用它了,可以将其释放掉
为保证对象的存在,每当创建引用到对象需要给对象发送一条 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
只要一个对象被释放了,我们就称这个对象为「僵尸对象(不能再使用的对象)」。
当一个指针指向一个僵尸对象(不能再使用的对象),我们就称这个指针为「野指针」。
只要给一个野指针发送消息就会报错(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;
}
给空指针发消息是没有任何反应的。
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSObject *p = [[NSObject alloc] init]; // 执行完引用计数为 1。
[p release]; // 执行完引用计数为 0,实例对象被释放。
p = nil;
[p release]; // 此时,p 就变空指针,发消息也不会报错
}
return 0;
}
内存管理的思考方式有四个基本法则,这四个基本很早之前就学过,再次复习说明真的很重要
id obj = [[NSObject alloc] init]; // 自己创建的对象,自己持有
[obj release];
而由各类实现的 copyWithZone: 方法和 mutableCopyWithZone: 方法将生成并持有对象的副本。
由以下四种方法名称开头的方法名,也将自己生成并持有对象:
allocMyObject
newMyObject
copyMyObject
mutableCopyMyObject
//取得的对象存在但不持有
id obj = [NSMutableArray array];
//持有该对象
[obj retain];
不再需要自己持有的对象就将其使用release释放。可以释放自己生成的但自己持有对象,也可以释放非自己生成的对象但是自己的持有的对象
//自己持有对象
id obj = [[NSObject alloc] init];
//释放对象
//指向对象的指针仍然被保留在变量obj中,貌似可以访问,但对象一经释放绝对不可访问
[obj release];
id obj = [NSMutableArray array];
//持有该对象
[obj retain];
[obj release];
如果不是自己持有的对象一定不能进行释放,倘若在应用程序中释放了非自己所持有的对象就会造成崩溃。
int main(int argc, const char * argv[]) {
@autoreleasepool {
//非自己持有的对象无法释放,crash
id obj = [NSMutableArray array];
[obj release];
}
return 0;
}
基于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;
Objective-C 提供了 autorelease 方法方便我们控制对于对象的释放时机的把握
void testRetainCount() {
NSObject *p = [NSObject new];
p = [p autorelease];
NSLog(@"count = %lu", [p retainCount]); // 计数还为 1
}
autorelease 实际上只是把对 release 的调用延迟了,对于每一个 autorelease,系统只是把该对象放入了当前的 autorelease pool 中,当该 pool 被释放时,该 pool 中的所有对象会被调用 release 方法。
autorelease
是Objective-C中的一种内存管理方式,它使用了自动释放池来延迟对象的释放时间,以避免过早地释放对象导致程序出错。autorelease方法实际上是将对象添加到当前自动释放池中,当自动释放池被销毁时,其中的所有对象都会被释放。
自动释放池是NSAutoreleasePool
类的一个实例,它是一个存储autorelease
对象的容器。每当一个对象被调用autorelease方法时,它会被添加到最近的自动释放池中。当自动释放池被销毁时,其中的所有对象都会被释放。在Cocoa应用程序中,主循环(RunLoop)会自动创建和释放自动释放池。
在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];
}
不要连续调用 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进行内存管理的一些基本规则和注意事项:
MRC虽然需要程序员手动管理内存,但是也有其优点,例如可以精确控制对象的生命周期,可以避免一些ARC中的内存管理问题。不过,在实际开发中,为了提高开发效率和代码质量,建议使用ARC进行内存管理。