iOS开发之高级面试题(二)
小编加了一个不错的面试题和iOS技巧分享群,883872094群资料可以自取。分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。
iOS开发之高级面试题(四)
9. @property中有哪些属性关键字?/ @property 后面可以有哪些修饰符?
属性可以拥有的特质分为四类:
-
原子性---
nonatomic
特质在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备 nonatomic 特质,则不使用同步锁。请注意,尽管没有名为“atomic”的特质(如果某属性不具备 nonatomic 特质,那它就是“原子的” ( atomic) ),但是仍然可以在属性特质中写明这一点,编译器不会报错。若是自己定义存取方法,那么就应该遵从与属性特质相符的原子性。
-
读/写权限---
readwrite(读写)
、readonly (只读)
-
内存管理语义---
assign
、strong
、weak
、unsafe_unretained
、copy
-
方法名---
getter=<name>
、setter=<name>
getter=<name>
的样式:@property (nonatomic, getter=isOn) BOOL on;
(setter=
这种不常用,也不推荐使用。故不在这里给出写法。)setter=<name>
一般用在特殊的情境下,比如:
在数据反序列化、转模型的过程中,服务器返回的字段如果以 init
开头,所以你需要定义一个 init
开头的属性,但默认生成的 setter
与 getter
方法也会以 init
开头,而编译器会把所有以 init
开头的方法当成初始化方法,而初始化方法只能返回 self 类型,因此编译器会报错。
这时你就可以使用下面的方式来避免编译器报错:
@property(nonatomic, strong, getter=p_initBy, setter=setP_initBy:)NSString *initBy;
另外也可以用关键字进行特殊说明,来避免编译器报错:
@property(nonatomic, readwrite, copy, null_resettable) NSString *initBy;
- (NSString *)initBy __attribute__((objc_method_family(none)));
- 不常用的:
nonnull
,null_resettable
,nullable
10. weak属性需要在dealloc中置nil么?
不需要。
在ARC环境无论是强指针还是弱指针都无需在 dealloc 设置为 nil , ARC 会自动帮我们处理
即便是编译器不帮我们做这些,weak也不需要在 dealloc 中置nil:
正如上文的:runtime 如何实现 weak 属性 中提到的:
我们模拟下 weak 的 setter 方法,应该如下:
- (void)setObject:(NSObject *)object
{
objc_setAssociatedObject(self, "object", object, OBJC_ASSOCIATION_ASSIGN);
[object cyl_runAtDealloc:^{
_object = nil;
}];
}
也即:
在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
11. @synthesize和@dynamic分别有什么作用?
- @property有两个对应的词,一个是 @synthesize,一个是 @dynamic。如果 @synthesize和 @dynamic都没写,那么默认的就是
@syntheszie var = _var;
- @synthesize 的语义是如果你没有手动实现 setter 方法和 getter 方法,那么编译器会自动为你加上这两个方法。
- @dynamic 告诉编译器:属性的 setter 与 getter 方法由用户自己实现,不自动生成。(当然对于 readonly 的属性只需提供 getter 即可)。假如一个属性被声明为 @dynamic var,然后你没有提供 @setter方法和 @getter 方法,编译的时候没问题,但是当程序运行到
instance.var = someVar
,由于缺 setter 方法会导致程序崩溃;或者当运行到someVar = var
时,由于缺 getter 方法同样会导致崩溃。编译时没问题,运行时才执行相应的方法,这就是所谓的动态绑定。
12. ARC下,不显式指定任何属性关键字时,默认的关键字都有哪些?
-
对应基本数据类型默认关键字是
atomic,readwrite,assign
-
对于普通的 Objective-C 对象
atomic,readwrite,strong
参考链接:
13. 用@property声明的NSString(或NSArray,NSDictionary)经常使用copy关键字,为什么?如果改用strong关键字,可能造成什么问题?
-
因为父类指针可以指向子类对象,使用 copy 的目的是为了让本对象的属性不受外界影响,使用 copy 无论给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
-
如果我们使用是 strong ,那么这个属性就有可能指向一个可变对象,如果这个可变对象在外部被修改了,那么会影响该属性.
copy 此特质所表达的所属关系与 strong 类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为 NSString 时,经常用此特质来保护其封装性,因为传递给设置方法的新值有可能指向一个 NSMutableString 类的实例。这个类是 NSString 的子类,表示一种可修改其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。只要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。
举例说明:
定义一个以 strong 修饰的 array:
@property (nonatomic ,readwrite, strong) NSArray *array;
然后进行下面的操作:
NSMutableArray *mutableArray = [[NSMutableArray alloc] init];
NSArray *array = @[ @1, @2, @3, @4 ];
self.array = mutableArray;
[mutableArray removeAllObjects];;
NSLog(@"%@",self.array);
[mutableArray addObjectsFromArray:array];
self.array = [mutableArray copy];
[mutableArray removeAllObjects];;
NSLog(@"%@",self.array);
打印结果如下所示:
2015-09-27 19:10:32.523 CYLArrayCopyDmo[10681:713670] (
)
2015-09-27 19:10:32.524 CYLArrayCopyDmo[10681:713670] (
1,
2,
3,
4
)
(详见仓库内附录的 Demo。)
为了理解这种做法,首先要知道,两种情况:
- 对非集合类对象的 copy 与 mutableCopy 操作;
- 对集合类对象的 copy 与 mutableCopy 操作。
1. 对非集合类对象的copy操作:
在非集合类对象中:对 immutable 对象进行 copy 操作,是指针复制,mutableCopy 操作时内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。用代码简单表示如下:
- [immutableObject copy] // 浅复制
- [immutableObject mutableCopy] //深复制
- [mutableObject copy] //深复制
- [mutableObject mutableCopy] //深复制
比如以下代码:
NSMutableString *string = [NSMutableString stringWithString:@"origin"];//copy
NSString *stringCopy = [string copy];
查看内存,会发现 string、stringCopy 内存地址都不一样,说明此时都是做内容拷贝、深拷贝。即使你进行如下操作:
[string appendString:@"origion!"]
stringCopy 的值也不会因此改变,但是如果不使用 copy,stringCopy 的值就会被改变。 集合类对象以此类推。 所以,
用 @property 声明 NSString、NSArray、NSDictionary 经常使用 copy 关键字,是因为他们有对应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
2、集合类对象的copy与mutableCopy
集合类对象是指 NSArray、NSDictionary、NSSet ... 之类的对象。下面先看集合类immutable对象使用 copy 和 mutableCopy 的一个例子:
NSArray *array = @[@[@"a", @"b"], @[@"c", @"d"]];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];
查看内容,可以看到 copyArray 和 array 的地址是一样的,而 mCopyArray 和 array 的地址是不同的。说明 copy 操作进行了指针拷贝,mutableCopy 进行了内容拷贝。但需要强调的是:此处的内容拷贝,仅仅是拷贝 array 这个对象,array 集合内部的元素仍然是指针拷贝。这和上面的非集合 immutable 对象的拷贝还是挺相似的,那么mutable对象的拷贝会不会类似呢?我们继续往下,看 mutable 对象拷贝的例子:
NSMutableArray *array = [NSMutableArray arrayWithObjects:[NSMutableString stringWithString:@"a"],@"b",@"c",nil];
NSArray *copyArray = [array copy];
NSMutableArray *mCopyArray = [array mutableCopy];
查看内存,如我们所料,copyArray、mCopyArray和 array 的内存地址都不一样,说明 copyArray、mCopyArray 都对 array 进行了内容拷贝。同样,我们可以得出结论:
在集合类对象中,对 immutable 对象进行 copy,是指针复制, mutableCopy 是内容复制;对 mutable 对象进行 copy 和 mutableCopy 都是内容复制。但是:集合对象的内容复制仅限于对象本身,对象元素仍然是指针复制。用代码简单表示如下:
[immutableObject copy] // 浅复制
[immutableObject mutableCopy] //单层深复制
[mutableObject copy] //单层深复制
[mutableObject mutableCopy] //单层深复制
这个代码结论和非集合类的非常相似。
参考链接:iOS 集合的深复制与浅复制
14. @synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo
的实例变量,那么还会自动合成新变量么?
在回答之前先说明下一个概念:
实例变量 = 成员变量 = ivar
这些说法,笔者下文中,可能都会用到,指的是一个东西。
正如 Apple官方文档 You Can Customize Synthesized Instance Variable Names 所说:
如果使用了属性的话,那么编译器就会自动编写访问属性所需的方法,此过程叫做“自动合成”( auto synthesis)。需要强调的是,这个过程由编译器在编译期执行,所以编辑器里看不到这些“合成方法” (synthesized method)的源代码。除了生成方法代码之外,编译器还要自动向类中添加适当类型的实例变量,并且在属性名前面加下划线,以此作为实例变量的名字。
@interface CYLPerson : NSObject
@property NSString *firstName;
@property NSString *lastName;
@end
在上例中,会生成两个实例变量,其名称分别为 _firstName
与 _lastName
。也可以在类的实现代码里通过 @synthesize
语法来指定实例变量的名字:
@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
上述语法会将生成的实例变量命名为 _myFirstName
与 _myLastName
,而不再使用默认的名字。一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。
总结下 @synthesize 合成实例变量的规则,有以下几点:
-
如果指定了成员变量的名称,会生成一个指定的名称的成员变量,
-
如果这个成员已经存在了就不再生成了.
-
如果是
@synthesize foo;
还会生成一个名称为foo的成员变量,也就是说:如果没有指定成员变量的名称会自动生成一个属性同名的成员变量,
-
如果是
@synthesize foo = _foo;
就不会生成成员变量了.
假如 property 名为 foo,存在一个名为 _foo
的实例变量,那么还会自动合成新变量么? 不会。如下图:
15. 在有了自动合成属性实例变量之后,@synthesize还有哪些使用场景?
回答这个问题前,我们要搞清楚一个问题,什么情况下不会autosynthesis(自动合成)?
-
同时重写了 setter 和 getter 时
-
重写了只读属性的 getter 时
-
使用了 @dynamic 时
-
在 @protocol 中定义的所有属性
-
在 category 中定义的所有属性
-
重载的属性
当你在子类中重载了父类中的属性,你必须 使用
@synthesize
来手动合成ivar。
除了后三条,对其他几个我们可以总结出一个规律:当你想手动管理 @property 的所有内容时,你就会尝试通过实现 @property 的所有“存取方法”(the accessor methods)或者使用 @dynamic
来达到这个目的,这时编译器就会认为你打算手动管理 @property,于是编译器就禁用了 autosynthesis(自动合成)。
因为有了 autosynthesis(自动合成),大部分开发者已经习惯不去手动定义ivar,而是依赖于 autosynthesis(自动合成),但是一旦你需要使用ivar,而 autosynthesis(自动合成)又失效了,如果不去手动定义ivar,那么你就得借助 @synthesize
来手动合成 ivar。
其实,@synthesize
语法还有一个应用场景,但是不太建议大家使用:
可以在类的实现代码里通过 @synthesize
语法来指定实例变量的名字:
@implementation CYLPerson
@synthesize firstName = _myFirstName;
@synthesize lastName = _myLastName;
@end
上述语法会将生成的实例变量命名为 _myFirstName
与 _myLastName
,而不再使用默认的名字。一般情况下无须修改默认的实例变量名,但是如果你不喜欢以下划线来命名实例变量,那么可以用这个办法将其改为自己想要的名字。笔者还是推荐使用默认的命名方案,因为如果所有人都坚持这套方案,那么写出来的代码大家都能看得懂。
举例说明:应用场景:
//
// .m文件
// http://weibo.com/luohanchenyilong/ (微博@iOS程序犭袁)
// https://github.com/ChenYilong
// 打开第14行和第17行中任意一行,就可编译成功
@import Foundation;
@interface CYLObject : NSObject
@property (nonatomic, copy) NSString *title;
@end
@implementation CYLObject {
// NSString *_title;
}
//@synthesize title = _title;
- (instancetype)init
{
self = [super init];
if (self) {
_title = @"微博@iOS程序犭袁";
}
return self;
}
- (NSString *)title {
return _title;
}
- (void)setTitle:(NSString *)title {
_title = [title copy];
}
@end
当你同时重写了 setter 和 getter 时,系统就不会生成 ivar(实例变量/成员变量)。这时候有两种选择:
- 要么如第14行:手动创建 ivar
- 要么如第17行:使用
@synthesize foo = _foo;
,关联 @property 与 ivar。
更多信息,请戳- 》 When should I use @synthesize explicitly?
16. objc中向一个nil对象发送消息将会发生什么?
在 Objective-C 中向 nil 发送消息是完全有效的——只是在运行时不会有任何作用:
-
如果一个方法返回值是一个对象,那么发送给nil的消息将返回0(nil)。例如:
Person * motherInlaw = [[aPerson spouse] mother];
如果 spouse 对象为 nil,那么发送给 nil 的消息 mother 也将返回 nil。
-
如果方法返回值为指针类型,其指针大小为小于或者等于sizeof(void*),float,double,long double 或者 long long 的整型标量,发送给 nil 的消息将返回0。
-
如果方法返回值为结构体,发送给 nil 的消息将返回0。结构体中各个字段的值将都是0。
-
如果方法的返回值不是上述提到的几种情况,那么发送给 nil 的消息的返回值将是未定义的。
具体原因如下:
objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
那么,为了方便理解这个内容,还是贴一个objc的源代码:
// runtime.h(类在runtime中的定义)
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
struct objc_class {
Class isa OBJC_ISA_AVAILABILITY; //isa指针指向Meta Class,因为Objc的类的本身也是一个Object,为了处理这个关系,runtime就创造了Meta Class,当给类发送[NSObject alloc]这样消息时,实际上是把这个消息发给了Class Object
#if !__OBJC2__
Class super_class OBJC2_UNAVAILABLE; // 父类
const char *name OBJC2_UNAVAILABLE; // 类名
long version OBJC2_UNAVAILABLE; // 类的版本信息,默认为0
long info OBJC2_UNAVAILABLE; // 类信息,供运行期使用的一些位标识
long instance_size OBJC2_UNAVAILABLE; // 该类的实例变量大小
struct objc_ivar_list *ivars OBJC2_UNAVAILABLE; // 该类的成员变量链表
struct objc_method_list **methodLists OBJC2_UNAVAILABLE; // 方法定义的链表
struct objc_cache *cache OBJC2_UNAVAILABLE; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率。
struct objc_protocol_list *protocols OBJC2_UNAVAILABLE; // 协议链表
#endif
} OBJC2_UNAVAILABLE;
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,然后在发送消息的时候,objc_msgSend方法不会返回值,所谓的返回内容都是具体调用时执行的。 那么,回到本题,如果向一个nil对象发送消息,首先在寻找对象的isa指针时就是0地址返回了,所以不会出现任何错误。
17. objc中向一个对象发送消息[obj foo]和objc_msgSend()
函数之间有什么关系?
具体原因同上题:该方法编译之后就是objc_msgSend()
函数调用.
我们用 clang 分析下,clang 提供一个命令,可以将Objective-C的源码改写成C++语言,借此可以研究下[obj foo]和objc_msgSend()
函数之间有什么关系。
以下面的代码为例,由于 clang 后的代码达到了10万多行,为了便于区分,添加了一个叫 iOSinit 方法,
//
// main.m
// http://weibo.com/luohanchenyilong/
// https://github.com/ChenYilong
// Copyright (c) 2015年 微博@iOS程序犭袁. All rights reserved.
//
#import "CYLTest.h"
int main(int argc, char * argv[]) {
@autoreleasepool {
CYLTest *test = [[CYLTest alloc] init];
[test performSelector:(@selector(iOSinit))];
return 0;
}
}
在终端中输入
clang -rewrite-objc main.m
就可以生成一个main.cpp
的文件,在最低端(10万4千行左右)
我们可以看到大概是这样的:
((void ()(id, SEL))(void )objc_msgSend)((id)obj, sel_registerName("foo"));
也就是说:
[obj foo];在objc动态编译时,会被转意为:
objc_msgSend(obj, @selector(foo));
。
18. 什么时候会报unrecognized selector的异常?
简单来说:
当调用该对象上某个方法,而该对象上没有实现这个方法的时候, 可以通过“消息转发”进行解决。
简单的流程如下,在上一题中也提到过:
objc是动态语言,每个方法在运行时会被动态转为消息发送,即:objc_msgSend(receiver, selector)。
objc在向一个对象发送消息时,runtime库会根据对象的isa指针找到该对象实际所属的类,然后在该类中的方法列表以及其父类方法列表中寻找方法运行,如果,在最顶层的父类中依然找不到相应的方法时,程序在运行时会挂掉并抛出异常unrecognized selector sent to XXX 。但是在这之前,objc的运行时会给出三次拯救程序崩溃的机会:
-
Method resolution
objc运行时会调用
+resolveInstanceMethod:
或者+resolveClassMethod:
,让你有机会提供一个函数实现。如果你添加了函数,那运行时系统就会重新启动一次消息发送的过程,否则 ,运行时就会移到下一步,消息转发(Message Forwarding)。 -
Fast forwarding
如果目标对象实现了
-forwardingTargetForSelector:
,Runtime 这时就会调用这个方法,给你把这个消息转发给其他对象的机会。 只要这个方法返回的不是nil和self,整个消息发送的过程就会被重启,当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。 这里叫Fast,只是为了区别下一步的转发机制。因为这一步不会创建任何新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。 -
Normal forwarding
这一步是Runtime最后一次给你挽救的机会。首先它会发送
-methodSignatureForSelector:
消息获得函数的参数和返回值类型。如果-methodSignatureForSelector:
返回nil,Runtime则会发出-doesNotRecognizeSelector:
消息,程序这时也就挂掉了。如果返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:
消息给目标对象。
为了能更清晰地理解这些方法的作用,git仓库里也给出了一个Demo,名称叫“ _objc_msgForward_demo
”,可运行起来看看。
19. 一个objc对象如何进行内存布局?(考虑有父类的情况)
-
所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中.
-
每一个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的
- 对象方法列表(对象能够接收的消息列表,保存在它所对应的类对象中)
- 成员变量的列表,
- 属性列表,
它内部也有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部还有一个superclass的指针,指向他的父类对象。
每个 Objective-C 对象都有相同的结构,如下图所示:
翻译过来就是
Objective-C 对象的结构图 |
---|
ISA指针 |
根类的实例变量 |
倒数第二层父类的实例变量 |
... |
父类的实例变量 |
类的实例变量 |
-
根对象就是NSobject,它的superclass指针指向nil
-
类对象既然称为对象,那它也是一个实例。类对象中也有一个isa指针指向它的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。
20. 一个objc对象的isa的指针指向什么?有什么作用?
指向他的类对象,从而可以找到对象上的方法
小编加了一个不错的面试题和iOS技巧分享群,883872094群资料可以自取
iOS开发之高级面试题(一)
小编加了一个不错的面试题和iOS技巧分享群,883872094群资料可以自取。分享BAT,阿里面试题、面试经验,讨论技术, 大家一起交流学习成长!希望帮助开发者少走弯路。
原文地址
图文来源于网络,如有侵权请联系小编删除