iOS —— 属性及其特质

【从历年weak看iOS面试】 2013年 面试官:代理用weak还是strong? 我 :weak 。 面试官:明天来上班吧。

2014年 面试官:代理为什么用weak不用strong? 我 : 用strong会造成循环引用。 面试官:明天来上班吧。

2015年 面试官:weak是怎么实现的? 我 :weak其实是 系统通过一个hash表来实现对象的弱引用 面试官:明天来上班吧。

2016年 面试官:weak是怎么实现的? 我 :runtime维护了一个weak表,用于存储指向某个对象的所有weak指针。weak表其实是一个hash(哈希)表,key是所指对象的地址,Value是weak指针的地址(这个地址的值是所指对象指针的地址)数组。 面试官:明天来上班吧。

2017年 面试官:weak是怎么实现的? 我 : 1 初始化时:runtime会调用objc_initWeak函数,初始化一个新的weak指针指向对象的地址。 2 添加引用时:objc_initWeak函数会调用 storeWeak() 函数, storeWeak() 的作用是更新指针指向,创建对应的弱引用表。 3 释放时,调用clearDeallocating函数。clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。 面试官:明天来上班吧。

2018年 面试官:weak是怎么实现的? 我 :跟2017年说的一样,还详细补充了objc_initWeak, storeWeak, clearDeallocating的实现细节。 面试官:小伙子基础不错。13k ,996干不干?干就明天来上班。。 下一个。

2019年 面试官:weak是怎么实现的? 我 : 别说了,拿纸来,我动手实现一个。 面试官:等写完后,面试官慢悠悠的说,小伙子不错,我考虑考虑,你先回去吧。

property的特质分五类@property(是否可空, 原子性, 读写权限, 内存管理语义, 存取方法名) ,本文将逐一探究。

@property 本质

@property = ivar + setter + getter

例如如下代码

  @property NSString *name;
复制代码

编译器会自动编写访问这些属性所需的方法,并且添加一个_name的实例变量。这个过程称为**“自动合成”**(autosynthesis)。

实际上,一个类经过编译后,会生成变量列表ivar_list,方法列表method_list,每添加一个属性,在变量列表ivar_list会添加对应的变量,如_name,方法列表method_list中会添加对应的setter方法和getter方法。

如果想改实例变量的名字,可以采用@synthesize name = _changeName;,用_changeName来代替_name,但一般情况下无需改变。

如果不想让编译器自动合成存取方法和实例变量,可以打如下代码。

@interface ClassName () {
    NSString *_name;
}

@property NSString *name;

@end

@implementation ClassName

@dynamic name;
@end
复制代码

既然写上@dynamic,实例变量及存取方法都失效,那么@property不就多此一举了。并不是这样的,属性的特质还会生效。


属性的特质

@property(是否可空, 原子性, 读写权限, 内存管理语义, 存取方法名)

这些特质会影响到系统自动生成的存取方法。

是否可空

有四个关键字:

  • nullable:可空。
  • nonnull:不可空。
  • null_resettable:get方法不能返回空,set方法可以为空。
  • null_unspecified:不确定是否为空。

这几个关键字是苹果在iOS 9.0引入的一个Objective-C的新特性:nullability annotations。利用这种特性,我们能减少同事之间的沟通成本,提前避免空崩溃的情况。

注意它只能修饰对象,不能修饰基本数据类型。

其有三种写法(null_resettable只有一种),但笔者习惯第一种写在括号里。方法的参数写法类似。

@property(nullable) NSString *name;
@property NSString *_Nullable name;
@property NSString *__nullable name;

 @property (null_resettable) NSString * name;


复制代码

如果需要每个属性或每个方法的参数和返回值都去指定nonnull和nullable,是一件非常繁琐的事。苹果为了减轻我们的工作量,专门提供了两个宏:NS_ASSUME_NONNULL_BEGIN和NS_ASSUME_NONNULL_END。在这两个宏之间定义的所有对象属性和方法参数默认都是nonnull

原子性

有两个关键字,默认atomic

  • nonatomic:非原子性。
  • atomic:原子性。

在并发编程中,如果某操作具备整体性,也就是说,系统其他部分无法观察到其中间步骤所生成的临时结果,而只能看到操作前与操作后的结果,那么该操作就是“原子的”,或者说,该操作具备“原子性”。

  • 区别 atomic 和 nonatomic 的区别在于,系统自动生成的 getter/setter 方法不一样。对于atomic的属性,系统生成的 getter/setter 会保证 get、set 操作的完整性,不受其他线程影响。比如,线程 A 的 getter 方法运行到一半,线程 B 调用了 setter:那么线程 A 的 getter 还是能得到一个完好无损的对象。系统采用自旋锁,对资源进行保护。

  • 缺陷

