最近统一整理了一下ios中属性和关键词的一些知识点,这些问题不管是面试题还是面试官都是经常会问到的,很多人都是会用,但是被问到的时候答不上来,这就会造成一个尴尬的局面,所以这些东西还是需要我们认真理解和掌握的,这样我们才能更好的使用。
我还整理的一些其他方面的,我会陆续在简书中更新。如果有写得不对的地方和理解不到位的地方,欢迎大家指正,跟大家一块学习(欢迎评论区交流)。(整理来自一些书籍、博客等)
问题汇总目录(先看看自己能答出几道):
1.0 属性的关键字
2.0 浅拷贝和深拷贝的区别
3.0 copy和strong的区别
4.0 为什么不可变对象要用copy?
5.0 assign可以用于OC对象吗?(野指针)
6.0 weak如何实现自动赋nil
7.0 swift中如何理解copy-on-wirte?
8.0 swift中,如何在结构体、enum、extension实例方法中修改成员变量?
9.0 请你讲讲@proprety关键字的作用
10.0 这个写法会出什么问题: @property (copy) NSMutableArray *array?
11.0 @synthesize合成实例变量的规则是什么?假如property名为person,存在一个名为_ person的实例变量,那么还会自动合成新变量么?
12.0 在有了自动合成属性实例变量之后,@synthesize还有哪些使用场景?
13.0 @synthesize和@dynamic分别有什么作用?
14.0 @protocol 和 category 中如何使用 @property
15.0 __block和 __weak的区别和使用
16.0 __block的实现原理你知道吗?
17.0 block的实质是什么?有几种block?分别是怎样产生的?
18.0 代理和block的比较
1.0 属性的关键字
属性的关键字不管是在笔试还是面试的过程中经常会被问到,但很多人只是会使用,关于它的定义和使用方式的理解是不到位的,我觉得要想更好的使用这些关键字,这些是需要完全理解和掌握的。接下来我们就详细的说一下属性的关键字都有哪一些。
属性的关键字分为三类:
- 表示原子性的(也就是线程安全的)
atomic:修饰的对象会保证setter和getter的完整性,任何线程访问它都可以得到一个完整的初始化的对象。因为要保证线程的安全,所以速度会比较慢。atomic比nonatomic安全,但也不是绝对安全的,他有一个特点是单写多读,就是保证同一时间只有一个线程执行setter方法,但是可以有多个线程执行getter方法,因为在它的setter有一把自旋锁。所以它的getter方法不是线程安全的。
nonatomic:修饰的对象不保证setter和getter的完整性,所以,当多个线程访问它时,它可能返回未初始化的对象。但是我们一般都用的nonatomic,因为atomic的线程安全开销太大,影响性能,即使需要保证线程安全,我们也可以通过自己的代码控制,而不用atomic。 - 表示引用计数的
strong:表示指向并拥有该对象。其修饰的对象引用计数会增加1。该对象引用计数为零时才会被销毁。当然,强行将其设为nil也可以销毁它。
weak:表示指向但不拥有该对象。其修饰的对象引用计数不会增加。无须手动设置,该对象会自行在内存中销毁。
assign:修饰基本数据类型,例如CGFloat等,这些类型不是对象,是有栈进行进行内存管理的。
copy:建立一个引用计数为1的新对象,赋值时对传入值进行一份拷贝,所以使用copy关键字的时候,你将一个对象复制给该属性,该属性并不会持有那个对象,而是会创建一个新对象,并将那个对象的值拷贝给它。而使用copy关键字的对象必须要实现NSCopying协议。
unsafe_unretained(IOS5前):跟 weak 类似,声明一个弱引用,但是当引用计数为 0 时,变量不会自动设置为 nil,现在基本都用weak了。
@property (nonatomic, strong) NSString *str1;
@property (nonatomic,__unsafe_unretain) NSString *str2;
执行如下的代码
self.str1 = @"A";
self.str2 = self.str1;
self.str1 = nil;
NSLog(@"self.str2 = %@",self.str2);
这次根本就不用输出,在没有输出前,程序已经崩溃了,其实就是野指针造成的,为何会造成野指针呢?同于用unsafe_unretained声明的指针,由于 self.str1=nil已将内存释放掉了,但是str2并不知道已被释放了,所以是野指针。然后访问野指针的内存就造成crash. 所以尽量少用unsafe_unretained关键字。
- 读写权限
readwrite(默认):可读可写
readonly:只读,当你希望暴露出来的属性不能被外界修改时就需要申明为readonly。
在oc中基本数据类型的默认关键字是atomic,readwrite和assign,普通属性的默认关键字是atomic,readwrite和strong。
2.0浅拷贝和深拷贝的区别
- 浅拷贝:简单来说就是指针的拷贝,也就说是对于内存地址的复制,所以目标对象和源对象是指向同一块内存空间的。
- 深拷贝:简单来说就是内容拷贝,也就是说重新产生了一个新的对象,内存地址发生了变化。
copy和mutableCopy在使用中的总结:
- 对于NSMutableString、NSMutableArray这样的可变对象来说,使用copy和mutableCopy出来的对象都是深拷贝,而且都是单层拷贝,举个例子有字典a和字典b,让b = [a copy]和b = [a mutableCopy],创建出来的b字典跟a字典的地址是不一样的,但是b字典里面的字典元素和a字典里面的字典元素的地址是一样的,也就是说目标字典和源字典的元素指向的内存地址是一样的。
- 对于NSString、NSArray这样的不可变对象来说,使用copy方法是浅拷贝,使用mutableCopy方法是深拷贝。
- 使用copy方法返回的对象都是不可变的对象。
- 如果我们想要实现数组里面的对象也可以实现深拷贝,可以利用归档。
如果我们让一个类对象进行复制的操作,我们需要遵守NSCopying或者NSMutableCopying协议,没有遵守就会出现异常。对于我们自定义的类,我们需要实现copyWithZone: 和mutableCopyWithZone: 方法,系统的类不需要,因为系统已经帮我们实现了。
iOS开发——深拷贝与浅拷贝详解
3.0 copy和strong的区别
- strong赋值是多个指针指向同一个地址,而copy对于可变对象的复制是在内存中重新创建了一个对象,指针指向不同的地址。
- 使用strong修饰的属性可能会指向一个可变的对象,设置完属性之后,可变实例的值可能会在对象不知情的情况下遭到更改。但是使用copy返回的都是不可变的对象,不会出现这种情况。
4.0 为什么不可变对象要用copy
因为该对象可能指向一个可变的对象,若用strong的话,设置完属性之后,可变实例的值可能会在对象不知情的情况下遭到更改。用copy的话就会重新生成一个新的对象,新的对象不会受源对象值改变的影响。
5.0 assign可以用于OC对象吗?
assign可以修饰OC对象如NSString等类型对象,使用assign修饰不会更改所赋新值的引用计数,也不会改变旧值的引用计数,如果所赋新值的引用计数为零对象被销毁时,属性并不知道,编译器不会将其置为nil,指针仍然指向被销毁的内存,造成“野指针”,在堆上容易造成崩溃。所以一般用assign来修饰基本数据类型,基本数据类型在栈上,而栈上的内存系统会自动处理,不会造成“野指针”。
扩展
野指针:指针指向了一个已经被回收的对象。
僵尸对象: 一个已经被释放的对象。
- 野指针可以访问僵尸对象吗?
当野指针指向的僵尸对象所占用的空间还没有分配给别人的时候,这个时候其实是可以访问的。因为对象的数据还在。当野指针指向的对象所占用的空间分配给了别人的时候 这个时候访问就会出问题。所以,你不要通过1个野指针去访问1个僵尸对象。 - 如何避免僵尸对象报错?
当一个指针变成野指针之后,将野指针的值设为nil。
6.0 weak如何实现自动赋nil
通过weak表。runtime维护了一个weak表,用来存储所有的weak指针。weak表其实就是一个哈希表,key代表对象的地址,value代表一个数组,数组里面存放的是weak指针的地址,此地址的值其实就是所指对象的地址。
释放时调用clearDeallocating函数,clearDeallocating函数首先根据对象地址获取所有weak指针地址的数组,然后遍历这个数组把其中的数据设为nil,最后把这个entry从weak表中删除,最后清理对象的记录。
问题点
1.0 从weak指针的创建到释放存在哈希表的增删改查,所以存在一定的性能开销。
2.0 使用 Weak 指针的时候,应首先获取一个 Strong 指针再使用。倒不是为了防止在使用过程中,对象被回收,形成野指针。 这个不用担心,因为你使用了 Weak 指针,对象就会被加入到 autoreleasepool 中,可以放心使用。但是要注意的是,如果在一个代码块中频繁使用 Weak 指针,还是应首先获取一个 Strong 指针,否则这个对象会被一次又一次的加入 autoreleasepool 中,也存在一定的性能开销。
详细请看--底层解析weak的实现原理
7.0 swift中如何理解copy-on-wirte?
在swift中当值类型(例如struct)在进行复制时,复制后的对象和原对象实际上在内存中指向的是同一个对象,只有当复制后的对象进行修改时,才会重新创建一个新的对象。在swift中我们平时使用的Int、Double、Dictionary、Array等都是使用结
构体(struct)实现的,我们用Array举个例子:
// arr1是一个值类型
let arr1 = ["tianyao", "ningfei", "tian"]
// 此时arr2和arr1在内存中是同一个数组,并没有产生新的数组
var arr2 = arr1
// 此时arr2追加了一个元素,被修改,那么arr2在内存中重新产生了一个新的数组,而不是原来的arr1
arr2.append("change")
从上面的代码我们可以看得出来,复制的数组和源数组指向的是同一个地址,只有当两者中的一方发生修改时,才会重新开辟一个新的地址。这样的设计使得值类型可以多次复制而不需要开辟新的内存,只有当一方有变化时才会开辟新的内存。这样使得内存有更高效的使用。
8.0 swift中,如何在结构体、enum、extension实例方法中修改成员变量?
在官方文档中有这么一句话:
“Structures and enumerations are value types. By default, the properties of a value type cannot be modified from within its instance methods.”
大概的意思是:虽然结构体和枚举中能够定义自己的方法,但是默认情况下,实例方法中是不可以修改值类型的属性。
这个问题的关键点就是一个关键词mutating(可变化,转变)。使用mutating来修饰结构体、枚举和extension定义的方法,就可以修改成员变量了。
- 注意
在设计协议的时候,由于protocal可以被class和struct或enum实现,故而要考虑是否用mutating来修饰方法。
类中不存在这个问题,因为勒种可以随意修改自己的成员变量。
9.0 请你讲讲@proprety关键字的作用
- 快速的为实例变量创建存储器,并允许使用点语法使用存储器,提供了一个外接访问成员变量的接口。
- 存储器是用于获取和设置是实例变量的方法,getter是用于获取实例变量的存取器,setter是用于设置实例变量的存取器。
10.0 这个写法会出什么问题: @property (copy) NSMutableArray *array?
- 使用copy其实是复制一个不可变的NSArray的对象,所以在添加、删除和修改数组内元素的时候,会因为找不到对应的方法程序崩溃。
- 因为默认是atomic,所以会存在性能上面的问题。
默认的情况下,由编译器所合成的方法会通过锁定机制确保其原子性,如果属性具备 nonatomic 特质,则不使用同步锁。开发中,几乎所有属性都声明为 nonatomic。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全” ,若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。例如,一个线程在连续多次读取某属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为 atomic,也还是会读到不同的属性值。
11.0 @synthesize合成实例变量的规则是什么?假如property名为person,存在一个名为_ person的实例变量,那么还会自动合成新变量么?
如果使用了属性的话,那么编译器就会自动编写访问属性所需的方法,此过程叫做“自动合成”。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法” 的源代码。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。
@synthesize合成实例变量的规则如下:
- 如果指定了成员变量的名称,则生成一个指定名称的成员变量。
例如:
@interface TYPerson : NSObject
@property NSString *firstName;
@end
上面的例子会生成一个_firstName的成员变量,如果想让成员变量重新命名,也可以在类的实现方法里面通过@synthesize语法来指定成员变量的名字,如下
@implementation TYPerson
@synthesize firstName = _tianYao;
@end
此时就会生成一个_tianYao的成员变量。
- 如果是@synthesize firstName;会生成一个_firstName的成员变量。即:没有指定成员变量的名称,就会生成一个同属性名相同的有下划线的成员变量。(@synthesize foo = _foo)
- 如果这个成员已经存在了,就不会再生成了。
假如property名为person,存在一个名为_ person的实例变量,那么还会自动合成新变量么? 答案是不会的
如果存在_person的实例变量,那么你再创建一个_person的变量,系统就会报错。
附加:成员变量 = 实例变量 = Ivar
12.0 在有了自动合成属性实例变量之后,@synthesize还有哪些使用场景?
- 重写了 setter 和 getter 时
- 重写了只读属性的 getter 时
- 使用了 @dynamic 时
- 在 @protocol 中定义的所有属性
- 在 category 中定义的所有属性
- 重载的属性
- 通过 @synthesize 语法来指定实例变量的名字(不建议使用)
13.0 @synthesize和@dynamic分别有什么作用?
简单的来说一个是自动的,一个是手动的。
当你定义了一个属性,使用@synthesize的时候,如果你没有手动的实现getter和setter的方法,那么编译器会自动的帮你添加这两个方法。那么使用@dynamic,你需要手动实现它的getter和setter的方法,编译器不会帮你做这件事情。假如你没有手动实现这两个方法的话,程序在编译的过程中没有问题,但当程序运行到someVar = var的时候,由于缺少getter方法,会导致系统崩溃。同样当运行到instance.var = someVar的时候,由于缺少setter方法,也会导致系统崩溃。
此过程编译时候是没有问题的,运行时才执行相应的方法,这就是动态绑定。
14.0 @protocol 和 category 中如何使用 @property?
a.在@protocol中使用@property:
在@protocol中添加@property,其实就是声明了getter和setter的方法。我们需要在实现这个协议的类中,手动添加实例变量,并实现getter和setter的方法。
b.category中使用@property:
在category中添加property时, 在@implentation添加 getter 和 setter方法时, 由于category不能添加实例变量,我们可以通过两个方法来实现:
- 使用临时全局变量来替代成员变量;
- 使用runtime 关联对象,实现成员变量(方法:objc_getAssociatedObject())
15.0 __block和 __weak的区别和使用
- __block用于修饰变量,它是引用修饰,所以,其修饰的值是可以动态变化的,即可以被重新赋值的。__block用于修饰某些block中将要修改的外部变量。
- __weak也是用于修饰变量的,主要是用于防止循环引用。
附加:__block和__weak的使用都跟block有关。
16.0 __block的实现原理你知道吗?
block是不允许修改外部变量的值得,这里的外部变量的值,其实就是栈中指针的内存地址,__block 所起到的作用就是只要观察到该变量被 block 所持有,就将“外部变量”在栈中的内存地址放到了堆中。进而在block内部也可以修改外部变量的值。
Block不允许修改外部变量的值Apple这样设计,应该是考虑到了block的特殊性,block也属于“函数”的范畴,变量进入block,实际就是已经改变了作用域。在几个作用域之间进行切换时,如果不加上这样的限制,变量的可维护性将大大降低。又比如我想在block内声明了一个与外部同名的变量,此时是允许呢还是不允许呢?只有加上了这样的限制,这样的情景才能实现。
17.0 block的实质是什么?有几种block?分别是怎样产生的?
- block本质上也是一个OC对象,它内部也有个isa指针,封装了函数调用以及函数调用环境的OC对象,以及封装函数及其上下文的OC对象。
常见的block有三种类型:
- _NSConcreteStackBlock:访问了auto变量的block是__NSStackBlock __,放在栈区。
- _NSConcreteMallocBlock:[__NSStackBlock __ copy]操作就变成了__NSMallocBlock __,放在堆区。
- _NSConcreteGlobalBlock:没有访问auto变量的block是__NSGlobalBlock __ ,放在数据段
在GC环境下还有3种使用的_NSConcreteFinalizingBlock,_NSConcreteAutoBlock,_NSConcreteWeakBlockVariable,可以看看官方文档。
附加:
GC(garbage collection):一个跟踪过程,它传递性地跟踪指向当前使用的对象的所有指针,以便找到可以引用的所有对象,然后重新使用在此跟踪过程中未找到的任何堆内存。公共语言运行库垃圾回收器还压缩使用中的内存,以缩小堆所需要的工作空间 。
18.0 代理和block的比较
- block是集中代码块,而代理是分散代码块,所以block的话更加适合轻便、简单的回调操作,比如网络传输的回调。代理则更加适合公共接口比较多的情况,这样做更加易于解耦代码的架构。
- 关于运行成本的区别,block运行的成本高。block出栈时候,需要将数据从栈区复制到堆区,当然,如果是对象就是加计数,使用完成或者block置为nil后才消除;delegate只是保存了一个对象指针,直接回调,并没有额外消耗。相对于c的函数指针,只多了个查表动作。
注意:block容易出现循环引用。
附加:通知跟block和delegate相比,它可以一对多,也可以跨控制器进行传值,它是基于kvo来实现的。(kvo的话后续会说)