【从历年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。
![](https://img.haomeiwen.com/i7793356/1cf6c5f1fead96a6.png)
原子性
有两个关键字,默认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方法 |
苹果是通过引用计数表管理引用计数的,这图将帮助我们更好理解。
![](https://img.haomeiwen.com/i7793356/7204d6cfbf6782c0.png)
autorelease
NSAutoreleasePool对象:暂时持有放进池中的对象,当自身废弃时,池中的对象都释放一次。(底层通过栈放进对象,入栈时持有,池废弃前出栈且释放)
NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
id obj = [[NSObject alloc] init];
[obj autorelease];
// obj被最近的一个池持有
[pool drain];
// 释放池中暂时持有的对象
在开发中,我们很少用到NSAutoreleasePool,因为主循环NSRunLoop会对NSAutoreleasePool对象进行生成、持有和废弃处理。
![](https://img.haomeiwen.com/i7793356/568c56b4867fb42e.png)
例如以下代码,读入大量图像的同时改变其尺寸,系统会自动生成。
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);
}
![](https://img.haomeiwen.com/i7793356/061598b75314970c.png)
对于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有什么区别?
- 对于不可变字符串
@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);
![](https://img.haomeiwen.com/i7793356/a6a28527560d7b64.png)
- 对于可变字符串
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);
![](https://img.haomeiwen.com/i7793356/e0d4bcbe3414e300.png)
可以看出对于不可变字符串,都是一样的,都持有原对象;对于可变字符串,strong持有原对象,copy的字符串新生成了一个可变对象。这也就是深浅拷贝的原理,不理解的可以看这篇文章iOS 图文并茂的带你了解深拷贝与浅拷贝。
![](https://img.haomeiwen.com/i7793356/28ece3ec9d4be76e.png)
即存的方法是这样的。
- (void)setCStr:(NSString *)cStr {
_cStr = [cStr copy];
}
总之,当我们声明属性时,如果不希望它因为源对象(当源对象为可变对象时)的改变而改变,则用copy修饰。
存取方法名
- getter=<name>
- setter=<name>
可以重设方法名,但存方法名不建议修改。修改取方法名常用在BOOL类型属性中,如下:
@property (nonatomic, getter=isOn) BOOL on;
参考
- [1] Matt Galloway.Effective Objective-C 2.0:编写高质量iOS与OS X代码的52个有效方法[M].北京:机械工业出版社,2014.21—28.
- [2] Kazuki Sakamoto Tomohiko Furumoto.Objective-C高级编程.北京: 人民邮电出版社, 2013.1-78
- [3] acBool.iOS面试之@property
- [4] 江小凡.iOS关键字:nullable,nonnull,null_resettable,null_unspecified详解
网友评论