本文介绍: Objective-C提供了两种内存管理机制MRC(MannulReferenceCounting)和ARC(AutomaticReferenceCounting),为Objective-C提供了内存手动自动管理。下面我们来探讨一下MRC和ARC的实现

Objective-C提供了两种内存管理机制MRC(Mannul Reference Counting)和ARC(Automatic Reference Counting),为Objective-C提供了内存的手动自动管理。下面我们来探讨一下MRC和ARC的实现

OC的内存管理方式

1.自己生成对象自己持有

iOS内存管理中,有四个这样的关键字newalloccopymutableCopy,如果自身使用这些关键字时候来产生对象,那么创建完之后,自身也就有了对象

// 使用alloc分配了内存,obj指向对象,该对象本身引用计数为1,不需要retain
id obj = [[NSObject alloc] init];
// 使用new分配了内存,objc指向对象,该对象本身引用计数为1,不需要retain
id obj = [NSObject new];

2.非自己生成的对象,自己也能持有

// NSMutableArray通过方法array产生了对象(并没有使用allocnewcopymutableCopt来产生对象),因此该对象不属于obj自身产生的
// 因此,需要使用retain方法让对象计数器+1,从而obj可以持有该对象(尽管该对象不是他产生的)
id obj = [NSMutableArray array];
[obj retain];    

3.不再需要自己持有的对象时释放

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

3.无法释放非自己持有的对象

id obj = [NSMutableArray array]; 
// 释放一个属于自己的对象
// obj没有进行retain操作而进行release操作然后autoreleasePool也会对其进行一次release操作,导致奔溃。
[obj release];

MRC实现原理

基本思想:通过手动引用计数来进行对象的内存管理

涉及方法

autorelease

autorelease即“自动释放”,是OC的一种内存自动回收机制,可以将一些临时变量通过自动释放池来回收统一释放。自动释放池销毁的时候,池子里面所有的对象都会做一次release操作在这里插入图片描述
那么,autorelease释放与简单的release释放有什么区别呢?

调用 autorelease 方法,就会把该对象放到离自己最近的自动释放池中(栈顶的释放池,多重自动释放池嵌套是以栈的形式存取的),即:使对象的持有权转移给了自动释放池(即注册到了自动释放池中),调用方拿到了对象,但这个对象还不被调用方所持有。当自动释放池销毁时,其中的所有的对象都会调用一次release操作。
在这里插入图片描述
本质上,区别在于autorelease 方法不会改变调用者的引用计数,它只是改变了对象释放时机,不再让程序员负责释放这个对象,而是交给自动释放池去处理

autorelease 方法相当于把调用者注册到 autoreleasepool 中,ARC环境下不能显式地调用 autorelease 方法和显式创建 NSAutoreleasePool 对象,但可以使用@autoreleasepool { }块代替(并不代表块中所有内容都被注册到了自动释放池中)。

对于所有调用过autorelease实例方法的对象,在废弃NSAutoreleasePool对象时,都将调用release实例方法。
在这里插入图片描述
eg

int main(int argc, const char * argv[]) {
        NSAutoreleasePool *pool = [[NSAutoreleasePool alloc]init];
        id obj = [[NSObject alloc]init];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [obj autorelease];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
        [pool drain];
        NSLog(@"%lu",(unsigned long)[obj retainCount]);
    return 0;
}

输出
请添加图片描述
自动释放池使用场景

关于retainCount

我们在MRC中,有时可能会想要打印引用计数,但retainCount方法并不是很有用,由于对象可能会处于自动释放池中,这会导致打印的引用计数并不精准,而且其他程序库也很有可能自行保留或释放对象,这都会扰乱引用计数的具体值。

同时,retainCount也存在过大的问题看看demo

int main(int argc, const char * argv[]) {
    NSString *firstString = @"你好";
    NSString *secondString = [NSString stringWithFormat:@"hello"];
    NSNumber *num1 = @2;
    NSNumber *num2 = @100;
    
    NSLog(@"%lu,%lu,%lu,%lu",(long)[firstString retainCount],(long)[secondString retainCount],(long)[num1 retainCount],(long)[num2 retainCount]);
    return 0;
}

