iOS_理解“属性”(property)这一概念

一、属性概念

​ “属性”(property)是Objective-C的一项特性,用于封装对象中的数据。OC对象通常会把其所需要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来访问。其中“获取方法”(getter)用于读取变量值,而“设置方法”(setter)用于写入变量值。(这一概念已经定型,并且经由“属性”这一特性而成为Objective-C 2.0的一部分)开发者可以令编译器自动编写与属性相关的存取方法。也可以使用“点语法”(dot syntax)更为容易地依照类对象来访问存取其中的数据。下面介绍与属性相关的细节。

​ 对于类中定义的实例变量,如果有增删的,则访问其时的偏移量就会相应的改变,在修之后又必须重新编译。如:两个库中使用了新旧两份不同的代码,那么运行时就会出现不兼容的现象,其他编程语言都有应对此问题的办法。

​ 而OC的做法是,把实例变量当做一种存储偏移量所用的“特殊变量”,交由“类对象”保管。偏移量会在运行时查找,如果类的定义变了,那么存储的偏移量也就边了,这样的话,无论何时访问实例变量,总能使用正确的偏移量。

​ 甚至可以在运行时向类中新增实例变量,这就是稳固的“应用程序二进制接口”(Application Binary Interface,ABI)。ABI定义了许多内容,其中就有生成代码时所应遵循的规范。有了这种“稳固的”的ABI,我们就可以在类的延展(extension)或类的实现(implementation)中定义实例变量了。所以不一定要在接口(如:.h文件)中把全部实例变量都声明好,可以将其隐藏在.m文件中。

​ 然而OC一般很少直接使用实例变量,而是声明属性通过其存取方法来使用。虽说属性最终还是通过实例变量来实现的,但它却提供了一种简洁的抽象机制,也有一套严格的命名规范。

例如:声明属性,不加任何修饰时系统会根据类型设置默认的修饰:

@interface Person: NSObject
@property NSInterger age; // 纯量类型:@property (atomic、readwrite、assign) NSInterger age;
@property NSString *name; // 对象类型:@property (atomic、readwrite、strong) NSString *name;
@end
// 下面会一一介绍这些修饰...

二、属性的生成:

1、@synthesize

​ 默认情况下@property是用@synthesize(合成)修饰的(即默认实现:@synthesize name = _name),自动生成 ivar + setter + getter。(也可以用@synthesize name = _myName修改实例变量的名字,但一般不建议修改)

@implementation Person: NSObject
// 下面三项都自动实现不需要开发者写
@synthesize name = _name // ivar
- (void)setName:(NSString *)name {...} // setter
- (NSString *)name {...} // getter
@end

​ 需要手动加上@@synthesize name = _name的情况:

  • setter 和 getter 方法都重写时,会屏蔽自动生成
  • 重写了只读属性的getter时,会屏蔽自动生成
  • 重写了父类属性时,会屏蔽自动生成

2、@dynamic

​ 还有一个修饰符是@dynamic(动态),即不需要自动生成(有自己实现或运行时动态绑定),但是如果调用时没有实现,则会crashUnrecognized selector to XXX (可以看一下另一篇博客:iOS_Objective-C 消息发送(消息查找 及 消息转发)过程


这里再提一下另外两种种情况:

  • 在Category中声明属性时,不会自动生成(ivar + setter + getter)。可以使用runtime实现关联对象:
// Person.m
#import <objc/runtime.h>
- (void)setPlace:(NSString *)place {
  objc_setAssociatedObject(self, @"place", place, OBJC_ASSOCIATION_COPY);
}
- (NSString *)place {
  return objc_getAssociatedObject(self, @"place");
}

对象关联类型:

关联类型等效的@property属性
OBJC_ASSOCIATION_ASSIGNassign
OBJC_ASSOCIATION_RETAIN_NONATOMICnonatomic,retain
OBJC_ASSOCIATION_COPY_NONATOMICnonatomic,copy
OBJC_ASSOCIATION_RETAINretain
OBJC_ASSOCIATION_COPYcopy
  • 在protocol里声明属性

    方法1:在代理的@implementaton里加:@synthesize propertyName;让系统为代理自动生成。

    方法2:代理重新声明一下该属性。


三、属性的特质:

​ 属性可以有用的特质分为4类:

1、原子性:

atomic原子性访问:

​ 默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性,如两个线程读写同一属性,那么无论何时,总能获得有效的值。

​ 但atomic并不是绝对的线程安全,如:当A线程进行写操作后,B线程又进行写(B线程会等待A线程写完退出setter方法后,再进入setter方法进行写)。上述操作完成写之后,A线程再读取出来的就不是其希望获取到的值了(而是B线程写入的值);另外若还有个C线程在A读之前进行了release操作(MRC模式下时),还会导致crash。这就破坏了线程安全,因而还需要我们为线程添加锁等操作来确保线程安全。

atomic只是保证了存取方法getter和setter的线程安全,并不能保证整个对象是线程安全的。如一个线程循环读数据,另一个线程循环写数据,那么就肯定会出现内存问题,因为这和getter和setter没有关系。(如:NSArrayobjectAtIndex:就不是线程安全的,需要加锁等确保安全)

nonatomic非原子性访问:

​ 就是去掉了atomic为存取方法添加的锁,即其getter和setter方法不是线程安全的。如:当A线程在进行写操作时,另一个线程突然闯入,把尚未修改好的属性值读取出来,这时线程读到的值就是不对的。

因为iOS中使用同步锁的开销较大,会带来性能问题。一般情况下不要求属性必须是“原子性”的,因为这也不能保证“线程安全”(thread safe)。(Max OS使用atomic通常不会有性能瓶颈)


2、读/写权限:

readwrite读写:

​ 属性默认用readwrite修饰,拥有“获取方法”(getter)和“设置方法”(setter)。若该属性由@@synthesize修饰,则编译器会自动生成这两个方法。

readonly只读:

​ 属性用readonly修饰,仅拥有“获取方法”(getter)方法。若该属性由@@synthesize修饰时,编译器仅为其合成获取方法。我们也可以在接口中用readonly修饰,在类的延展(extension)用readwrite修饰,就可以实现在外部不可修改,在内部(实现文件中)可以修改。


3、内存管理语义:

1)assign:“纯量类型”(scalar type)