然而,即使用atomic也不能保证绝对的线程安全。举例来说,假设有一个线程A在不断的读取属性name的值,同时有一个线程B修改了属性name的值,那么即使属性name是atomic,线程A读到的仍旧是修改后的值。

简单点说,只保证setter和getter的操作完整性,不保证属性的线程安全。

除了不能保证线程安全,atomic还会消耗系统资源,因此,开发iOS程序时一般都会使用 nonatomic 属性

读写权限

有两个关键字,默认readwrite

  • readwrite:读写。
  • readonly:只读。

在《Effective Objective-C 2.0》第27条,有这么一句。

把某个属性对外公开为只读属性,然后在"class-continuation分类"中将其重新定义为读写属性。

相关代码如下:

@interface EOCPerson : NSObject
@property (nonatomic, copy, readonly) NSString *firstName;
@property (nonatomic, copy, readonly) NSString *lastName;
@end

@interface EOCPerson () 
@property (nonatomic, copy, readwrite) NSString *firstName;
@property (nonatomic, copy, readwrite) NSString *lastName;
@end
复制代码

内存管理语义

有五个关键字,对象默认strong,基本数据类型默认assign。

  • assign:“设置方法”只会执行针对“纯量类型”(基本数据类型)的简单赋值操作。
  • strong:此特质表明该属性定义了一种“拥有关系”。为这种属性设置新值时,设置方法会先保留新值,并释放旧值,然后再将新值设置上去。
  • weak:此特质表明该属性定义了一种“非拥有关系”。为这种属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同aasign类似,然而再属性所指的对象遭到摧毁时,属性值也会清空。
  • unsafe_unretained:此特质的语义和assign相同,但是它适用于“对象类型”,该特质表达一种“非拥有关系”,当目标对象遭到摧毁时,属性值不会自动清空,这一点与weak有区别。
  • copy:此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝”。

ARC(自动引用计数)

要理解内存管理语义,就必须和ARC联系起来。

对象操作Objective-C 方法
生成并持有对象alloc/new/copy/mutableCopy等方法
持有对象retain方法
释放对象release方法
废弃对象dealloc方法

苹果是通过引用计数表管理引用计数的,这图将帮助我们更好理解。

autorelease

NSAutoreleasePool对象:暂时持有放进池中的对象,当自身废弃时,池中的对象都释放一次。(底层通过栈放进对象,入栈时持有,池废弃前出栈且释放)

  NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
  
  id obj = [[NSObject alloc] init];

  [obj autorelease];
  // obj被最近的一个池持有
  
  [pool drain];
  // 释放池中暂时持有的对象
复制代码

在开发中,我们很少用到NSAutoreleasePool,因为主循环NSRunLoop会对NSAutoreleasePool对象进行生成、持有和废弃处理。

例如以下代码,读入大量图像的同时改变其尺寸,系统会自动生成。

  for (int i = 0; i < 图像数; ++i) {
    /*
     * 读入图像
     * 大量产生 autorelease 的对象
     * 由于没有废弃NSAutoreleasePool 对象
     * 最终导致内存不足!
    */
  }
复制代码

但我们处理完每张图像后,可以释放掉,所以这时我们应该手动创建NSAutoreleasePool对象。

   for (int i = 0; i < 图像数; ++i) {

    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];

    /*
     * 读入图像
     * 大量产生 autorelease 的对象
    */

    [pool drain];

    /*
     * 通过[pool drain],
     * autorelease 的对象被一齐release。
    */
  }
复制代码

注意 : 在ARC下, 不能使用NSAutoreleasePool这个类来创建自动释放池, 而应该用@autoreleasepool { } 这个block。官方文档说明, 使用@autoreleasepool这个block比NSAutoreleasePool更高效!并且在MRC环境下同样适用。

NSAutoreleasePool pool = [[NSAutoreleasePool alloc] init];
[pool release];

// 用以下代码代替上述代码 :

@autoreleasepool {
}

复制代码

内存管理语义关键字的使用及区别

先来了解堆和栈。

  • 栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
  • 堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表。需要程序员自己申请,并指明大小(iOS生成对象时系统底层会算好大小)。