请添加图片描述
系统会尽可能把NSString实现单例对象。如果字符串例子上的那样,是一个编译常量,那么就可以这样来实现了。在这种情况下,编译器会把NSString对象所表示数据放在应用程序二进制文件里,这样的话,运行程序时就可以直接用了,无需再创建NSString对象。

NSNumber也类似,它使用了一种叫做“标签指针”的概念标注指定类型数值。这种做法不使用NSNumber对象,而是把与数值有关的全部消息都放在指针值里。运行系统会在消息派发期间检测到这种标签指针,并对它执行相应操作,使其行为看上去和真正的NSNumber对象一样。

对于上述所说的单例对象,其保留计数绝对不会改变。这种对象的release和retain操作都是“空操作”。

ARC

内存管理方案

iOS内存管理方案有:

修饰符

当ARC有效时,id类型和对象类型必须附加所有权修饰符,一共有如下四种。

__strong修饰

__strong修饰符是id类型和对象类型默认所有权修饰符。

id obj  = [[NSObject alloc] init];
  
//在没有明确指定所有权修饰符时,默认__strong
id __strong obj = [[NSObject alloc] init];

不论调用哪种方法,强引用修饰变量会持有该对象,如果已经持有则引用计数不会增加。
__strong修饰符表示对对象的强引用。持有强引用的变量超出作用域时被废弃,随着强引用的失效,引用的对象会随之释放。

__strong对象相互赋值

__strong修饰符的变量不仅只在变量作用域中,在赋值上也能够正确的管理其对象的所有者

id __strong obj0 = [[NSObject alloc] init];//生成对象A			
id __strong obj1 = [[NSObject alloc] init];//生成对象B		
id __strong obj2 = nil;
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)	
obj1 = nil;//obj1不再强引用对象B	
obj0 = nil;//obj0不再强引用对象B	
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃

过程如下
在这里插入图片描述

__strong循环引用带来的内存泄漏

Test类中一个强引用类型的成员变量obj_,设置set方法。

@interface Test : NSObject {
    id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end

#import "Test.h"

@implementation Test
- (id)init {
    self = [super init];
    return self;
}

- (void)setObject:(id __strong)obj {
    obj_ = obj;
}
@end

声明两个对象test0和test1,他们内部也有自己的成员变量obj_。

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        id test0 = [[Test alloc] init];//生成TestA
        id test1 = [[Test alloc] init];//生成TestB
        [test0 setObject:test1];
        [test1 setObject:test0];
    }
    return 0;
}

如果通过set方法给两个对象的成员变量分别赋值一个对象所持有的TestA/TestB对象,就会造成如下结果
test0 持有 TestA,test0.obj 持有 TestB,test1 持有 TestB,test1.obj 持有 TestA。请添加图片描述
由于两个对象互相强引用导致内存泄露,内存泄漏就是应当废弃的对象在超出生命周期后继续存在

如何解决这个问题呢?接下来看__weak修饰符。

__weak修饰符

弱引用表示并不持有对象,当所引用的对象销毁了,这个变量就自动设为nil。
可以利用__weak修饰符来解决循环引用问题。

@interface Test : NSObject {
    id __weak obj_;
}
- (void)setObject:(id __strong)obj;
@end

@implementation Test
- (id)init {
    self = [super init];
    return self;
}

- (void)setObject:(id __strong)obj {
    obj_ = obj;
}
@end

使用__weak之后,test0持有对象A,test1持有对象B,而test0的object和test1的object并不持有对象A和对象B,超出作用域后,就没有变量再持有对象A和对象B,对象A和对象B就会被释放,就解决了循环引用的问题。

__unsafe_unretained修饰符

__unsafe_unretained__weak很像,唯一区别就是,__unsafe_unretained变量引用的对象再被销毁以后,不会被自动设置为nil,仍然指向对象销毁前的内存地址。所以它的名字叫做unsafe,此时你再尝试通过变量访问这个对象的属性或方法就会crash。一旦对象释放,则会成为悬垂指针,程序崩溃,因此__unnsafe_unretained修饰符的变量一定要在赋值的对象存在的情况下使用。

