iOS @property 属性相关的总结

2,795 阅读11分钟

@property 的本质

本质 @property = ivar + getter + setter;(实例变量 + getter方法 + setter方法)。 在编译期自动生成 getter、setter,还自动向类中添加适当类型的实例变量,也可以用 @synthesize 语法来指定实例变量的名字。

@property 常用属性

读写属性:readwritereadonly
setter语意:assignretain / copy
原子性(多线程管理):atomicnonatomic
强弱引用:strongweak

  • 读写属性:
    readwrite :同时生成 setget 方法(默认)
    readonly :只会生成 get 方法

  • 控制set方法的内存管理:
    retainrelease 旧值,retain 新值。希望获得源对象的所有权时,对其他 NSObject 和其子类(用于 OC 对象)
    copyrelease 旧值,copy 新值。希望获得源对象的副本而不改变源对象内容时(一般用于 NSStringblock )
    assign :直接赋值,不做任何内存管理(默认属性),控制需不需生成 set 方法。对基础数据类型 (NSIntegerCGFloat )和C数据类型(int , float , double , char , 等等),另外还有id类型

  • 原子性(多线程管理):

    • atomic
      默认属性,访问方法都为原子型事务访问。锁被加到所属对象实例级,性能低。原子性就是说一个操作不可以中途被 cpu 暂停然后调度, 即不能被中断, 要不就执行完, 要不就不执行. 如果一个操作是原子性的,那么在多线程环境下, 就不会出现变量被修改等奇怪的问题。原子操作就是不可再分的操作,在多线程程序中原子操作是一个非常重要的概念,它常常用来实现一些同步机制,同时也是一些常见的多线程 Bug 的源头。当然,原子性的变量在执行效率上要低些。
    • nonatomic
      非原子性访问。不加同步,尽量避免多线程抢夺同一块资源。是直接从内存中取数值,因为它是从内存中取得数据,它并没有一个加锁的保护来用于cpu中的寄存器计算Value,它只是单纯的从内存地址中,当前的内存存储的数据结果来进行使用。 多线程并发访问会提高性能,但无法保证数据同步。尽量避免多线程抢夺同一块资源,否则尽量将加锁资源抢夺的业务逻辑交给服务器处理,减少移动客户端的压力。
      当有多个线程需要访问到同一个数据时,OC中,我们可以使用 @synchronized (变量)来对该变量进行加锁(加锁的目的常常是为了同步或保证原子操作)。
  • 强指针(strong)、弱指针(weak)

    • strong
      strong 系统一般不会自动释放,在 oc 中,对象默认为强指针。作用域销毁时销毁引用。在实际开放中一般属性对象一般 strong 来修饰(NSArrayNSDictionary),在使用懒加载定义控件的时候,一般也用strong。
    • weak
      weak 所引用对象的计数器不会加一,当对象被释放时指针会被自动赋值为 nil,系统会立刻释放对象。
    • __unsafe_unretained 弱引用 当对象被释放时指针不会被自动赋值为 ni
      在ARC时属性的修饰符是可以用 assign 的(相当于 __unsafe_unretained
      在ARC时属性的修饰符是可以用 retain 的 (相当于 __strong)
    • 假定有N个指针指向同一个对象,如果至少有一个是强引用,这个对象只要还在作用域内就不会被释放。相反,如果这N个指针都是弱引用,这个对象马上就被释放
    • 在使用 sb 或者 xib 给控件拖线的时候,为什么拖出来的先属性都是用 weak 修饰呢?
      由于在向 xib 或者 sb 里面添加控件的时候,添加的子视图是添加到了跟视图 View 上面,而 控制器 Controller 对其根视图 View 默认是强引用的,当我们的子控件添加到 view 上面的时候,self.view addSubView: 这个方法会对添加的控件进行强引用,如果在用 strong 对添加的子控件进行修饰的话,相当于有两条强指针对子控件进行强引用, 为了避免这种情况,所以用 weak 修饰。
      注意:
      (1)addSubView 默认对其 subView 进行了强引用
      (2)在纯手码实现界面布局时,如果通过懒加载处理界面控件,需要使用strong强指针
  • ARC管理内存是用 assign 还是用 weak
    assign : 如果由于某些原因代理对象被释放了,代理指针就变成了野指针。
    weak : 如果由于某些原因代理对象被释放了,代理指针就变成了空指针,更安全(weak 不能修饰基本数据类型,只能修饰对象)。

weak修饰符

  • weak的作用:
    weak 关键字的作用弱引用,所引用对象的计数器不会加一,并在引用对象被释放的时候自动被设置为 nil,大大避免了野指针访问坏内存引起崩溃的情况,另外 weak 还可以用于解决循环引用。

  • 使用场景:
    用于一些对象相互引用的时候,避免出现强强引用,对象不能被释放,出现内存泄露的问题。

  • 实现原理:
    runtime 维护了一个 weak 表,用于存储指向某个对象的所有 weak 指针。weak 表其实是一个hash(哈希)表,Key 是所指对象的地址,Valueweak 指针的地址(这个地址的值是所指对象的地址)数组。(备注:strong 是通过 runtime 维护的一个自动计数表结构)
    weak 的实现原理可概括三步:

    1. 初始化时:runtime 会调用 objc_initWeak 函数,初始化一个新的 weak 指针指向对象的地址。
    2. 添加引用时:objc_initWeak 函数会调用 objc_storeWeak() 函数, objc_storeWeak() 的作用是更新指针指向,创建对应的弱引用表。
    3. 释放时,调用 clearDeallocating 函数。clearDeallocating 函数首先根据对象地址获取所有 weak 指针地址的数组,然后遍历这个数组把其中的数据设为 nil ,最后把这个 entryweak 表中删除,最后清理对象的记录。
  • 文章推荐:
    iOS 底层解析weak的实现原理(包含weak对象的初始化,引用,释放的分析)
    浅谈iOS之weak底层实现原理

深浅复制

深复制与浅复制

  • 深复制
    • 源对象和副本对象不同的各两个对象
    • 源对象的引用计数器不变,副本对象的引用计数器为 1(因为是新产生的)
    • 本质产生了新的对象
  • 浅复制
    • 源对象和副本对象是一个对象
    • 源对象(副本对象)引用计数器 + 1,相当于做了一次 retain 操作
    • 本质没有产生新的对象

复制与计数器

  • 深复制
// 深复制:产生了新对象,新对象(副本对象)的计数器是1, 源对象的计数器不变
// str : 1
NSString *str = [NSString stringWithFormat:@"Tom"];
// str2 : 1
NSMutableString *str2 = [str mutableCopy];     
NSLog(@"str=%zd, str2=%zd", [str retainCount], [str2 retainCount]);
[str2 release];
  • 浅复制
// 浅复制:没有产出新对象, 源对象(副本对象)的计数器会+1
NSString *str = [NSString stringWithFormat:@"Jack"];
NSString *str2 = [str copy];
[str2 release];
NSLog(@"%zd", [str retainCount]);

copy 与 mutableCopy

copy 产生不可变副本,mutableCopy 产生可变副本 copy与mutableCopy.png 理论上讲OC中的任何一个对象都可以复制(copymutableCopy ),只是Foundation库中的类的对象可以直接复制,自定义的类的对象需要做一些额外的工作才能复制,但实际做app几乎不需要复制自定义类的对象。

不可变对象和可变对象的 copymutableCopy 对比

不可变对象 copy不可变对象 是浅复制,其他都是深复制。(不可变对象指NSArray、NSDictionary、NSString等)

可变对象/不可变对象的copy/mutableCopy.png

copy 与 strong

NSMutableArraycopystrong 修饰后的变化 把NSMutableArray用copy修饰有时就会crash,因为对这个数组进行了增删改操作,而copy后的数组变成了不可变数组NSArray,没有响应的增删改方法,所以对其进行增删改操作就会报错。 举例如下:

NSMutableArray *b = [NSMutableArray array];
a = b;
  • @property (nonatomic, copy) NSMutableArray *a;
NSMutableArray *b = [NSMutableArray array];
// a被copy后就成了NSArray了。
a = [b copy];
  • @property (nonatomic, strong) NSMutableArray *a; 如果是 strong,直接是赋值 a = b;右边是什么,左边就是什么,并且是强引用新值,左边的类型会与右边的相同,不会改变。

怎么用 copy 修饰符?

  1. NSString、NSArray、NSDictionary 等经常使用 copy 关键字,是因为他们有对应的可变类型 NSMutableString、NSMutableArray、NSMutableDictionary。他们之前可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性时拷贝一份。
  2. block 也使用 copy

文章推荐

iOS 深拷贝浅拷贝与@property 引用计数关键字Strong,Copy,Weak,Assign
iOS 浅谈:深.浅拷贝与copy.strong
数组的属性修饰符到底用strong还是copy?
iOS中copy和mutableCopy的详细分析

NSString 为什么用 copy 而不用 retain

我们通过实际操作来说明,我们把str赋值给 zhangsanname 属性,然后去改变 str,结果: 用 @property (nonatomic, retain) NSString *name; 修饰的name答应结果为 zhangsanabcname 属性被修改了; 用 @property (nonatomic, copy) NSString *name; 修饰的 name 答应结果为 zhangsanname 属性没有被修改。

NSMutableString *str = [NSMutableString string];
str.string = @"zhangsan";
        
Person *zhangsan = [[[Person alloc] init] autorelease];
zhangsan.name = str;
        
[str appendString:@"abc"];
        
NSLog(@"%@ %@",  str, zhangsan.name);

下面我们来看代码set方法的内部实现: 当.h用@property (nonatomic, retain) NSString *name;时,_name = [name retain];相当于[name retain]_name = name;,而这两句话相当于是先把原来的作引用计数+1,再把指针付给 _name ,实际上指向的是一块内存,这样会导致原来的内容改变,_name 也会改变,而实际中我们一般不希望 _name 改变,所以我们不用retain。

- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        
        _name = [name retain];
        //_name = [name retain];相当于下边两句,而这两句话相当于是先把原来的作引用计数+1,再把指针付给_name,实际上指向的是一块内存,这样会导致原来的内容改变,_name也会改变,而实际中我们一般不希望_name改变,所以我们不用retain
//        [name retain];
//        _name = name;
    }
}