​ 用于修饰“纯量类型”(scalar type,如:CGFloat、NSInteger等)。setter方法直接赋值(如:_age = age),不更改引用计数。因为纯量类型一般分配在栈上,由系统自动管理,不会造成野指针。

​ 如果用来修饰对象的话,当被修饰的对象释放后,assign修饰的属性指针不会置为nil,成为野指针。如果后续这块地址被分配,再使用该属性则会crash(Error:EXC_BAD_ACCESS)。

2)retain:“拥有关系”(owning relationship)

​ 强引用,一个对象当还有强指针指向时,引用计数不会清零;当没有强指针指向时(如:调用release使其引用计数-1),则会被系统回收。其setter方法会对旧值release,再对新值retain:

- (void)setProperty:(id *)property {
    if (_property != property) {
        [_property release]; // 对旧值release
        _property = [property retain]; // 对新值retain
    }
}

3)strong:“拥有关系”(owning relationship)

​ 强引用,ARC模式下用strong代替MRC模式下的retain。(ARC模式后才新增的修饰符)

4)weak:“非拥有关系”(nonowning relationship)

​ 弱引用,同assign类似,但是在当前属性指向的对象被释放时,该属性会被置空(=nil),OC给nil发送消息是不会报错的。

5)unsafe_unretained:“不安全非拥有”

​ 跟assign类似,不过是用于“对象类型”(object type),表示一种“非拥有关系”(不保留,unretained),当目标对象被销毁时也不会置为nil(“不安全”,unsafe)。

​ unsafe_unretained 差不多是实际使用最少的一个标识符了,在使用中它的用处主要有下面几点:

  1. 兼容性考虑。iOS4 以及之前还没有引入 weak,这种情况想表达弱引用的语义只能使用 unsafe_unretained。这种情况现在已经很少见了。
  2. 性能考虑。使用 weak 对性能有一些影响,因此对性能要求高的地方可以考虑使用 unsafe_unretained 替换 weak。一个例子是 YYModel 的实现,为了追求更高的性能,其中大量使用 unsafe_unretained 作为变量标识符。

6)copy:“拷贝”

​ 跟strong类型,然而不会保留新值,而是将其内容“拷贝”(Copy)一份到一块新的内存,即:该属性指针传入新值的指针是指向两块不同的内存地址。

- (void)setProperty:(id *)property {
    if (_property != property) {
        [_property release]; // 对旧值release
        _property = [property copy]; // 在拷贝出新值
    }
}

​ 当属性类型为NSString*时,经常使用此特性来保护其封装性,因为传递给setter方法的新值,有时候可能是一个指向NSMutableString类的实例,若此时不拷贝字符串,那么设置完属性后,字符串的值就可能会在对象不知情的情况下遭人更改。所以这个时候就要拷贝copy一份不可变的字符串,确保对象中的字符串值不会无意间变动。


4、方法名:

getter=XXX:指定“获取方法”的方法名

​ 如果属性是Bool型,而你想为其获取方法加上“is”前缀,就可以用这个特性来指定。如:UISwitch类中表示“开关”(switch)是否打开的属性就是这样定义的:

@property (nonatomic, getter=isOn) BOOL on;

setter=XXX:指定“设置方法”的方法名

​ 修改setter方法名的用法不太常见。


当我们不写这些特质时,默认情况是:

  • “纯量类型”(scalar type):atomic、readwrite、assign
  • “对象类型”(object type):atomic、readwrite、strong

总结:通过上述特质,可以微调由编译器所合成的存取方法。“属性定义”就相当于“类”和“待设置的属性值”之间达成的契约。