__autoreleasing修饰符

ARC无效时使用autorelease,在ARC下__autoreleasing的使用:

@autoreleasepool {
	id __autoreleasing obj = [[NSObject alloc] init];
}

在这里插入图片描述
@autoreleasepool块即相当于上文的NSAutoreleasePool类生成、持有及废弃。
附有__autoreleasing修饰符相当于变量调用了autorelease方法。
以下为使用__weak修饰符的例子,虽然__weak修饰符是为了避免循环引用而使用的,但在访问附有__weak修饰符的变量时,实际上必定要访问注册到AutoreleasePool的对象。

id __weak obj_= obj;
NSLog(@"%@", [obj_ class]);
//等价于:
id __weak obj_ = obj;
id __autoreleasing tmp = obj_;
NSLog(@"%@", [tmp class]);

具体ARC规则

id obj = [[NSObject alloc] init];
void *p = (__bridge void *)obj;

ARC的工作原理

ARC 的工作原理大致是这样:当我们编译源码的时候,编译器分析源码每个对象的生命周期,然后基于这些对象的生命周期,在合适的地方添加相应的引用计数操作代码retain, releaseautorelease

具体一点就是, 前端编译器会为“拥有的”每一个对象插入相应的release语句。如果对象的所有权修饰符是__strong,那么它就是被拥有的。如果在某个方法内创建了一个对象(局部变量),前端编译器会在方法末尾自动插入release语句以销毁它。而类拥有的对象(实例变量/属性)会在dealloc方法内被释放。事实上,你并不需要dealloc方法或调用父类dealloc方法,ARC会自动帮你完成一切。此外,由编译器生成的代码甚至会比你自己写的release语句性能还要好,因为编辑器可以作出一些假设。在ARC中,没有类可以覆盖release方法,也没有调用它的必要。ARC会通过直接使用objc_release优化调用过程。而对于retain也是同样的方法。ARC会调用objc_retain来取代保留消息

ARC 是工作在编译期的一种技术方案,这样的好处是:

  1. 编译之后,ARC 与非 ARC 代码没有什么差别的,所以二者可以在源码中共存。实际上,你可以通过编译参数 -fnoobjcarc关闭部分源代码的 ARC 特性
  2. 相对垃圾回收这类内存管理方案,ARC 不会带来运行时的额外开销,所以对于应用的运行效率不会有影响。相反,由于 ARC 能够深度分析一个对象的生命周期,它能够做到比人工管理引用计数更加高效。例如在一个函数中,对一个对象刚开始有一个引用计数 +1的操作,之后又紧接着有一个 -1 的操作,那么编译器就可以把这两个操作都优化掉。

Q&A

ARC在编译期和运行期做了什么

  1. 在编译期,ARC会把互相抵消的retain、release、autorelease操作约简。
  2. ARC包含有运行期组件,可以在运行期检测到autorelease和retain这一对多余的操作。为了优化代码,在方法中返回自动释放的对象时,要执行一个特殊函数

我还需要为我的对象编写 dealloc 方法吗?

有时候需要。
因为ARC不会自动处理malloc/free、Core Foundation对象的生命周期管理、文件描述符等等,所以你仍然可以通过编写dealloc方法来释放这些资源
你不必(实际上不能)释放实例变量,但可能需要对系统类和其他未使用ARC编写代码调用[self setDelegate:nil]
ARC下的dealloc方法中不需要且不允许调用[super dealloc],Runtime会自动处理。

ARC它将 retains/releases 调用的代码放在哪了?

尝试不要去思考ARC将retains/releases调用的代码放在哪里,而是思考应用程序算法,思考对象的strong和weak指针、所有权、以及可能产生的循环引用。

ARC 中仍然可能存在循环引用吗?

是的,ARC自动retain/release,也继承了循环引用问题。幸运的是,迁移到ARC的代码很少开始泄漏,因为属性已经声明是否retain。

原文地址:https://blog.csdn.net/LANGJ1/article/details/125956165

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任

如若转载,请注明出处:http://www.7code.cn/show_22486.html

如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱suwngjj01@126.com进行投诉反馈,一经查实,立即删除

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注