- (void)dealloc {
//    [_name release];
//    _name = nil;
    self.name = nil;
    [super dealloc];
}

当.h用@property (nonatomic, copy) NSString *name;时,当传入的值为可变对象时,调用 _name = [name copy]; copy 会创建一个新的对象赋值给 _name,所以 _namename 是两块无关的内容,改变 name 不会影响 _name

- (void)setName:(NSString *)name {
    if (_name != name) {
        [_name release];
        // 当传入的值为可变对象时,copy会创建一个新的对象赋值给_name,所以_name和name是两块无关的内容,改变name不会影响_name
        _name = [name copy];
    }
}

- (void)dealloc {
//    [_name release];
//    _name = nil;
    self.name = nil;
    [super dealloc];
}

@protocol 和 category 中如何使用 @property

  1. 在 protocol 中使用 property 只会生成 setter 和 getter 方法声明,使用属性的目的,是希望遵守该协议的对象能实现该属性。
  2. category 使用 @property 也只会生成 setter 和 getter 方法声明,如果真的要给 category 增加属性实现,需要借助运行时的两个函数: objc_setAssociatedObject、objc_getAssociatedObject。

@synthesize

  1. 在实现文件中使用 @synthesize propertyName,编译器先会查找这个属性名的setter方法和getter方法有没有被人为实现,如果已经实现,则不再实现,如果没有,则会帮我们生成一个属性命的setter方法和getter方法。
  2. 当在实现文件中使用了 @synthesize propertyName,编译器还会做一件事情,在类成员变量中查找一个名为 _propertyName 的成员变量,如果没有,再继续查找名为 propertyName 的成员变量,如果这两个都没有,编译器会自动为我们生成一个私有的名为 _propertyName 的成员变量。注意,系统自动创建的都是私有的。
  3. 当在实现文件中这样写 @synthesize propertyName = varName;时,setter getter 方法所对应的是一个名为 varName 的成员变量,修改和读取的是 varName 成员变量的值。
  4. 当我们在实现文件中不写 @synthesize propertyName 时,在Xcode 4.5之前的版本不会帮我们自动实现 settergetter 方法,系统当然也不再会为我们生成对应的成员变量。但是在Xcode 4.5之后可以不用写@synthesize了,就跟3、4一样了。
  5. 当我们既定义了 @synthesize,又在实现文件中人为重写 settergetter 方法时,那么 @synthesize 将不再工作,也就不会为我们创建没有定义的 _propertyName 成员变量了,这时候如果在 settergetter 方法中调用 _propertyName 将会发生编译错误

@synthesize 和 @dynamic 分别有什么用

  1. @property 有两个对应的词 @synthesize、 @dynamic。如果 @synthesize 和@dynamic 都没写,那么默认的就是 @syntheszie var = _var;
  2. @synthesize 语义是如果没有手动实现 setter方法 和 getter方法,那么编译器会主动添加。
  3. @dynamic 告诉编译器 setter方法 和 getter方法 用户自己实现,不自动生成。假如一个属性被声明为 @dynamic var,然后你没有实现 @setter方法和 @getter 方法,编译时候没有问题,但程序运行到 instance.var = someVar,由于缺 setter 方法 会导致程序崩溃,或者运行到 someVar = var 时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。