注意:如果自己来实现时,应保证其具备属性所声明的特质。如:我们将某个属性声明为copy,那么就应该在其setter中拷贝相关对象,否则会误导该属性的使用者,而且若不遵从这一约定,还会令程序产生bug。另外如果想在其他方法里设置属性值时,也同样需要遵循属性定义中宣称的语义。

​ 如:在“初始化方法”(initializer)中设置名字时,也需要遵循属性定义中宣称的“copy”语义:

- (id)initWithName:(NSString *)name {
  self = [super init];
  if (self) {
    _name = [name copy]; // 遵循声明的语义
  }
}

现在属性的所有修饰已介绍完毕,下面来看一些相关的面试题吧。以下是我在面试中遇到过的问题,归类整理了一下。

四、灵魂拷问

  1. 用assign修饰“对象类型”(object type)会如何?
    ​ 会报warning⚠️,当指向对象被释放掉后,再使用该属性会crash。

  2. 用strong/weak/copy 修饰“纯量类型”(scalar type)时会如何?
    ​ 会报Error❗️,这些修饰符只能用来修饰“对象类型”(object type)。

  3. weak和assign的区别?
    ​ assign变量在指向变量释放后不会置为nil,再使用会crash。而weak会置为nil。

  4. weak和strong的区别?
    ​ 当一个对象还有strong类型的指针指向时,不会被释放。若仅有weak类型的指针指向时,会被释放。

  5. 系统是如何实现weak变量的?
    ​ Runtime维护了一个全局的hash(哈希)表:key为对象内存地址,value为可变数组可以存放n个weak对象的指针地址。当实例对象存在weak指针指向自己时,系统会为这个实例创建一个子类,将这个实例的isa指正指向子类,然后重写dealloc方法,在dealloc方法中添加清除weak指针操作(以当前对象内存地址为key,到hash表找找到所有指向自己的weak指针,遍历置为nil,并将此项从表中删除)。详情见此

  6. NSString和NSArray和NSDictionary共同点?
    ​ 都属于“容器类型”(collection)的对象,用copy修饰表示不希望值跟随外部改变,用strong修饰会跟随指向内存地址的内存的改变而改变。

  7. NSMutableArray用copy修饰,会怎如何?
    ​ 变成不可变数组,进行可变操作时会crash。

  8. block要用copy还是strong?__block是什么?
    ​ block是对象,可以retain和release。但是block创建的时候时分配在栈上,作用域属于创建时的作用域,因为栈区的特点就是创建的对象在出了作用域后随时可能被销毁,一旦销毁,后续再使用就会crash。所以声明为copy,对其进行copy到堆区(对block执行copy后,会存放在堆区)。
    ​ 用strong也是一样的,strong修饰的block默认会执行copy操作。但是为了block属性声明和实际操作一致,最好声明为copy。
    __block:在RAC和MRC下都可以用,修饰纯量类型or对象类型都可以,使其在block代码块中可以被修改。
    有关block更详细的介绍可以看这篇:iOS_理解Block(代码块)+底层实现

  9. block用weak修饰可以吗?
    ​ 当block属于全局块(global block,这种块不会捕获任何数据)时,可以使用weak修饰。如:仅操作某个数据库、单例、发送通知等。因为global类型的block是存储在全局的数据段中,对其进行copy也是空操作,因为全局块不可能为系统所回收,这种块实际上相当于单例。

  10. __weak__strong__unsafe_unretained__autorelease都是什么?
    这些都是变量的生命周期限定符:lifetime qualifiers
    __weak:在ARC下,为了避免循环引用。经常用:__weak typeof(self) weakSelf = self;(弱引用self),然后在block中用weakSelf来实现。不能修饰“纯量类型”(scalar type)(warning⚠️)。
    __strong:强引用,保证在block执行完之前,对象不会被释放。经常用:__strong typeof(weakSelf) strongSelf = weakSelf;(强引用weakSelf,就是使其引用计数+1,避免被系统回收)来实现避免self在block执行到一半时被释放。strongSelf实质就是block代码块里的局部变量,block返回后strongSelf就会释放。
    __unsafe_unretained:该引用不对对象保持强引用,并在对象被释放后不会置为nil, 从而成为一个野指针,所以是unsafe的
    __autorelease:用于表示通过引用(id *)传递并在返回时自动释放的参数

  11. block里用weakSelf和strongSelf调用方法的区别?
    ​ 即:[weakSelf someMethod][strongSelf someMethod]的区别,其实上一条已经说明了(但是当那个面试官突然这么问时,我脑子没转过来>_<)。就是在执行weakSelf调用的方法时,如果self被是否则会导致程序crash。而用strongSelf调用则不会。

  12. xib或storyboard拖的控件为什么是weak?
    ​ 因为xib或storyboard对该控件已经有一个强引用了,而拖出来的属性应该跟这个控件保持相同的生命周期,所以应该用weak修饰。

​ 若用strong修饰的话,当控件离开了xib或sb时,你还想做别的操作时可以这么设置。(当我们不再引用这个控件时,它才会被销毁)

参考:
《Effective Objective-C 2.0》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫小言mo

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值