由此可见,基本数据类型放在栈中,对象放在堆中。

assign

基本数据类型用 assign 修饰。

对象通常不能用assign修饰,虽然编译器不会报错。

  • 为什么基本数据类型能用assign,对象不能用?

assign修饰的对象,当对象废弃之后,对象会变为野指针,不知道指向哪,再向该对象发消息,非常容易崩溃。栈上空间的分配和回收都是系统来处理的,因此开发者无需关注,也就不会产生野指针的问题。对象是在堆上的,所以对象不能用。

strong 和 weak

strong的对象会持有对象,而weak不会持有对象。举个例子。

@property (nonatomic, weak)  UIView *weakViewA;
@property (nonatomic, weak) UIView *weakViewB;
@property (nonatomic, strong) UIView *strongView;

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.weakViewA = [[UIView alloc] init];
    NSLog(@"weakViewA = %@", self.weakViewA);
    
    UIView *tempView = [[UIView alloc] init];
    self.weakViewB = tempView;
    NSLog(@"weakViewB = %@", self.weakViewB);
    
    self.strongView = [[UIView alloc] init];
    NSLog(@"strongView = %@", self.strongView);    
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    NSLog(@"touchesBegan-----weakViewA = %@", self.weakViewA);
    NSLog(@"touchesBegan-----weakViewB = %@", self.weakViewB);
    NSLog(@"touchesBegan-----strongView = %@", self.strongView);
}
复制代码

对于weakViewA,指向初始化的对象,由于引用数为0,且没有强指针,所以马上就会被废弃,故为空。

对于weakViewB,由于有tempView引用着生成的对象,所以weakViewB当时还是有值的。当过了viewDidLoad方法后,tempView这个局部变量将被释放,对象也同时废弃。所以到了touchBegan后,又变回空了。

对于strongView,一直都又强指针引用着生成的对象,当然不会被释放。

  • 这里再引申一下,为什么IBOutlet属性是weak。

原因:在xib中,控件会被加进控制器的view的subviews中,已经有强引用了。

类似的,对上面提到的weakViewB,笔者看过很多旧代码会这么写,道理是一样的。

    UIView *tempView = [[UIView alloc] init];
    [self.view addSubview:tempView];
    self.weakViewB = tempView;
复制代码
unsafe_unretained

在iOS5.0后,基本被weak替代。但是weak会额外消耗资源(后面说到weak原理会提到),所以并不全优于unsafe_unretained。

unsafe_unretained,不安全的所有权修饰符,和weak一样不会持有对象。其修饰的指针又名悬垂指针,当指针指向的对象被废弃时,指针成为了野指针,处理不好会导致程序崩溃。这点和weak不一样,weak指针在对象废弃时,会置为nil。

copy

通常,可变对象属性修饰符使用strong,不可变对象属性修饰符使用copy

  • copy和strong有什么区别?
  1. 对于不可变字符串
@property (nonatomic, strong) NSString *sStr;
@property (nonatomic, copy) NSString *cStr;

    NSString *tempStr = @"str";
    self.sStr = tempStr;
    self.cStr = tempStr;
    NSLog(@"tempStr = %p", tempStr);
    NSLog(@"sStr = %p", self.sStr);
    NSLog(@"cStr = %p", self.cStr);
复制代码

  1. 对于可变字符串
    NSMutableString *tempStr = [NSMutableString stringWithString:@"mutableStr"];
    self.sStr = tempStr;
    self.cStr = tempStr;
    NSLog(@"tempStr = %p", tempStr);
    NSLog(@"sStr = %p", self.sStr);
    NSLog(@"cStr = %p", self.cStr);
复制代码

可以看出对于不可变字符串,都是一样的,都持有原对象;对于可变字符串,strong持有原对象,copy的字符串新生成了一个可变对象。这也就是深浅拷贝的原理,不理解的可以看这篇文章iOS 图文并茂的带你了解深拷贝与浅拷贝

即存的方法是这样的。

- (void)setCStr:(NSString *)cStr {
    _cStr = [cStr copy];
}
复制代码

总之,当我们声明属性时,如果不希望它因为源对象(当源对象为可变对象时)的改变而改变,则用copy修饰。

存取方法名

  • getter=
  • setter=

可以重设方法名,但存方法名不建议修改。修改取方法名常用在BOOL类型属性中,如下:

  @property (nonatomic, getter=isOn) BOOL on;
复制代码

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值