【iOS】OC高级编程 iOS多线程与内存管理阅读笔记——自动引用计数(二)

自动引用计数

前言

上一篇我们主要学习了一些引用计数方法的内部实现,现在我们学习ARC规则。


ARC规则

所有权修饰符

OC中,为了处理对象,可以将变类型定义为id类型或各种对象类型。

对象类型: 即OC类的指针,例如"NSObject* "
id类型: 用于隐藏对象类型的类名部分,相当于C语言中的(void *)

ARC有效时,id类型和对象类型同C语言其他类型不同,必须附加上所有权修饰符

  • __strong修饰符
  • __weak修饰符
  • __unsafe_unretained修饰符
  • __autoreleasing修饰符

__strong修饰符

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

objectivec 复制代码
id obj = [[NSObject alloc] init];

id __strong obj = [[NSObject alloc] init];

这两种代码是一样的。

但是,当ARC无效时,该如何实现__strong修饰符呢。

objectivec 复制代码
{
	id obj = [[NSObject alloc] init];

	[obj release]
}

如上述代码所示,附有__strong修饰符的变量obj在超出其变量作用域时,即在该变量被废弃时,会释放其被赋予的对象。

因此,我们可以通过在最后调用release代码,实现这一功能。

如"strong"所示,__strong修饰符表示对对象的"强引用"。持有强引用的变量在超出其作用域时废弃。随着强引用的失效,引用的对象会随之释放。

对于自己生成并持有对象的源代码来说,对象的所有者和对象的生存周期都是明确的,那么如果是取得非自己生成并持有的对象呢。

objectivec 复制代码
{
	id__strong obj = [NSMutableArray array];
}

这里我们通过NSMutableArray类的array类方法学习。

objectivec 复制代码
{
	//取得非自己生成并持有的对象
	
	id __strong obj = [NSMutableArray array];

	//变量obj为强引用,所以自己持有对象。
}
//变量obj超出其作用于,强引用失效,自动释放自己持有的对象。

可见取得非自己生成但是持有的对象的生存周期也是明确的

即使是OC类成员变量,也可以在方法参数上,使用附有__strong修饰符的变量。

objectivec 复制代码
@interface Test : NSObject
{
    id __strong obj_;
}
- (void)setObject:(id __strong)obj;
@end

@implementation Test
- (id)init
{
    self = [super init];
    return self;
}
- (void)setObject:(id __strong)obj
{
    obj_ = obj;
}
@end

下面我们进行使用:

objectivec 复制代码
{
	id __strong test = [[Test alloc] init];
	//test持有Test对象的强引用

	[test setObject:[[NSObject alloc] init];
	//Test对象的obj_成员,持用NSObjcet对象的强引用。
}
/*
	因为test变量超出其作用域,强引用失效
	所以自动释放Test对象。
	Test对象的所有者不存在,因此废弃该对象。

	废弃Test对象的同时,Test对象的obj_成员也被废弃,
	NSObjcet对象的强引用失效
	自动释放NSObjcet对象
	所有者不存在,废弃该对象。

*/

通过这种方法,无需额外工作便可以使用于类成员变量以及方法参数中。

修饰符可以保证将附有这些修饰符的自动变量初始化为nil。

objectivec 复制代码
id __strong ojb0;
//这两种初始化方式相同
id __strong obj0 == nil;

通过__strong修饰符,不必再次键入retain或者release即可实现OC内存管理的思考方式。

并且,id类型和对象类型的所有权修饰符默认为__strong修饰符,所以不需要写上"__strong"。这一设定使得ARC有效以及简单的编程遵循了OC内存管理的思考方式。

__weak修饰符

如果仅使用__strong修饰符,容易发生循环引用的问题,这对项目是毁灭性的。

如以下这种情况:

objectivec 复制代码
{
	id test0 = [[Test alloc] init];
	//test0持有Test对象A的强引用
	
	id test1 = [[Test alloc] init];
	//test1持有Test对象B的强引用

	[test0 setObject:test1];
	/*
	Test对象A的obj_成员变量持用Test对象B的引用
	此时,持有Test对象B的强引用的变量为
	Test对象A的obj_和test1。
	*/

	[test1 setObject:test0];
		/*
	Test对象B的obj_成员变量持用Test对象A的引用
	此时,持有Test对象A的强引用的变量为
	Test对象B的obj_和test0。
	*/
}

/*
因为 test0 变量超出其作用域,强引用失效,
所以自动释放 Test 对象 A。
因为 test1 变量超出其作用域,强引用失效,
所以自动释放 Test 对象 B。
此时,持有 Test 对象 A 的强引用的变量为
Test 对象 B 的 obj_。
此时,持有 Test 对象 B 的强引用的变量为
Test 对象 A 的 obj_。
发生内存泄漏!
*/

如下图所示:

循环引用容易发生内存泄漏:即应当废弃的对象在超出其生存周期后继续存在。

上述代码分别将对象A赋给test0,对象B赋给test1后,在超出作用域后无法正确被释放。

为了避免以上这种情况,我们可以采用__weak修饰符。

__weak修饰符:提供弱引用,不能持有对象实例。

objectivec 复制代码
id __weak obj = [[NSObject alloc] init];

会出现以下警告。

变量 obj 持有对持有对象的弱引用。因此,为了不以自己持有的状态来保存自己生成并持有的对象,生成的对象会立即被释放。

如果使用以下代码,将对象赋值给附有__strong修饰符的变量之后,在赋值附有__weak修饰符的变量,就不会发生警告。

objectivec 复制代码
{
	//自己生成并且持有对象
	id __strong obj0 = [[NSObject alloc] init];
	//obj0变量为强引用,所以自己持有对象
	
	id __weak obj1 = obj2;
	//obj1变量持有生成对象的弱引用
}
//因为obj0变量超出其作用域,强引用失效,所以自动释放自己持有的对象
//因为对象的所有者不在,所以会自动废弃obj1

因此上述代码只需要将可能发生循环引用的类成员变量改成附有__weak修饰符的成员变量,即可避免循环引用的问题。如下修:

objectivec 复制代码
@interface Test : NSObject
{
    id __weak obj_;
}
- (void)setObject:(id __strong)obj;
@end

此时对象引用情况如图所示:

__weak修饰符还有另一优点:在持有某对象的弱引用时,若该对象被废弃,则此弱引用将自动失效切处于nil被赋值的状态(空弱引用)。

objectivec 复制代码
id __weak obj1 = nil;
        {
            id __strong obj0 = [[NSObject alloc] init];
            obj1 = obj0;
            NSLog(@"A: %@", obj1);
        }
        /*
		obj0变量超出其作用域,强引用失效
		所以自动释放自己持有的对象
		因为对象无持用者,所以废弃该对象

		废弃对象的同时
		持有该对象弱引用的obj1变量的弱引用失效,nil赋值给obj1
		*/
NSLog(@"B: %@", obj1);

源代码的结果如下:

像这样,使用__weak修饰符即可避免循环引用。通过检查附有__weak修饰符的变量是否为nil,可以判断被赋值的对象是否已废弃。

__unsafe_unretained修饰符

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

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

与附有__weak修饰符的变量一样,因此自己生成并持有的对象不能继续为自己所有,所以生成的对象会立即释放。但是当废弃时并不会自动置nil。

objectivec 复制代码
id __unsafe_unretained obj1 = nil;
        {
            id __strong obj0 = [[NSObject alloc] init];
            obj1 = obj0;
            NSLog(@"A: %@", obj1);
        }
NSLog(@"B: %@", obj1);

以上代码偶尔会运行成功,但更多情况下访问一个空对象会报错。

__autoreleasing修饰符

在 ARC 有效时,用 @autoreleasepool 块替代 NSAutoreleasePool 类,用附有 __autoreleasing 修饰符的变量替代 autorelease 方法

objectivec 复制代码
/* ARC无效 */
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
[pool drain];

/*  有效 */
@autoreleasepool {
    id __autoreleasing obj = [[NSObject alloc] init];
}

但是,通常情况下我们不会显示的附加__autoreleasing修饰符和__strong修饰符。

当使用alloc/new/copy/mutableCopy以外的方法来取得丢下时,该对象会自动被注册到autorelease方法中。

访问附有__weak修饰符的变量时,必须访问注册到autoreleasepool的对象。这是因为__weak修饰符纸持有对象的弱引用,而对象有可能被废弃,但是如果把要访问的对象注册到autoreleasepool中,在@autoreleasepool块结束之前都能确保该对象存在。因此:
使用附有__weak修饰符的变量时必定要使用注册到autoreleasepool中的对象

当我们显示的制定__autoreleasing修饰符时,必须注意对象变量要为自动变量(包括局部变量,函数以及方法参数)

无论 ARC 是否有效,调试用的非公开函数 _objc_autoreleasePoolPrint()都可使用。

该函数可以用于打印当前自动释放池中的所有对象信息。

规则

当ARC有效时,需要遵守的规则:

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

不能使用 retain/release/retainCount/autorelease

内存管理是编译器的工作,因此没必要使用内存管理的方法。

设置ARC有效时,无需(禁止)再次键入retain或release代码。

实际上,再次键入retain和release代码时会报错,所以应该是禁止键入。

同样的,retainCount和release也会引起编译错误。

不能使用 NSAllocateObject/NSDeallocateObject

在ARC有效时,禁止使用NSAllocateObject函数。同retain方法一样,会引起编译报错。同一释放对象的NSDeallocateObject函数也不可使用。

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

当ARC无效时,用于对象生成/持有的方法必须遵守以下的命名规则。

使用alloc/new/copy/mutableCopy时,必须返回给调用方所应当持有的对象。

但是当ARC有效时,init开始的方法必须是实例方法,并且要返回对象。返回的对象应为id类型或该方法声明类的对象类型,或者是该类型的父类或者子类。该返回对象并不注册到autoreleasepool上。基本知识对alloc方法返回值的对象进行初始化处理并返回该对象。如下所示:

objectivec 复制代码
-(void) initWithObject:(id) obj;

对象型变量不能作为 C 语言结构体(struct/union)的成员

objectivec 复制代码
struct Data {
	NSMutableArray *array;
};

以上代码会报错

显式转换 "id" 和 "void *"

objectivec 复制代码
//id和void*互转时需要通过__bridge转换
	id obj = [[NSObject alloc] init];
	void *p = (__bridge void *)obj;
	id o = (__bridge id)p;

__bridge_retained 转换可使要转换赋值的变量也持有所赋值的对象。

objectivec 复制代码
/* ARC无效 */
id obj = [[NSObject alloc] init];
void *p = obj;
[(id)p retain];

__bridge_retained 转换变为了 retain。变量 obj 和变量 p 同时持有对象。

objectivec 复制代码
void *p = 0;
{
    id obj = [[NSObject alloc] init];
    p = (__bridge_retained void *)obj;
}
NSLog(@"class=%@", [(__bridge id)p class]);

变量作用域结束时,虽然随着持有强引用的变量 obj 失效,对象随之释放,但由于 __bridge_retained 转换使变量 p 看上去处于持有该对象的状态,因此该对象不会被废弃。

__bridge_transfer 转换提供与此相反的动作,被转换的变量所持有的对象在该变量被赋值给转换目标变量后随之释放。

objectivec 复制代码
id obj = (__bridge_transfer id)p;
//上述代码在ARC无效时如下表达:
/* ARC无效 */
id obj = (id)p;
[obj retain];
[(id)p release];

同 __bridge_retained 转换与 retain 类似,__bridge_transfer 转换与 release 相似。在给 id obj 赋值时 retain 即相当于 __strong 修饰符的变量。

属性

当ARC有效时,以下可作为这种属性声明中使用的属性来用

以上各种属性赋值给指定的属性中就相当于赋值给附加各属性对应的所有权修饰符的变量中。

只有 copy 属性不是简单的赋值,它赋值的是通过 NSCopying 接口的 copyWithZone: 方法复制赋值源所生成的对象。

另外,在声明类成员变量时,如果同属性声明中的属性不一致则会引起编译错误。比如下面这种情况。

objectivec 复制代码
id obj;//默认为__strong

@property (nonatomic, weak) id obj;
//会出现报错

//需要改成以下形式
id __weak obj;

数组

使用修饰符赋值数组的使用与变量相同。

objectivec 复制代码
id objs[10];

id __weak objs[10];

__unsafe_unretained 修饰符以外的 __strong/__weak/__autoreleasing 修饰符保证其指定的变量初始化为 nil。同样地,附有 __strong/__weak/__autoreleasing 修饰符变量的数组也保证其初始化为 nil

下面我们就来看看数组中使用附有 __strong 修饰符变量的例子。

objectivec 复制代码
{
    id objs[2];
    objs[0] = [[NSObject alloc] init];
    objs[1] = [NSMutableArray array];
}

数组超出其变量作用域时,数组中各个附有 __strong 修饰符的变量也随之失效,其强引用消失,所赋值的对象也随之释放。这与不使用数组的情形完全一样。

将附有 __strong 修饰符的变量作为动态数组来使用时又如何呢?在这种情况下,根据不同的目的选择使用 NSMutableArrayNSMutableDictionaryNSMutableSet 等 Foundation 框架的容器。这些容器会恰当地持有追加的对象并为我们管理这些对象。

像这样使用容器虽然更为合适,但在 C 语言的动态数组中也可以使用附有 __strong 修饰符的变量,只是必须要遵守一些事项。以下按顺序说明。

声明动态数组用指针。

复制代码
id __strong *array = nil;

声明动态数组时,我们需要显式的指定为__strong修饰符。

objectivec 复制代码
id __strong *array = nil;

由于 "id * 类型" 默认 为 "id __autoreleasing * 类型",所以有必要显式指定为 strong 修饰符。另外,虽然保证了附有 __strong 修饰符的 id 型变量被初始化为 nil,但并不保证附有 __strong 修饰符的 id 指针型变量被初始化为 nil

使用类名如下述描述:

objectivec 复制代码
NSObject * __strong *array = nil;

其次,使用 calloc 函数确保想分配的附有 __strong 修饰符变量的容量占有的内存块。

objective-c 复制代码
array = (id __strong *)calloc(entries, sizeof(id));

该源代码分配了 entries 个所需的内存块。由于使用附有 __strong 修饰符的变量前必须先将其初始化为 nil,所以这里使用使分配区域初始化为 0 的 calloc 函数来分配内存。不使用 calloc 函数,在用 malloc 函数分配内存后可用 memset 等函数将内存填充为 0。

但是,像下面的源代码这样,将 nil 代入到 malloc 函数所分配的数组各元素中来初始化是非常危险的。

objective-c 复制代码
array = (id __strong *)malloc(sizeof(id) * entries);
for (NSUInteger i = 0; i < entries; ++i)
    array[i] = nil;

这是因为由 malloc 函数分配的内存区域没有被初始化为 0,因此 nil 会被赋值给附有 __strong 修饰符的并被赋值了随机地址的变量中,从而释放一个不存在的对象。在分配内存时推荐使用 calloc 函数。

像这样,通过 calloc 函数分配的动态数组就能完全像静态数组一样使用。

objective-c 复制代码
array[0] = [[NSObject alloc] init];

但是,在动态数组中操作附有 __strong 修饰符的变量与静态数组有很大差异,需要自己释放所有的元素。

当我们要废弃数组时,不能如下直接free。会使数组各元素的值的对象无法释放,引起内存泄漏。如下述代码所示。

objective-c 复制代码
free(array);

这是因为:在静态数组中,编译器能够根据变量的作用域自动插入释放赋值对象的代码,而在动态数组中,编译器不能确定数组的生存周期,所以无从处理

如以下源代码所示,一定要将 nil 赋值给所有元素中,使得元素所赋值对象的强引用失效,从而释放那些对象。在此之后,使用 free 函数废弃内存块。

objective-c 复制代码
for (NSUInteger i = 0; i < entries; ++i)
    array[i] = nil;
free(array);

同初始化时的注意事项相反,即使用 memset 等函数将内存填充为 0 也不会释放所赋值的对象。这非常危险,只会引起内存泄漏。对于编译器,必须明确地使用赋值给附有 __strong 修饰符变量的源代码。所以请注意,必须将 nil 赋值给所有数组元素。

并且,memcpy和realloc函数也会有危险,因为数组元素所赋值的对象有可能被保留在内存中或是重复被废弃,所以也禁止使用。


相关推荐
你要飞2 小时前
Hexo + Butterfly 博客添加 Live2D 看板娘指南
笔记
ajsbxi5 小时前
【Java 基础】核心知识点梳理
java·开发语言·笔记
RollingPin5 小时前
iOS八股文之 RunLoop
ios·多线程·卡顿·ios面试·runloop·ios保活·ios八股文
呱呱巨基5 小时前
vim编辑器
linux·笔记·学习·编辑器·vim
新子y5 小时前
【小白笔记】普通二叉树(General Binary Tree)和二叉搜索树的最近公共祖先(LCA)
开发语言·笔记·python
聪明的笨猪猪5 小时前
Java JVM “调优” 面试清单(含超通俗生活案例与深度理解)
java·经验分享·笔记·面试
爱学习的uu5 小时前
CURSOR最新使用指南及使用思路
人工智能·笔记·python·软件工程
YuCaiH6 小时前
Linux文件处理
linux·笔记·嵌入式
Cathy Bryant6 小时前
大模型损失函数(二):KL散度(Kullback-Leibler divergence)
笔记·神经网络·机器学习·数学建模·transformer
qq_398586546 小时前
Threejs入门学习笔记
javascript·笔记·学习