招聘一个靠谱的 iOS
近一年内陆续面试了不少人了,从面试者到面试官的转变让我对 iOS 招聘有了很多其它的感受。经过了前段时间的一大波面试。我们最终找到了志同道合的小伙伴。面试也临时告一段落了。总结以下试人过程中的感受,你也能够读到我们对简历、算法、性格、iOS 基础、底层知识的看法和一些常问的面试题。
一个靠谱的简历
简历很能反映一个人的性格和水平,相比于你在学校获得多少奖项。工作经历、项目经历、熟悉的技术等更加关键,假设还有博客和一些 Github 上的项目,好感度++,但记得在去面试前收拾下,我们真的会挨个文件 review 你的开源码的。
我们还喜欢关注一些细节。比方简历里keyword的拼写。看似无关紧要但很能反映出对自己的要求。常常见一个简历中iOS 这三个字母的拼写就出现 IOS、iOS、ios 三种的。很不能忍,再列举几个常见问题:
iPhone -> IPHONE IPhone
Xcode -> XCode xcode
Objective-C -> Object-C
JSON -> Json
HTTP -> Http
还有,注意中英文间用一个半角空格隔开,排版会美丽非常多,简历承载的不仅是内容,还有细节和态度,上面这些点往往都反映着面试者的代码风格、做事的认真程度。当然,简历写的非常美丽但面聊之后发现啥都不会的也有,甚至见过来面试上来就跟我说简历是假的。就想求个面试机会这样的 - -
面试
别迟到。别迟到。别迟到,重要的事说三遍。
有变动提前通知 HR。碰到过暂时有事没来,和谁都不说一声,打电话过去还要求改个时间的,这样的直接拜拜。
面试时最好准备纸、笔、简历。可能用不上。但非常能体现认真程度。有条件的话带着 Mac 和源代码,手机中装好全部在简历中出现的 App。
关于算法
我们是有用主义。iOS 开发中非常少须要自己写复杂的算法,所以不在面试考核标准中。
代码规范
这是一个重点考察项。以前在微博上发过一个风格纠错题:
也曾在面试时让人当场改过。槽点不少,可以有 10 处以上改动的就基本达到标准了(处女座的人在这方面表现都非常优秀
一个区分度非常大的面试题
考察一个面试者基础咋样。基本上问一个 @property 就够了:
- @property 后面能够有哪些修饰符?
- 什么情况使用 weak keyword,相比 assign 有什么不同?
- 怎么用 copy keyword?
- 这个写法会出什么问题:
@property (copy) NSMutableArray *array;
- 怎样让自己的类用 copy 修饰符?怎样重写带 copy keyword的 setter?
这一套问题区分度比較大,假设上面的问题都能回答正确,能够延伸问更深入点的:
- @property 的本质是什么?ivar、getter、setter 是怎样生成并加入到这个类中的
- @protocol 和 category 中怎样使用 @property
- runtime 怎样实现 weak 属性
每一个人擅长的领域不一样,我们通常会从简历上找自己写擅长的技术聊,假如自己并非非常熟,最好别写出来或扯出来,万一面试官刚好非常精通这里就露馅了。
Checklist
总结过些面试题。没坚持下去,后来把这些当 checklist,面试的时候实在没话聊的时候做个提醒。语言、框架、执行机制性质的:
[※]@property中有哪些属性keyword?
[※]weak属性须要在dealloc中置nil么?
[※※]@synthesize和@dynamic分别有什么作用?
[※※※]ARC下,不显式指定不论什么属性keyword时,默认的keyword都有哪些?
[※※※]用@property声明的NSString
(或NSArray
,NSDictionary
)常常使用copy
keyword,为什么?假设改用strong
keyword。可能造成什么问题?
[※※※]@synthesize合成实例变量的规则是什么?假如property名为foo。存在一个名为_foo的实例变量,那么还会自己主动合成新变量么?
[※※※※※]在有了自己主动合成属性实例变量之后。@synthesize还有哪些使用场景?
[※※]objc中向一个nil对象发送消息将会发生什么?
[※※※]objc中向一个对象发送消息[obj foo]
和objc_msgSend()
函数之间有什么关系?
[※※※]什么时候会报unrecognized selector
的异常?
[※※※※]一个objc对象怎样进行内存布局?(考虑有父类的情况)
[※※※※]一个objc对象的isa
的指针指向什么?有什么作用?
[※※※※]以下的代码输出什么?
1 2 3 4 5 6 7 8 9 10 11 |
@implementation Son : Father - (id)init { self = [super init]; if (self) { NSLog(@"%@", NSStringFromClass([self class])); NSLog(@"%@", NSStringFromClass([super class])); } return self; } @end |
[※※※※]runtime怎样通过selector找到相应的IMP地址?(分别考虑类方法和实例方法)
[※※※※]使用runtime Associate
方法关联的对象,须要在主对象dealloc的时候释放么?
[※※※※※]objc中的类方法和实例方法有什么本质差别和联系?
[※※※※※]_objc_msgForward
函数是做什么的,直接调用它将会发生什么?
[※※※※※]runtime怎样实现weak变量的自己主动置nil?
[※※※※※]是否能向编译后得到的类中添加实例变量?是否能向执行时创建的类中加入实例变量?为什么?
[※※※]runloop和线程有什么关系?
[※※※]runloop的mode作用是什么?
[※※※※]以+ scheduledTimerWithTimeInterval...
的方式触发的timer,在滑动页面上的列表时。timer会暂定回调。为什么?怎样解决?
[※※※※※]猜想runloop内部是怎样实现的?
[※]objc使用什么机制管理对象内存?
[※※※※]ARC通过什么方式帮助开发人员管理内存?
[※※※※]不手动指定autoreleasepool的前提下。一个autorealese对象在什么时刻释放?(比方在一个vc的viewDidLoad中创建)
[※※※※]BAD_ACCESS在什么情况下出现?
[※※※※※]苹果是怎样实现autoreleasepool的?
[※※]使用block时什么情况会发生引用循环。怎样解决?
[※※]在block内怎样改动block外部变量?
[※※※]使用系统的某些block api(如UIView
的block版本号写动画时),是否也考虑引用循环问题?
[※※]GCD的队列(dispatch_queue_t)分哪两种类型?
[※※※※]怎样用GCD同步若干个异步调用?(如依据若干个url异步载入多张图片。然后在都下载完毕后合成一张整图)
[※※※※]dispatch_barrier_async的作用是什么?
[※※※※※]苹果为什么要废弃dispatch_get_current_queue?
[※※※※※]下面代码执行结果怎样?
1 2 3 4 5 6 7 8 9 |
- (void)viewDidLoad { [super viewDidLoad]; NSLog(@"1"); dispatch_sync(dispatch_get_main_queue(), ^{ NSLog(@"2"); }); NSLog(@"3"); } |
[※※]addObserver:forKeyPath:options:context:
各个參数的作用各自是什么,observer中须要实现哪个方法才干获得KVO回调?
[※※※]怎样手动触发一个value的KVO
[※※※]若一个类有实例变量NSString *_foo
,调用setValue:forKey:
时,能够以foo
还是_foo
作为key?
[※※※※]KVC的keyPath
中的集合运算符怎样使用?
[※※※※]KVC和KVO的keyPath
一定是属性么?
[※※※※※]怎样关闭默认的KVO的默认实现。并进入自己定义的KVO实现?
[※※※※※]apple用什么方式实现对一个对象的KVO?
[※※]IBOutlet连出来的视图属性为什么能够被设置成weak
?
[※※※※※]IB中User Defined Runtime Attributes
怎样使用?
[※※※]怎样调试BAD_ACCESS错误
[※※※]lldb(gdb)经常使用的调试命令?
这些小题能够做为讨论的入口,依据面试者的回答再继续聊下去。当中一些题比較底层。是留给屌屌的面试者或者试探评级用的,普通情况并非重点的考察内容。
业务能力
毕竟寻常的工作内容不是 runtime、runloop。不怎么会用究竟层的黑魔法,80% 的时间都是和搭建页面、写业务逻辑、网络请求打交道。
要求面试者可以熟练构建 UI,我会找一个面试者做过的页面让他分析下页面结构、约束或者 frame 布局的连法和计算方法。有时也会让面试者说说 UITableView 经常使用的几个 delegate 和 data source 代理方法,动态 Cell 高度计算什么的。接下来,在手机里随便找一个 App 的页面,让面试者当场说说假设是他写应该用哪些 UI 组件和布局方式等。问几个问题后就能大概了解业务能力了,我们这边重度使用 IB 和 AutoLayout。假如面试者依旧使用代码码 UI 也到没关系。有“从良”意愿就非常好~
程序架构和一些设计模式假设面试者自己认为还不错的话也会聊聊。但跪求别说 Singleton 了,用的越多对水平就越表示怀疑。
对设计模式自信的我一般问一个问题,抽象工厂模式在 Cocoa SDK 中哪些类中体现?
架构上 MVC 还是 MVVM 还是 MVP 神马的到是能够聊聊各自的见解。反正也没有正确答案。仅仅要别搞的太离谱即可,比方有的人说网络请求和数据库的操作最好放到 UIView 的子类里面干。
网络请求、数据库等各家都有成熟的封装,基本知道咋用即可。除此之外,我们还会顺带的问下除了 iOS 开发外,还会什么其它编程语言、或者熟悉哪种脚本语言和 Terminal 操作等,甚至还问问是怎样翻墙- -,相信这些技能都是非常重要的。
性格
大家都是敲代码的。没啥必要用奇怪的、非常难的问题难为对方,更关键的还是性格。和 Team 的风格是不是和的来。一个心态良好的面试者须要有个寻常心。不傲娇也不跪舔。表达要正常。常常遇到问一个问题后一两分钟一直处于沉思状态。一句话不说。交流像挤牙膏一样,非常是憋屈。还有非常屌屌的,明明不懂仍然强行据理力争。镇得住面试官也罢,撞枪口上就别怪不客气了- - 。决定要不要一个人基本上聊 5 分钟就能够确定了。喜欢水到渠成的感觉。看对眼了挡都挡不住。
招聘告一段落,后面将会有更精彩的事情发生。最后,再次感谢大家的支持和对我的信任。
答案:
《招聘一个靠谱的 iOS》—參考答案(上)
说明:面试题来源是微博@我就叫Sunny怎么了的这篇博文:《招聘一个靠谱的 iOS》,当中共55题,除第一题为纠错题外。其它54道均为简答题。
博文中给出了高质量的面试题,可是未给出答案,我尝试着总结了下答案,分两篇发:这是上篇 ,下一篇文章将公布在这里。会把剩余问题总结下,而且进行勘误,欢迎各位指正文中的错误。请持续关注微博@iOS程序犭袁。(答案未经出题者校对,如有纰漏。请向微博@iOS程序犭袁指正。)
出题者简单介绍: 孙源(sunnyxx),眼下就职于百度。负责百度知道 iOS client的开发工作,对技术喜欢刨根问底和总结最佳实践,热爱分享和开源。维护一个叫 forkingdog 的开源小组。
1. 风格纠错题
改动方法有非常多种。现给出一种做演示样例:
以下对详细改动的地方。分两部分做下介绍:硬伤部分和优化部分 。由于硬伤部分没什么技术含量。为了节省大家时间。放在后面讲。大神请直接看优化部分。
优化部分
1)enum建议使用 NS_ENUM 和 NS_OPTIONS 宏来定义枚举类型,參见官方的 Adopting Modern Objective-C 一文:
1
2
3
4
5
|
//定义一个枚举 typedef NS_ENUM(NSInteger, CYLSex) { CYLSexMan, CYLSexWoman }; |
2)age属性的类型:应避免使用基本类型。建议使Foundation数据类型,相应关系例如以下:
1
2
3
4
|
int -> NSInteger unsigned -> NSUInteger float -> CGFloat 动画时间 -> NSTimeInterval |
同一时候考虑到age的特点,应使用NSUInteger。而非int。 这样做的是基于64-bit 适配考虑,详情可參考出题者的博文《64-bit Tips》。
3)假设project项目很庞大,须要拆分成不同的模块。能够在类、typedef宏命名的时候使用前缀。
4)doLogIn方法不应写在该类中:尽管LogIn的命名不太清晰,但笔者推測是login的意思,而登录操作属于业务逻辑,观察类名UserModel。以及属性的命名方式,应该使用的是MVC模式,并不是MVVM,在MVC中业务逻辑不应当写在Model中。(假设是MVVM,抛开命名规范,UserModel这个类可能相应的是用户注冊页面,假设有特殊的业务需求,比方:login相应的应当是注冊并登录的一个Button,出现login方法也可能是合理的。)
5)doLogIn方法命名不规范:加入了多余的动词前缀。 请牢记:
假设方法表示让对象运行一个动作,使用动词打头来命名,注意不要使用do,does这样的多余的keyword,动词本身的暗示就足够了。
6)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中不要用with来连接两个參数:withAge:应当换为age:。age:已经足以清晰说明參数的作用,也不建议用andAge::通常情况下,即使有类似withA:withB:的命名需求。也一般是使用withA:andB:这样的命名。用来表示方法运行了两个相对独立的操作(从设计上来说,这时候也能够拆分成两个独立的方法)。它不应该用作阐明有多个參数。比方以下的:
1
2
3
4
5
6
|
//错误,不要使用"and"来连接參数 - (int)runModalForDirectory:(NSString *)path andFile:(NSString *)name andTypes:(NSArray *)fileTypes; //错误,不要使用"and"来阐明有多个參数 - (instancetype)initWithName:(CGFloat)width andAge:(CGFloat)height; //正确。使用"and"来表示两个相对独立的操作 - (BOOL)openFile:(NSString *)fullPath withApplication:(NSString *)appName andDeactivate:(BOOL)flag; |
7)因为字符串值可能会改变,所以要把相关属性的“内存管理语义”声明为copy。(原因在下文有具体论述:用@property声明的NSString(或NSArray,NSDictionary)常常使用copykeyword,为什么?)
8)“性别”(sex)属性的:该类中仅仅给出了一种“初始化方法” (initializer)用于设置“姓名”(Name)和“年龄”(Age)的初始值,那怎样对“性别”(Sex)初始化?
Objective-C 有 designated 和 secondary 初始化方法的观念。 designated 初始化方法是提供全部的參数。secondary 初始化方法是一个或多个,而且提供一个或者很多其它的默认參数来调用 designated 初始化方法的初始化方法。
举例说明:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
// .m文件 // @implementation CYLUser - (instancetype)initWithName:(NSString *)name age:(int)age sex:(CYLSex)sex { if (self = [ super init]) { _name = [name copy]; _age = age; _sex = sex; } return self; } - (instancetype)initWithName:(NSString *)name age:(int)age { return [self initWithName:name age:age sex:nil]; } @end |
上面的代码中initWithName:age:sex: 就是 designated 初始化方法,另外的是 secondary 初始化方法。由于不过调用类实现的 designated 初始化方法。
由于出题者没有给出.m文件,所以有两种推測:1:本来打算仅仅设计一个designated 初始化方法。但漏掉了“性别”(sex)属性。那么终于的改动代码就是上文给出的第一种改动方法。2:不打算初始时初始化“性别”(sex)属性,打算后期再改动。假设是这样的情况,那么应该把“性别”(sex)属性设为readwrite属性。终于给出的改动代码应该是:
.h中暴露 designated 初始化方法,是为了方便子类化 (想了解很多其它。请戳--》 《禅与 Objective-C 编程艺术 (Zen and the Art of the Objective-C Craftsmanship 中文翻译)》。
)
9)依照接口设计的惯例。假设设计了“初始化方法” (initializer),也应当搭配一个快捷构造方法。而快捷构造方法的返回值,建议为instancetype。为保持一致性。init方法和快捷构造方法的返回类型最好都用instancetype。
10)假设基于第一种改动方法:既然该类中已经有一个“初始化方法” (initializer),用于设置“姓名”(Name)、“年龄”(Age)和“性别”(Sex)的初始值: 那么在设计相应@property时就应该尽量使用不可变的对象:其三个属性都应该设为“仅仅读”。用初始化方法设置好属性值之后。就不能再改变了。
在本例中,仍需声明属性的“内存管理语义”。于是能够把属性的定义改成这样
1
2
3
|
@property (nonatomic, copy, readonly) NSString *name; @property (nonatomic, assign, readonly) NSUInter age; @property (nonatomic, assign, readonly) CYLSex sex; |
因为是仅仅读属性,所以编译器不会为其创建相应的“设置方法”,即便如此,我们还是要写上这些属性的语义,以此表明初始化方法在设置这些属性值时所用的方式。
要是不写明语义的话。该类的调用者就不知道初始化方法里会拷贝这些属性,他们有可能会在调用初始化方法之前自行拷贝属性值。
这样的操作多余并且低效。
11)initUserModelWithUserName假设改为initWithName会更加简洁。并且足够清晰。
12)UserModel假设改为User会更加简洁,并且足够清晰。
13)UserSex假设改为Sex会更加简洁,并且足够清晰。
硬伤部分
1)在-和(void)之间应该有一个空格
2)enum中驼峰命名法和下划线命名法混用错误:枚举类型的命名规则和函数的命名规则同样:命名时使用驼峰命名法,勿使用下划线命名法。
3)enum左括号前加一个空格,或者将左括号换到下一行
4)enum右括号后加一个空格
5)UserModel :NSObject 应为UserModel : NSObject。也就是:右側少了一个空格。
6)@interface与@property属性声明中间应当间隔一行。
7)两个方法定义之间不须要换行。有时为了区分方法的功能也可间隔一行,但演示样例代码中间隔了两行。
8)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名与參数之间多了空格。
并且- 与(id)之间少了空格。
9)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中方法名与參数之间多了空格:(NSString*)name前多了空格。
10)-(id)initUserModelWithUserName: (NSString*)name withAge:(int)age;方法中(NSString*)name,应为(NSString *)name。少了空格。
11)doLogIn方法命名不清晰:笔者推測是login的意思。应该是粗心手误造成的。
12)第二个@property中assign和nonatomic调换位置。
2. 什么情况使用 weak keyword,相比 assign 有什么不同?
什么情况使用 weak keyword?
1)在ARC中,在有可能出现循环引用的时候,往往要通过让当中一端使用weak来解决,比方:delegate代理属性
2)自身已经对它进行一次强引用,没有必要再强引用一次,此时也会使用weak,自己定义IBOutlet控件属性一般也使用weak;当然,也能够使用strong。在下文也有论述:《IBOutlet连出来的视图属性为什么能够被设置成weak?》
不同点:
1)weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这样的属性设置新值时,设置方法既不保留新值,也不释放旧值。此特质同assign类似, 然而在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
而 assign 的“设置方法”仅仅会运行针对“纯量类型” (scalar type,比如 CGFloat 或 NSlnteger 等)的简单赋值操作。
2)assigin 能够用非OC对象,而weak必须用于OC对象
3. 怎么用 copy keyword?
用途:
1)NSString、NSArray、NSDictionary 等等常常使用copykeyword,是由于他们有相应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary;
2)block也常常使用copykeyword。详细原因见官方文档:Objects Use Properties to Keep Track of Blocks:
block使用copy是从MRC遗留下来的“传统”,在MRC中,方法内部的block是在栈区的,使用copy能够把它放到堆区.在ARC中写不写都行:对于block使用copy还是strong效果是一样的,但写上copy也无伤大雅,还能时刻提醒我们:编译器自己主动对block进行了copy操作。
以下做下解释: copy此特质所表达的所属关系与strong类似。
然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为NSString时,经经常使用此特质来保护其封装性,由于传递给设置方法的新值有可能指向一个NSMutableString类的实例。
这个类是NSString的子类,表示一种可改动其值的字符串,此时若是不拷贝字符串。那么设置完属性之后。字符串的值就可能会在对象不知情的情况下遭人更改。所以,这时就要拷贝一份“不可变” (immutable)的字符串,确保对象中的字符串值不会无意间变动。仅仅要实现属性所用的对象是“可变的” (mutable),就应该在设置新属性值时拷贝一份。
用@property声明 NSString、NSArray、NSDictionary 常常使用copykeyword,是由于他们有相应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作。为确保对象中的字符串值不会无意间变动。应该在设置新属性值时拷贝一份。
该问题在下文中也有论述:用@property声明的NSString(或NSArray,NSDictionary)常常使用copykeyword。为什么?假设改用strongkeyword,可能造成什么问题?
4. 这个写法会出什么问题: @property (copy) NSMutableArray *array;
两个问题:
1、加入,删除,改动数组内的元素的时候,程序会由于找不到相应的方法而崩溃.由于copy就是复制一个不可变NSArray的对象。
2、使用了atomic属性会严重影响性能。
第1条的相关原因在下文中有论述《用@property声明的NSString(或NSArray,NSDictionary)常常使用copykeyword。为什么?假设改用strongkeyword。可能造成什么问题?》 以及上文《怎么用 copy keyword?》也有论述。
第2条原因,例如以下:
该属性使用了同步锁。会在创建时生成一些额外的代码用于帮助编写多线程程序。这会带来性能问题,通过声明nonatomic能够节省这些尽管非常小可是不必要额外开销。
在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。
假设属性具备nonatomic特质,则不使用同步锁。请注意,虽然没有名为“atomic”的特质(假设某属性不具备nonatomic特质,那它就是“原子的”(atomic))。
在iOS开发中,你会发现。差点儿全部属性都声明为nonatomic。
普通情况下并不要求属性必须是“原子的”,由于这并不能保证“线程安全” ( thread safety),若要实现“线程安全”的操作。还需採用更为深层的锁定机制才行。
比如,一个线程在连续多次读取某属性值的过程中有别的线程在同一时候改写该值。那么即便将属性声明为atomic。也还是会读到不同的属性值。
因此,开发iOS程序时一般都会使用nonatomic属性。可是在开发Mac OS X程序时。使用 atomic属性通常都不会有性能瓶颈。
5. 怎样让自己的类用 copy 修饰符?怎样重写带 copy keyword的 setter?
若想令自己所写的对象具有拷贝功能,则需实现NSCopying协议。假设自己定义的对象分为可变版本号与不可变版本号,那么就要同一时候实现NSCopyiog与NSMutableCopying协议。
详细步骤:
1)需声明该类遵从NSCopying协议
2)实现NSCopying协议。该协议仅仅有一个方法:
1
|
- (id)copyWithZone: (NSZone*) zone |
注意:一提到让自己的类用 copy 修饰符。我们总是想覆写copy方法,事实上真正须要实现的却是“copyWithZone”方法。
以第一题的代码为例:
然后实现协议中规定的方法:
但在实际的项目中。不可能这么简单,遇到更复杂一点。比方类对象中的数据结构可能并未在初始化方法中设置好。须要另行设置。
举个样例,假如CYLUser中含有一个数组。与其它CYLUser对象建立或解除朋友关系的那些方法都须要操作这个数组。那么在这样的情况下。你得把这个包括朋友对象的数组也一并拷贝过来。以下列出了实现此功能所需的所有代码:
// .m文件
以上做法能满足主要的需求,可是也有缺陷:假设你所写的对象须要深拷贝。那么可考虑新增一个专门运行深拷贝的方法。
【注:深浅拷贝的概念,在下文中有介绍。详见下文的:用@property声明的NSString(或NSArray,NSDictionary)常常使用copykeyword。为什么?假设改用strongkeyword,可能造成什么问题?】
在样例中,存放朋友对象的set是用“copyWithZooe:”方法来拷贝的,这样的浅拷贝方式不会逐个复制set中的元素。若须要深拷贝的话。则可像以下这样,编写一个专供深拷贝所用的方法:
1
2
3
4
5
6
7
8
9
|
- (id)deepCopy { CYLUser *copy = [[[self copy] allocWithZone:zone] initWithName:_name age:_age sex:sex]; copy->_friends = [[NSMutableSet alloc] initWithSet:_friends copyItems:YES]; return copy; } |
至于怎样重写带 copy keyword的 setter这个问题,
假设抛开本例来回答的话,例如以下:
1
2
3
|
- (void)setName:(NSString *)name { _name = [name copy]; } |
假设单单就上文的代码而言。我们不须要也不能重写name的 setter :由于是name是仅仅读属性。所以编译器不会为其创建相应的“设置方法”,用初始化方法设置好属性值之后。就不能再改变了。( 在本例中。之所以还要声明属性的“内存管理语义”--copy,是由于:假设不写copy。该类的调用者就不知道初始化方法里会拷贝这些属性。他们有可能会在调用初始化方法之前自行拷贝属性值。这样的操作多余而低效。)。
那怎样确保name被copy?在初始化方法(initializer)中做:
1
2
3
4
5
6
7
8
9
10
11
|
- (instancetype)initWithName:(NSString *)name age:(int)age sex:(CYLSex)sex { if (self = [ super init]) { _name = [name copy]; _age = age; _sex = sex; _friends = [[NSMutableSet alloc] init]; } return self; } |
6. @property 的本质是什么?ivar、getter、setter 是怎样生成并加入到这个类中的。
@property 的本质是什么?
@property = ivar + getter + setter;
以下解释下:
“属性” (property)有两大概念:ivar(实例变量)、存取方法(access method = getter + setter)。
“属性” (property)作为 Objective-C 的一项特性。基本的作用就在于封装对象中的数据。 Objective-C 对象一般会把其所须要的数据保存为各种实例变量。实例变量一般通过“存取方法”(access method)来訪问。当中,“获取方法” (getter)用于读取变量值。而“设置方法” (setter)用于写入变量值。这个概念已经定型,而且经由“属性”这一特性而成为Objective-C 2.0的一部分。 而在正规的 Objective-C 编码风格中,存取方法有着严格的命名规范。 正由于有了这样的严格的命名规范,所以 Objective-C 这门语言才干依据名称自己主动创建出存取方法。事实上也能够把属性当做一种keyword。其表示:
编译器会自己主动写出一套存取方法,用以訪问给定类型中具有给定名称的变量。 所以你也能够这么说:
@property = getter + setter;
比如以下这个类:
1
2
3
4
|
@interface Person : NSObject @property NSString *firstName; @property NSString *lastName; @end |
上述代码写出来的类与以下这样的写法等效:
1
2
3
4
5
6
|
@interface Person : NSObject - (NSString *)firstName; - (void)setFirstName:(NSString *)firstName; - (NSString *)lastName; - (void)setLastName:(NSString *)lastName; @end |
ivar、getter、setter 是怎样生成并加入到这个类中的?
“自己主动合成”( autosynthesis)
完毕属性定义后。编译器会自己主动编写訪问这些属性所需的方法,此过程叫做“自己主动合成”( autosynthesis)。须要强调的是,这个过程由编译 器在编译期运行。所以编辑器里看不到这些“合成方法”(synthesized method)的源码。
除了生成方法代码 getter、setter 之外,编译器还要自己主动向类中加入适当类型的实例变量,而且在属性名前面加下划线,以此作为实例变量的名字。
在前例中,会生成两个实例变量,其名称分别为 _firstName与_lastName。
也能够在类的实现代码里通过 @synthesize语法来指定实例变量的名字.
1
2
3
4
|
@implementation Person @synthesize firstName = _myFirstName; @synthesize lastName = myLastName; @end |
我为了搞清属性是怎么实现的,以前反编译过相关的代码,大致生成了五个东西:
1)OBJC_IVAR_$类名$属性名称 :该属性的“偏移量” (offset)。这个偏移量是“硬编码” (hardcode)。表示该变量距离存放对象的内存区域的起始地址有多远。
2)setter与getter方法相应的实现函数
3)ivar_list :成员变量列表
4)method_list :方法列表
5)prop_list :属性列表
也就是说我们每次在添加一个属性,系统都会在ivar_list中加入一个成员变量的描写叙述,在method_list中添加setter与getter方法的描写叙述,在属性列表中添加一个属性的描写叙述,然后计算该属性在对象中的偏移量,然后给出setter与getter方法相应的实现,在setter方法中从偏移量的位置開始赋值,在getter方法中从偏移量開始取值,为了可以读取正确字节数,系统对象偏移量的指针类型进行了类型强转.
7. @protocol 和 category 中怎样使用 @property
1)在protocol中使用property仅仅会生成setter和getter方法声明,我们使用属性的目的,是希望遵守我协议的对象能实现该属性
2)category 使用 @property 也是仅仅会生成setter和getter方法的声明,假设我们真的须要给category添加属性的实现,须要借助于执行时的两个函数:
①objc_setAssociatedObject
②objc_getAssociatedObject
8. runtime 怎样实现 weak 属性
要实现weak属性,首先要搞清楚weak属性的特点:
weak 此特质表明该属性定义了一种“非拥有关系” (nonowning relationship)。为这样的属性设置新值时,设置方法既不保留新值。也不释放旧值。此特质同assign类似。 然而在属性所指的对象遭到摧毁时。属性值也会清空(nil out)。
那么runtime怎样实现weak变量的自己主动置nil?
runtime 对注冊的类, 会进行布局,对于 weak 对象会放入一个 hash 表中。 用 weak 指向的对象内存地址作为 key,当此对象的引用计数为0的时候会 dealloc,假如 weak 指向的对象内存地址是a,那么就会以a为键。 在这个 weak 表中搜索。找到全部以a为键的 weak 对象。从而设置为 nil。
我们能够设计一个函数(伪代码)来表示上述机制:
objc_storeWeak(&a, b)函数:
objc_storeWeak函数把第二个參数--赋值对象(b)的内存地址作为键值key,将第一个參数--weak修饰的属性变量(a)的内存地址(&a)作为value。注冊到 weak 表中。
假设第二个參数(b)为0(nil),那么把变量(a)的内存地址(&a)从weak表中删除,
你能够把objc_storeWeak(&a, b)理解为:objc_storeWeak(value, key),而且当key变nil。将value置nil。
在b非nil时,a和b指向同一个内存地址。在b变nil时,a变nil。此时向a发送消息不会崩溃:在Objective-C中向nil发送消息是安全的。
而假设a是由assign修饰的,则: 在b非nil时,a和b指向同一个内存地址,在b变nil时,a还是指向该内存地址。变野指针。此时向a发送消息极易崩溃。
以下我们将基于objc_storeWeak(&a, b)函数。使用伪代码模拟“runtime怎样实现weak属性”:
1
2
3
4
5
6
7
|
// 使用伪代码模拟:runtime怎样实现weak属性 id obj1; objc_initWeak(&obj1, obj); /*obj引用计数变为0。变量作用域结束*/ objc_destroyWeak(&obj1); |
以下对用到的两个方法objc_initWeak和objc_destroyWeak做下解释:
整体说来,作用是: 通过objc_initWeak函数初始化“附有weak修饰符的变量(obj1)”,在变量作用域结束时通过objc_destoryWeak函数释放该变量(obj1)。
以下分别介绍下方法的内部实现:
objc_initWeak函数的实现是这种:在将“附有weak修饰符的变量(obj1)”初始化为0(nil)后,会将“赋值对象”(obj)作为參数,调用objc_storeWeak函数。
1
2
|
obj1 = 0; obj_storeWeak(&obj1, obj); |
也就是说:
weak 修饰的指针默认值是 nil (在Objective-C中向nil发送消息是安全的)
然后obj_destroyWeak函数将0(nil)作为參数。调用objc_storeWeak函数。
1
|
objc_storeWeak(&obj1, 0); |
前面的源码与下列源码同样。
1
2
3
4
5
6
7
8
|
// 使用伪代码模拟:runtime怎样实现weak属性 id obj1; obj1 = 0; objc_storeWeak(&obj1, obj); /* ... obj的引用计数变为0。被置nil ... */ objc_storeWeak(&obj1, 0); |
objc_storeWeak函数把第二个參数--赋值对象(obj)的内存地址作为键值,将第一个參数--weak修饰的属性变量(obj1)的内存地址注冊到 weak 表中。假设第二个參数(obj)为0(nil)。那么把变量(obj1)的地址从weak表中删除。在后面的相关一题会具体解释。
使用伪代码是为了方便理解,以下我们“真枪实弹”地实现下:
怎样让不使用weak修饰的@property,拥有weak的效果。
我们从setter方法入手:
1
2
3
4
5
6
7
|
- (void)setObject:(NSObject *)object { objc_setAssociatedObject(self, "object" , object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }]; } |
也就是有两个步骤:
1)在setter方法中做例如以下设置:
1
|
objc_setAssociatedObject(self, "object" , object, OBJC_ASSOCIATION_ASSIGN); |
2)在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。做到这点。相同要借助runtime:
1
2
3
4
5
6
7
8
|
//要销毁的目标对象 id objectToBeDeallocated; //能够理解为一个“事件”:当上面的目标对象销毁时。同一时候要发生的“事件”。 id objectWeWantToBeReleasedWhenThatHappens; objc_setAssociatedObject(objectToBeDeallocted, someUniqueKey, objectWeWantToBeReleasedWhenThatHappens, OBJC_ASSOCIATION_RETAIN); |
知道了思路,我们就開始实现cyl_runAtDealloc方法,实现过程分两部分:
第一部分:创建一个类。能够理解为一个“事件”:当目标对象销毁时。同一时候要发生的“事件”。
借助block运行“事件”。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
// .h文件 // 这个类,能够理解为一个“事件”:当目标对象销毁时,同一时候要发生的“事件”。 typedef void (^voidBlock)(void); @interface CYLBlockExecutor : NSObject - (id)initWithBlock:(voidBlock)block; @end // .m文件 // 这个类。能够理解为一个“事件”:当目标对象销毁时。同一时候要发生的“事件”。借助block运行“事件”。 #import "CYLBlockExecutor.h" @interface CYLBlockExecutor() { voidBlock _block; } @implementation CYLBlockExecutor - (id)initWithBlock:(voidBlock)aBlock { self = [ super init]; if (self) { _block = [aBlock copy]; } return self; } - (void)dealloc { _block ? _block() : nil; } @end |
第二部分:核心代码:利用runtime实现cyl_runAtDealloc方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
// CYLNSObject+RunAtDealloc.h文件 // 利用runtime实现cyl_runAtDealloc方法 #import "CYLBlockExecutor.h" const void *runAtDeallocBlockKey = &runAtDeallocBlockKey; @interface NSObject (CYLRunAtDealloc) - (void)cyl_runAtDealloc:(voidBlock)block; @end // CYLNSObject+RunAtDealloc.m文件 // 利用runtime实现cyl_runAtDealloc方法 #import "CYLNSObject+RunAtDealloc.h" #import "CYLBlockExecutor.h" @implementation NSObject (CYLRunAtDealloc) - (void)cyl_runAtDealloc:(voidBlock)block { if (block) { CYLBlockExecutor *executor = [[CYLBlockExecutor alloc] initWithBlock:block]; objc_setAssociatedObject(self, runAtDeallocBlockKey, executor, OBJC_ASSOCIATION_RETAIN); } } @end |
用法: 导入
1
|
#import "CYLNSObject+RunAtDealloc.h" |
然后就能够使用了:
1
2
3
4
|
NSObject *foo = [[NSObject alloc] init]; [foo cyl_runAtDealloc:^{ NSLog(@ "正在释放foo!" ); }]; |
假设对cyl_runAtDealloc的实现原理有兴趣,能够看下这篇博文 Fun With the Objective-C Runtime: Run Code at Deallocation of Any Object
9. @property中有哪些属性keyword?/ @property 后面能够有哪些修饰符?
属性能够拥有的特质分为四类:
-
原子性---nonatomic特质
在默认情况下,由编译器合成的方法会通过锁定机制确保其原子性(atomicity)。假设属性具备nonatomic特质,则不使用同步锁。请注意,虽然没有名为“atomic”的特质(假设某属性不具备nonatomic特质,那它就是“原子的” ( atomic) ),可是仍然能够在属性特质中写明这一点,编译器不会报错。若是自定义存取方法。那么就应该遵从与属性特质相符的原子性。
-
读/写权限---readwrite(读写)、readooly (仅仅读)
-
内存管理语义---assign、strong、 weak、unsafe_unretained、copy
-
方法名---getter=、setter=
getter=的样式:
1
|
@property (nonatomic, getter=isOn) BOOL on; |
( setter=这样的不经常使用,也不推荐使用。故不在这里给出写法。
)
-
不经常使用的:nonnull,null_resettable,nullable
10. weak属性须要在dealloc中置nil么?
不须要。
在ARC环境不管是强指针还是弱指针都无需在deallco设置为nil。ARC会自己主动帮我们处理。
即便是编译器不帮我们做这些,weak也不须要在dealloc中置nil:
正如上文的:runtime 怎样实现 weak 属性 中提到的:
我们模拟下weak的setter方法,应该例如以下:
1
2
3
4
5
6
7
|
- (void)setObject:(NSObject *)object { objc_setAssociatedObject(self, "object" , object, OBJC_ASSOCIATION_ASSIGN); [object cyl_runAtDealloc:^{ _object = nil; }]; } |
也即:在属性所指的对象遭到摧毁时,属性值也会清空(nil out)。
11. @synthesize和@dynamic分别有什么作用?
1)@property有两个相应的词,一个是@synthesize,一个是@dynamic。
假设@synthesize和@dynamic都没写,那么默认的就是@syntheszie var = _var;
2)@synthesize的语义是假设你没有手动实现setter方法和getter方法,那么编译器会自己主动为你加上这两个方法。
3)@dynamic告诉编译器:属性的setter与getter方法由用户自己实现,不自己主动生成。(当然对于readonly的属性仅仅需提供getter就可以)。
假如一个属性被声明为@dynamic var,然后你没有提供@setter方法和@getter方法,编译的时候没问题,可是当程序执行到instance.var = someVar。因为缺setter方法会导致程序崩溃;或者当执行到 someVar = var时。因为缺getter方法相同会导致崩溃。编译时没问题。执行时才执行对应的方法,这就是所谓的动态绑定。
12. ARC下,不显式指定不论什么属性keyword时,默认的keyword都有哪些?
-
相应基本数据类型默认keyword是
atomic,readwrite,assign
-
对于普通的OC对象
atomic,readwrite,strong
參考链接:
13. 用@property声明的NSString(或NSArray,NSDictionary)常常使用copykeyword,为什么?假设改用strongkeyword。可能造成什么问题?
1)由于父类指针能够指向子类对象,使用copy的目的是为了让本对象的属性不受外界影响,使用copy不管给我传入是一个可变对象还是不可对象,我本身持有的就是一个不可变的副本.
2)假设我们使用是strong,那么这个属性就有可能指向一个可变对象,假设这个可变对象在外部被改动了,那么会影响该属性.
copy此特质所表达的所属关系与strong类似。然而设置方法并不保留新值,而是将其“拷贝” (copy)。 当属性类型为NSString时。经经常使用此特质来保护其封装性。由于传递给设置方法的新值有可能指向一个NSMutableString类的实例。这个类是NSString的子类。表示一种可改动其值的字符串,此时若是不拷贝字符串,那么设置完属性之后,字符串的值就可能会在对象不知情的情况下遭人更改。
所以,这时就要拷贝一份“不可变” (immutable)的字符串。确保对象中的字符串值不会无意间变动。
仅仅要实现属性所用的对象是“可变的” (mutable)。就应该在设置新属性值时拷贝一份。
为了理解这样的做法。首先要知道。对非集合类对象的copy操作:
在非集合类对象中:对immutable对象进行copy操作,是指针复制,mutableCopy操作时内容复制。对mutable对象进行copy和mutableCopy都是内容复制。
用代码简单表演示样例如以下:
-
[immutableObject copy] // 浅复制
-
[immutableObject mutableCopy] //深复制
-
[mutableObject copy] //深复制
-
[mutableObject mutableCopy] //深复制
比方下面代码:
1
2
|
NSMutableString *string = [NSMutableString stringWithString:@ "origin" ]; //copy NSString *stringCopy = [string copy]; |
查看内存,会发现 string、stringCopy 内存地址都不一样,说明此时都是做内容拷贝、深拷贝。即使你进行例如以下操作:
1
|
[string appendString:@ "origion!" ] |
stringCopy的值也不会因此改变。可是假设不使用copy,stringCopy的值就会被改变。
集合类对象以此类推。 所以。
用@property声明 NSString、NSArray、NSDictionary 常常使用copykeyword,是由于他们有相应的可变类型:NSMutableString、NSMutableArray、NSMutableDictionary,他们之间可能进行赋值操作,为确保对象中的字符串值不会无意间变动,应该在设置新属性值时拷贝一份。
參考链接:iOS 集合的深复制与浅复制
14. @synthesize合成实例变量的规则是什么?假如property名为foo,存在一个名为_foo的实例变量,那么还会自己主动合成新变量么?
在回答之前先说明下一个概念:
实例变量 = 成员变量 = ivar
这些说法,笔者下文中。可能都会用到,指的是一个东西。
正如 Apple官方文档 You Can Customize Synthesized Instance Variable Names 所说:
假设使用了属性的话,那么编译器就会自己主动编写訪问属性所需的方法,此过程叫做“自己主动合成”( auto synthesis)。须要强调的是,这个过程由编译器在编译期运行。所以编辑器里看不到这些“合成方法” (synthesized method)的源码。除了生成方法代码之外,编译器还要自己主动向类中加入适当类型的实例变量。而且在属性名前面加下划线,以此作为实例变量的名字。
1
2
3
4
|
@interface CYLPerson : NSObject @property NSString *firstName; @property NSString *lastName; @end |
在上例中,会生成两个实例变量,其名称分别为 _firstName与_lastName。也能够在类的实现代码里通过@synthesize语法来指定实例变量的名字:
1
2
3
4
|
@implementation CYLPerson @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end |
上述语法会将生成的实例变量命名为_myFirstName与_myLastName,而不再使用默认的名字。
普通情况下无须改动默认的实例变量名,可是假设你不喜欢下面划线来命名实例变量,那么能够用这个办法将其改为自己想要的名字。
笔者还是推荐使用默认的命名方案,由于假设全部人都坚持这套方案。那么写出来的代码大家都能看得懂。
总结下@synthesize合成实例变量的规则,有下面几点:
1)假设指定了成员变量的名称,会生成一个指定的名称的成员变量,
2)假设这个成员已经存在了就不再生成了.
3)假设是 @synthesize foo; 还会生成一个名称为foo的成员变量。也就是说:假设没有指定成员变量的名称会自己主动生成一个属性同名的成员变量。
4)假设是 @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语法来指定实例变量的名字:
1
2
3
4
|
@implementation CYLPerson @synthesize firstName = _myFirstName; @synthesize lastName = _myLastName; @end |
上述语法会将生成的实例变量命名为_myFirstName与_myLastName,而不再使用默认的名字。
普通情况下无须改动默认的实例变量名,可是假设你不喜欢下面划线来命名实例变量。那么能够用这个办法将其改为自己想要的名字。
笔者还是推荐使用默认的命名案,由于假设全部人都坚持这套方案。那么写出来的代码大家都能看得懂。
举例说明:应用场景:
结果编译器报错:
当你同一时候重写了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)。
比如:
1
|
Person * motherInlaw = [[aPerson spouse] mother]; |
假设spouse对象为nil,那么发送给nil的消息mother也将返回nil。
1)假设方法返回值为指针类型。其指针大小为小于或者等于sizeof(void*),float,double,long double 或者long long的整型标量,发送给nil的消息将返回0。
2)假设方法返回值为结构体,发送给nil的消息将返回0。结构体中各个字段的值将都是0。
3)假设方法的返回值不是上述提到的几种情况。那么发送给nil的消息的返回值将是没有定义的。
详细原因例如以下:
objc是动态语言,每一个方法在执行时会被动态转为消息发送。即:objc_msgSend(receiver, selector)。
那么,为了方便理解这个内容,还是贴一个objc的源码:
1
2
3
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
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()函数调用.假设我没有记错的大概是这种:
1
|
((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:,让你有机会提供一个函数实现。
假设你加入了函数并返回 YES,那执行时系统就会又一次启动一次消息发送的过程。假设 resolve 方法返回 NO ,执行时就会移到下一步,消息转发(Message Forwarding)。
-
Fast forwarding
假设目标对象实现了-forwardingTargetForSelector:,Runtime 这时就会调用这种方法,给你把这个消息转发给其它对象的机会。 仅仅要这种方法返回的不是nil和self。整个消息发送的过程就会被重新启动。当然发送的对象会变成你返回的那个对象。否则,就会继续Normal Fowarding。
这里叫Fast。仅仅是为了差别下一步的转发机制。
由于这一步不会创建不论什么新的对象,但下一步转发会创建一个NSInvocation对象,所以相对更快点。
-
Normal forwarding
这一步是Runtime最后一次给你拯救的机会。首先它会发送-methodSignatureForSelector:消息获得函数的參数和返回值类型。假设-methodSignatureForSelector:返回nil,Runtime则会发出-doesNotRecognizeSelector:消息。程序这时也就挂掉了。假设返回了一个函数签名,Runtime就会创建一个NSInvocation对象并发送-forwardInvocation:消息给目标对象。
19. 一个objc对象怎样进行内存布局?(考虑有父类的情况)
-
全部父类的成员变量和自己的成员变量都会存放在该对象所相应的存储空间中.
-
每个对象内部都有一个isa指针,指向他的类对象,类对象中存放着本对象的
1)对象方法列表(对象可以接收的消息列表,保存在它所相应的类对象中)
2)成员变量的列表
3)属性列表
它内部也有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部另一个superclass的指针,指向他的父类对象。
1)根对象就是NSobject,它的superclass指针指向nil。
2)类对象既然称为对象,那它也是一个实例。
类对象中也有一个isa指针指向它的元类(meta class)。即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。
如图:
20. 一个objc对象的isa的指针指向什么?有什么作用?
指向他的类对象,从而能够找到对象上的方法
21. 以下的代码输出什么?
1
2
3
4
5
6
7
8
9
10
11
|
@implementation Son : Father - (id)init { self = [ super init]; if (self) { NSLog(@ "%@" , NSStringFromClass([self class])); NSLog(@ "%@" , NSStringFromClass([ super class])); } return self; } @end |
答案:
都输出 Son
1
2
|
NSStringFromClass([self class]) = Son NSStringFromClass([ super class]) = Son |
解惑:
(下面解惑部分摘自微博@Chun_iOS的博文刨根问底Objective-C Runtime(1)- Self & Super)
这个题目主要是考察关于objc中对 self 和 super 的理解。
self 是类的隐藏參数,指向当前调用方法的这个类的实例。而 super 是一个 Magic Keyword, 它本质是一个编译器标示符,和 self 是指向的同一个消息接受者。
上面的样例无论调用[self class]还是[super class],接受消息的对象都是当前 Son *xxx 这个对象。而不同的是。super是告诉编译器。调用 class 这种方法时。要去父类的方法,而不是本类里的。
当使用 self 调用方法时,会从当前类的方法列表中開始找。假设没有,就从父类中再找;而当使用 super 时,则从父类的方法列表中開始找。然后调用父类的这种方法。
真的是这样吗?继续看:
使用clang重写命令:
1
|
$ clang -rewrite-objc test.m |
发现上述代码被转化为:
1
2
|
NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_0, NSStringFromClass(((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName( "class" )))); NSLog((NSString *)&__NSConstantStringImpl__var_folders_gm_0jk35cwn1d3326x0061qym280000gn_T_main_a5cecc_mi_1, NSStringFromClass(((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){ (id)self, (id)class_getSuperclass(objc_getClass( "Son" )) }, sel_registerName( "class" )))); |
从上面的代码中。我们能够发如今调用 [self class] 时,会转化成 objc_msgSend函数。看下函数定义:
1
|
id objc_msgSend(id self, SEL op, ...) |
我们把 self 做为第一个參数传递进去。
而在调用 [super class]时,会转化成 objc_msgSendSuper函数。看下函数定义:
1
|
id objc_msgSendSuper(struct objc_super * super , SEL op, ...) |
第一个參数是 objc_super 这样一个结构体。其定义例如以下:
1
2
3
4
|
struct objc_super { __unsafe_unretained id receiver; __unsafe_unretained Class super_class; }; |
结构体有两个成员。第一个成员是 receiver, 类似于上面的 objc_msgSend函数第一个參数self 。
第二个成员是记录当前类的父类是什么。
所以,当调用 [self class] 时,实际先调用的是 objc_msgSend函数。第一个參数是 Son当前的这个实例,然后在 Son 这个类里面去找 - (Class)class这种方法,没有,去父类 Father里找,也没有,最后在 NSObject类中发现这种方法。
而 - (Class)class的实现就是返回self的类别,故上述输出结果为 Son。
objc Runtime开源码对- (Class)class方法的实现:
1
2
3
|
- (Class)class { return object_getClass(self); } |
而当调用 [super class]时。会转换成objc_msgSendSuper函数。第一步先构造 objc_super 结构体,结构体第一个成员就是 self 。 第二个成员是 (id)class_getSuperclass(objc_getClass(“Son”)) , 实际该函数输出结果为 Father。 第二步是去 Father这个类里去找 - (Class)class,没有。然后去NSObject类去找。找到了。最后内部是使用 objc_msgSend(objc_super->receiver, @selector(class))去调用, 此时已经和[self class]调用同样了。故上述输出结果仍然返回 Son。
22. runtime怎样通过selector找到相应的IMP地址?(分别考虑类方法和实例方法)
每个类对象中都一个方法列表,方法列表中记录着方法的名称,方法实现,以及參数类型,事实上selector本质就是方法名称,通过这种方法名称就能够在方法列表中找到相应的方法实现.
23. 使用runtime Associate方法关联的对象,须要在主对象dealloc的时候释放么?
-
在ARC下不须要
-
在MRC中,对于使用retain或copy策略的须要
24. objc中的类方法和实例方法有什么本质差别和联系?
类方法:
-
类方法是属于类对象的
-
类方法仅仅能通过类对象调用
-
类方法中的self是类对象
-
类方法能够调用其它的类方法
-
类方法中不能訪问成员变量
-
类方法中不定直接调用对象方法
实例方法:
-
实例方法是属于实例对象的
-
实例方法仅仅能通过实例对象调用
-
实例方法中的self是实例对象
-
实例方法中能够訪问成员变量
-
实例方法中直接调用实例方法
-
实例方法中也能够调用类方法(通过类名)