【iOS 开发-ARC规则】

前言

对于现在的Xcode,开发者不需要手动的管理内存和对象的引用计数,自从引入了ARC模式之后,我们不需要过多的关注内存方面,但是对于iOS的对象的引用计数和内存管理的了解是必要的,本篇博客记录ARC的深入学习了解。

ARC

ARC(Automatic Reference Counting)是指内存管理中对引用采取自动计数的计数

在Objective-C中采用Automatic Reference Counting(ARC)机制,让编译器来进行内存管理。在新一代Apple LLVM编译器中设置ARC为有效状态,就无需再次键入retain或者release代码,这就降低程序崩溃、内存泄露等风险的同时,很大程度上减少了开发程序的工作量。编译器完全清除目标对象,并能立刻释放那些不再被使用的对象。如此一来,引用程序具有可预防性,且能流畅运行,速度也将大幅提升。

  • 现在的Xcode默认是ARC模式开启,关闭ARC操作很简单,在Buliding Settings选择找到AutoR这个选项,设置为NO

请添加图片描述

ARC的思考方式

前言的博客复习了引用计数,需要强调的是ARC的思考方式。如何理解ARC关键就在这。

  • 自己生成的对象自己所持有
  • 非自己生成的对象自己也能持有
  • 不再需要自己持有的对象时释放
  • 非自己持有的对象无法释放

具体的对于对象的操作
请添加图片描述

alloc方法和其他生成并持有的方法存在区别,他属于类方法,其他都是实例方法

Clang

既然要探究ARC的本质和深入学习,那么就需要学会查看代码是如何由编译器实现的.

找到对应的二级文件 cd文件地址,输入下面的代码,就出现了main.cpp方法

clang -rewrite-objc4 main.m

请添加图片描述

请添加图片描述

他的内部实现是C++实现的,接下来根据一段代码对ARC进行分析。

void dftFun() {
    id obj = [[NSObject alloc] init];
    NSLog(@"%@", obj);
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {
        // insert code here...
        NSLog(@"Hello, World!");
        dftFun();
    }
    return 0;
}

请添加图片描述
主要对dftFun函数进行分析

objc_storeStrong

