Objective-C Property笔记(二)| 青训营笔记

131 阅读5分钟

这是我参与「第四届青训营 」笔记创作活动的第2天

前言

本文主要讨论属性的修饰符

属性修饰符有哪些

分类属性修饰符
线程安全atomic、nonatomic
读写权限readwrite、readonly、setter、getter
内存管理assign、weak、unsafe_unretained、retain、strong、copy
可空性(nullable、Nullable 、__nullable)、 (nonnull、Nonnull、nonnull)、 (null_unspecified、_Null_unspecified 、null_unspecified)、 null_resettable

线程安全

atomic和nonatomic的区别在于atomic修饰符会为系统自动生成的getter/setter方法进行加锁操作,使得用于多线程时,保证了getter和setter存取方法的线程安全。但也会消耗更多的系统资源。atomic是默认的修饰符。

线程安全指的是多线程操作共享数据不会出现意想不到的结果。但要注意的是,atomic不是线程安全的,atomic只是对属性的存取方法进行了加锁操作,这种安全仅仅只是getter和setter方法的读写安全,并非真正的线程安全。

在下面这篇博客中,博主做了实验

iOS进阶之atomic一定是线程安全的吗(10)_沐雨07的博客-CSDN博客_atomic 线程安全

博主开启了10000个异步任务,首先name是一个nonatomic修饰的属性,下面这段代码在多线程中不断调用setName方法,结果直接使得程序崩溃。

- (void)nonatomic{
    for (NSInteger i = 0; i < 10000; i++) {
        dispatch_async(dispatch_get_global_queue(0, 0), ^{
            self.name = [NSString stringWithFormat:@"name:%ld", i];
        });
    }
}

setName方法的本质是先释放旧值再保留新的值,再将指针指向新的值,由于多线程的存在,对_name成员变量进行了多次的release导致了程序的crash。

-(void)setName:(NSString *)name{
    if (_name != name) {
        [_name release];
        [name retain];
        _name = name;
    }
}

而如果加了atomic会在上述代码的基础上加锁

-(void)setName:(NSString *)name{
	{lock}
    if (_name != name) {
        [_name release];
        [name retain];
        _name = name;
	   }
	{unlock}
}
// 使用synchronize实现
-(void)setName:(NSString *)name{
	@synchronized(self){
    if (_name != name) {
        [_name release];
        [name retain];
        _name = name;
	   }
	}
}

atomic的存在保证了不会出现多个线程同时修改这个值的情况出现,也就不会因为多次的release导致程序崩溃,但至于这个值最终会是什么,是不可预见的,因为多线程的调用顺序不可预知。简而言之,atomic只保证了setter和getter的操作完整性,但不保证属性的线程安全。

比如在上述的博客中,number是一个atomic修饰的变量,虽然保证了number属性的线程安全,但下面这段代码的运行结果却不可预见

/*
1. 将number的值存入寄存器
2. 将number+1
3. 修改number的值
*/
- (void)atomic{
        self.number = 0;
        dispatch_apply(10000, dispatch_get_global_queue(0, 0), ^(size_t index) {
            self.number ++;
        });
        NSLog(@"_number:%d", self.number);
    }

Untitled.png

这篇文章也给出了两个来证明atomic不是线程安全的例子

从源代码理解atomic为什么不是线程安全 - 掘金

但回过头去想,atomic本来就只是对getter和setter方法进行了加锁而已,他本来不是去考虑线程安全的操作。用一个评论的话:

atomic只是个锁,线程安全是是利用锁来构建的一种代码模式,两者从逻辑上就不能对等

读写权限

读写权限比较好理解

  • readwrite:可读可写,默认,同时生成 setter 方法和 getter 方法的声明和实现。
  • readonly:只有getter方法

Untitled 1.png

  • setter : 指定setter的方法名,默认比如name的setter就是setName
  • getter:指定getter的方法名,默认比如name的getter就是name

内存管理