void dftFun() {
    id obj = ((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)((NSObject *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("NSObject"), sel_registerName("alloc")), sel_registerName("init"));
    
}
  • 查询了一些博客发现我们的源码是objc2 ,但现在已经更新到了objc4,所以上述代码不作揖解释,参考大佬博客分析该过程
void defaultFunction() {
 
	id obj = obj_msgSend(NSObject, @selector(new));
 
	objc_storeStrong(obj, null);
 
}
    • obj_msgSend主要用于消息发送,就是告诉编译器我们要初始化这个对象
    • obj_msgSend(NSObject, @selector(new))就是新建一个对象,而objc_storeStrong是 objc4 库中的方法,具体逻辑如下:
// strong
void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

其中涉及到了block里看到的类似 objc_retainobjc_release,其实就是持有和释放对象的过程

  • 检查输入的 obj 地址 和指针指向的地址是否相同。
  • 持有对象,引用计数 + 1 。objc_retain(obj)
  • 指针指向 obj。
  • 原来指向的对象引用计数 - 1。objc_release(prev)

isa指针结构体

在分析 ARC 相关源码之前,需要对 isa 有一定了解,其中存储了一些非常重要的信息,下面是 isa 的结构组成
请添加图片描述

union isa_t 
{
    Class cls;
    uintptr_t bits;
    struct {
         uintptr_t nonpointer        : 1;//->表示使用优化的isa指针
         uintptr_t has_assoc         : 1;//->是否包含关联对象
         uintptr_t has_cxx_dtor      : 1;//->是否设置了析构函数,如果没有,释放对象更快
         uintptr_t shiftcls          : 33; // MACH_VM_MAX_ADDRESS 0x1000000000 ->类的指针
         uintptr_t magic             : 6;//->固定值,用于判断是否完成初始化
         uintptr_t weakly_referenced : 1;//->对象是否被弱引用
         uintptr_t deallocating      : 1;//->对象是否正在销毁
         uintptr_t has_sidetable_rc  : 1;//1->在extra_rc存储引用计数将要溢出的时候,借助Sidetable(散列表)存储引用计数,has_sidetable_rc设置成1
        uintptr_t extra_rc          : 19;  //->存储引用计数
    };
};

其中nonpointer、weakly_referenced、has_sidetable_rcextra_rc都是 ARC 有直接关系的成员变量

objc_object

为什么要引入isa?

因为在OC 中每一个对象都是一个结构体,结构体都包含了一个isa 成员变量
在运行时,类的对象被定义为objc_object 类型,就是对象结构体

objc_object就是isa结构体封装而成的

struct objc_object {
    isa_t isa;
};

从下面代码可以知道,objc_object就是 isa 基础上一层封装。

struct objc_class : objc_object {
    isa_t isa; // 继承自 objc_object
    Class superclass;
    cache_t cache; // 方法实现缓存和 vtable
    class_data_bits_t bits; // class_rw_t * plus custom rr/alloc flags
};
  • 因为OC的每一个对象都是一个结构体,并且是Objc_object的子类, 所以OC的类
    Objc_Class的结构和Objc_object类似。
    • isa:objc_object 指向类,objc_class 指向元类。
    • superclass:指向父类。
    • cache:存储用户消息转发优化的方法缓存和 vtable 。这个在小蓝书有了解过,是可以利用缓存优化程序的执行速度和内存占用机制
    • bitsclass_rw_tclass_ro_t保存了方法、协议、属性等列表和一些标志位。

ARC 规则

上面的介绍是对ARC的基础理解,了解了每一个对象结构体的基本组成isa_t结构体的引入为了下文的探究作揖准备。

所有权修饰符

OC编程为了处理对象,可将变量类型定义为包含id类型在内的各种类型

对象类型:就是指向NSObject这样的OC类的指针,NSObject *, id类型则可以隐藏对象类型的类名部分, 和void * 一样

ARC有效的时候,id必须附加所有权修饰符,一共四种

__strong修饰符

_strong修饰符是id类型和对象类型默认的所有权修饰符

在 MRC 时代 Retain 修饰符将会使被引用的对象引用计数 + 1 ,在 ARC 中 __strong 修饰符作为其替代者,不论调用哪种方法,强引用修饰的变量会持有该对象,如果已经持有则引用计数不会增加

__strong修饰符是id类型和对象类型默认添加的修饰符,如下的代码

id obj = [NSObject new];

等同于

id __strong obj1 = [NSObject new];

强引用对象的所有者和对象的生命周期

  • 持有强引用的变量在超出其作用域时被废弃
  • 随着强引用的失效
  • 引用的对象会随之释放
    请添加图片描述
非自己生成但是持有的对象

上面讲的是自己生成并持有的对象的生命周期,那么非自己生成但是持有的对象声明周期如何呢请添加图片描述

__strong修饰符变量相互赋值

通过__strong变量间的互相引用理解废弃和引用

id __strong obj0 = [[NSObject alloc] init];//生成对象A			
id __strong obj1 = [[NSObject alloc] init];//生成对象B		
id __strong obj2 = nil;
  • 生成对象A B
    请添加图片描述
  • obj2引用对象B
obj0 = obj1;//obj0强引用对象B;而对象A不再被ojb0引用,被废弃
obj2 = obj0;//obj2强引用对象B(现在obj0,ojb1,obj2都强引用对象B)

请添加图片描述

  • obj0-obj2全部nil,所以B没有任何对象强引用,B被废弃
obj1 = nil;//obj1不再强引用对象B
obj0 = nil;//obj0不再强引用对象
obj2 = nil;//obj2不再强引用对象B,不再有任何强引用引用对象B,对象B被废弃

请添加图片描述

  • 我们可以理解为强引用修饰符就是持有者的转变
__strong用在方法参数容易循环引用
  • __strong可以修饰方法参数,但是一般不这么做的原因如下,我们创建一个Test类
    里面有一个obj_变量
@interface TestClass : NSObject {
    id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end
  • 重写set方法
#import "TestClass.h"

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

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

  • 在主函数用TestClass生成两个对象Ta, Tb被t0,t1引用。
 id test0 = [[TestClass alloc] init];//生成TestA
 id test1 = [[TestClass alloc] init];//生成TestB

通过set方法给 两个对象的成员变量分别赋值另一个对象所持有的TestA/TestB对象

  • test0 持有 TestA
  • test0.obj 持有 TestB
  • test1 持有 TestB
  • test1.obj 持有 TestA
[test0 setObject:test1];
 [test1 setObject:test0];
  • 书上的解释
    请添加图片描述
    请添加图片描述

这时候发现如果想要尝试废弃testa, 需要test0 和test1.obj置空。因为成员变量的生命周期是与对象同步的 然而废除test1.obj需要废除test1,也就是废除testb如此一来重复上述操作,循环引用,也就是强引用容易造成循环引用

请添加图片描述

对象自身的强引用也会造成循环引用

请添加图片描述

  id t1 = [[TestClass alloc] init];
        [t1 setObject:t1];
循环引用

循环引用容易发生内存泄漏,因为对象在超出生命周期之后应该被废弃,但是还是继续存在了。

__weak修饰符

避免循环引用出现了__weak修饰符,他也是四大所有权修饰符之一。

__weak 弱引用 弱引用不能持有对象实例。

 id __weak obj = [NSObject new];

请添加图片描述
将保留对象分配给弱变量,因为不自己持有对象,对象将在分配后释放.

可以将对象赋值给__strong修饰的变量之后再次赋值给__weak修饰符变量即可

  id __strong obj1 = [NSObject new];
        id __weak obj = obj1;

也就是obj变量成了持有对象的弱引用,达到了效果

__weak避免弱引用

同理,可以在刚才的循环引用部分加入__weak修饰符修饰使变量在超出生存区域的时候被释放

请添加图片描述

空弱应用

__weak持有某对象的弱引用时候,该对象被废弃,该弱引用将自动失效且处于nil被赋值状态,叫做空弱引用

 id __weak obj = nil;
        {
            id __strong obj1 = [NSObject new];
            obj = obj1;
            NSLog(@"%@", obj);
        }
        NSLog(@"%@", obj);
      

请添加图片描述

废弃对象的同时,持有该弱引用的obj1变量的弱引用失效,nil赋值给obj1.

__unsafe_unretained修饰符

__unsafe_unretained修饰符正如其名unsafe所示,是不安全的所有权修饰符。

附有__unsafe_unretained修饰符的变量不属于编译器的内存管理对象。

垂悬指针

和__weak一样,不能持有自己直接生成的对象,所以生成的对象也会被自己释放。

同样的代码,换成 __unsafe_unretained

  id __unsafe_unretained obj2 = nil;
        {
            id __strong obj1 = [NSObject new];
            obj2 = obj1;
            NSLog(@"1%@", obj2);
        }
        NSLog(@"2%@", obj2);![请添加图片描述](https://img-blog.csdnimg.cn/491c31bb07884096ba9273881d637e15.png)

代码直接崩溃了,去掉最后一行

  id __unsafe_unretained obj2 = nil;
        {
            id __strong obj1 = [NSObject new];
            obj2 = obj1;
            NSLog(@"1%@", obj2);
        }
   

请添加图片描述

  • 并没有崩溃,按理说最后一行的打印应该是null,但是程序崩溃了。
    查看汇编
    请添加图片描述
    发现在 NSLog(@"2%@", obj2)之后代码崩溃,结果obj2指向了指针存在,指向了不存在的对象。
    正常应该是
    请添加图片描述
  • 解释
    • weak 修饰的指针变量,在指向的内存地址销毁后自动置为 nil。
    • _Unsafe_Unretain 不会置为 nil,容易出现 悬垂指针,发生崩溃。但是 _Unsafe_Unretain__weak 效率高。
      悬垂指针 指针指向的内存已经被释放了,但是指针还存在,这就是一个 悬垂指针 或者说 野指针。

在使用__unsafe_unretained修饰符时,赋值给附有__strong修饰符的变量时有必要确保被赋值的对象确实存在,如果不存在,那么程序就会崩溃

__autoreleasing修饰符

  • 在ARC有效的时候autorelease无效,也就是不能使用autorelease方法,但是在ARC有效的时候autoreleasing是起作用的。
    在MRC下调用autorelease和ARC下的操作如图
    请添加图片描述
    在ARC有效的时候,@autorelease块代替NSAutoreleasPool类,附有__autoreleasing的修饰符变量代替autorelease方法。

自动调用

编译器会检查方法名是否以alloc/new/copy/mutableCopy开始,如果不是讲自动将返回值的对象注册到autoreleasepool

和__strong一样,下列情况不使用__autoreleasing也自动把对象注册到自动释放池里

+ (id) array {
	return [[NSMutableArray alloc]init];
}

如下:
+ (id) array {
	id obj = [[NSMutableArray alloc]init];
	return obj;
}

return使得对象变量超出其作用域,所以该强引用对应的自己持有的对象会被自动释放,但该对象作为函数的返回值,编译器也会自动注册到自动释放池

在访问附有__weak修饰符的变量的时候,实际上必定要访问到autoreleasepool对象,__weak是对于某个对象的弱引用,而对象有可能在超出自己的作用域的时候被废弃了,所以访问到autoreleasepool为了保证对象存在到我们需要的时候

显式的使用__autoreleaseing修饰符的时候,对象必须要为自动变量(局部变量,函数,方法参数。

不论ARC有效或者无效,都推荐使用pool上的对象,因为Runloop等实现不论ARC有效还是无效,均能随时释放注册到pool中的对象

规则

ARC需要遵守如下规则

  • 不能使用retain/release/retainCount/autorelease
  • 不能使用NSAllocateObject/NSDeallocateObject
  • 必须遵守内存管理的方法名规则
  • 不要显式调用dealloc
  • 使用@autorelease块代替NSAutoreleasePool
  • 不能使用区域(NSZone)
  • 对象型变量不能作为C语言结构体的成员
  • 显式转换id和void*

前面两条就是之前所了解过

必须遵守内存管理的方法名规则

ARC无效的时候,用于对象生成的持有的方法必须遵守以下命名规则
alloc new copy mutableCopy等上述名称方法返回对象必须返回给调用方应当持有的对象,ARC有效和上述一样,但是init需要注意

以init开始的方法规则要比上述严格,该方法必须是实例方法并且必须返回对象,返回的类型为方法的声明类型,超类或子类,该方法返回对象不注册到autoPool里。

- (id) initWithObject:(id) obj;上述方法遵守规则并返回了对象
- (void) initThisObject;没有返回对象不允许使用
- (void) initialize 之前说过,这个方法不属于上述行列,是在对象初始化之前必须调用的,不需要遵守上述规则
不要显式调用dealloc

dealloc无法释放不属于该对象的一些东西,需要我们重写时加上去,例如

  • 通知的观察者,或KVO的观察者

  • 对象强委托/引用的解除(例如XMPPMannerger的delegateQueue)

  • 做一些其他的注销之类的操作(关闭程序运行期间没有关闭的资源)

ARC无效的时候调用需要[super dealloc]
ARC有效的时候我们需要记述废弃对象时候我们需要的处理

显示的转化“id”和“void*”

ARC无效的时候我们可以转化“id”和“void*”和调用一些实例方法


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

ARC有效的时候这样是不行的请添加图片描述
在ARC有效的时候 id 型或对象型变量赋值给void * 或者逆向赋值时都需要进行特定的转换。如果只想单纯地赋值,则可以使用 “__bridge转换”。

- (void)OCAndVoidUse__bridgeInARC {
    
    id obj = [[NSObject alloc] init];
    
    void *p = (__bridge void *)obj;
    
    id o = (__bridge id)(p);
}

但是转换为 void * 的 __bridge转换,安全性与赋值给 __unsafe_unretained修饰符相近,甚至会更低 。如果管理时不注意赋值对象的所有者,就会 因悬垂指针而导致程序崩溃 。

此时P不持有对象 __bridge并不会改变持有情况。

__brideg总结
在这里插入图片描述

属性

这里简单介绍一下属性声明表
ARC有效的时候,属性声明使用的属性来用,代替作用

请添加图片描述
copy方法是赋值给copy with zone方法的复制出来的的对象,需要注意。

总结

ARC的规则是针对MRC和ARC下不同的总结,更多的是表面的方法需要注意的东西,ARC的实现则是探讨到了底层的代码,需要更加详细的学习,二者结合更能理解ARC,先学会表面方法的的规则,在去深入学习

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值