@property retain, assign, copy, nonatomic in Objective-C

  1. assign
    • 默认参数
    • 常用于非指针型变量如CGFLoat、NSInteger、int、float、double、char,因为这些类型一般分配在栈上,栈的内存由系统自动处理,不会造成野指针
    • 也可以修饰对象类型,不会增加引用计数
    • 会产生悬垂指针(指针仍然指向原对象地址,变成悬垂指针,野指针)
    • setter方法直接赋值
  2. strong
    • 强引用,将指向的新对象的引用计数加1
    • setter方法的实现是release旧值,retain新值,用于OC对象类型
  3. weak
    • 弱引用,不会增加对象的引用计数器,用于在block、代理中解决循环引用的问题
    • 声明为weak的指针指向的地址一旦被释放,这些指针都会被赋值为nil,即不会产生悬垂指针
    • setter方法直接赋值
  4. copy
    • setter方法的实现是release旧值、copy新值,用于NSString、NSArray等不可变对象
  5. retain
    • 在MRC下才使用,ARC下用strong
  6. unsafe_unretained
    • 和assign有点像,在MRC下才使用,可以修饰基本数据类型,也可以修饰对象类型,也会产生垂悬指针

说到底每一种修饰符对应了不同的setter方法,下面对几个重要的修饰符做一些补充说明,部分代码参考这篇文章

Objective c 知识总结 @property - 掘金

  • retain的setter代码

@property ( nonatomic, retain) NSObject *obj;

- (void)setObj:(NSObject *)obj{
      if ( _obj != obj ) {
        [_obj release];
        _obj = [obj retain]; // release旧值,retain新值
      }
  }
  • copy的setter代码

@property ( nonatomic, copy ) NSObject *obj;
// MRC 下:
- (void)setObj:(NSObject *)obj{
  if ( _obj != obj ) {
    [_obj release];
    _obj = [obj copy]; // release旧值,copy新值
  }
}
// ARC 下:
- (void)setObj:(NSObject *)obj{
      _obj = [obj copy]; 
  }
  • 对可变对象使用copy修饰符的问题

首先personWithProperty这个对象声明了NSMutableArray类型的nickName变量,但是用copy修饰,在之后初始化这个变量,并试图往其中添加元素,注意编译器不会给出任何的警告!当程序运行时,程序直接崩溃。

@property (nonatomic,copy) NSMutableArray* nickName;

Untitled 2.png

在之前也提到过,copy会在setter中对NSMutableArray发出一个copy的消息,产生了不可变的copy,即这里的nickname变成了一个不可变对象。往不可变对象中添加元素导致了程序的崩溃。

  • 循环引用问题

循环引用是因为多个对象之间强引用导致堆中的资源不能让系统回收。关于循环引用之后会专门用新的文章讲解。这里给出最粗暴的导致循环引用的例子。

首先定义了一个Dog类,它有属性owner,并且用strong修饰。我们让之前的person实例作为它的owner的值。同时在person声明一个dog属性,让dog实例作为这个属性的值。

@interface Dog : NSObject

@property (nonatomic,strong) id owner;

@end

// PersonProperty.h
@property (nonatomic,strong) Dog* dog;
// 循环引用
  Dog* dog = [Dog new];
  dog.owner = personWithProperty;
  personWithProperty.dog = dog;

之后再person类中重写析构方法

- (void)dealloc{
    NSLog(@"DEALLOC!!");
}

当循环引用时,析构函数不会被调用 Untitled 3.png

当把dog类或者person类中的strong改为weak后,析构函数成功调用

@property (nonatomic,weak) Dog* dog;
// 或者
@property (nonatomic,weak) id owner;

Untitled 4.png

总结

本文进一步了解了property的性质,主要讨论了property的各种修饰符。关于线程安全的修饰符有nonatomic和atomic,其中atomic通过加锁保证了getter和setter方法的完整性,但不能保证线程安全。读写权限中可以修改setter和getter方法的名字,也可以设置只读属性。内存管理相关的修饰符中,要注意copy用于修饰不可变对象。

通常在ARC的环境里,strong代替retain,用weak弱引用对象,用assign修饰基本数据类型,copy修饰不可变类型。

提一下为什么不推荐使用strong修饰不可变类型,比如用strong修饰NSArray?这是为了防止赋值给不可变类型的数据是可变类型。具体可以看推荐阅读的第二篇文章。

推荐阅读

OC - 属性关键字和所有权修饰符 - 掘金

Objective-C属性修饰符strong和copy的区别