前言
话说 iOS 面试怎么离得开 Objective-C 这苹果公司使用的划时代的语言呢?我贴出维基百科的一段objC的介绍吧。
Objective-C最大的特色是承自Smalltalk的讯息传递模型(message passing),此机制与今日C++式之主流风格差异甚大。Objective-C里,与其说物件互相呼叫方法,不如说物件之间互相传递讯息更为精确。此二种风格的主要差异在于呼叫方法/讯息传递这个动作。C++里类别与方法的关系严格清楚,一个方法必定属于一个类别,而且在编译时(compile time)就已经紧密绑定,不可能呼叫一个不存在类别里的方法。但在Objective-C,类别与讯息的关系比较松散,呼叫方法视为对物件发送讯息,所有方法都被视为对讯息的回应。所有讯息处理直到执行时(runtime)才会动态决定,并交由类别自行决定如何处理收到的讯息。也就是说,一个类别不保证一定会回应收到的讯息,如果类别收到了一个无法处理的讯息,程式只会抛出异常,不会出错或崩溃。
链接 https://zh.wikipedia.org/zh-hans/Objective-C
博客的目的是为了面试嘛,所以下面咱们看看Objective-C (下文简写为OC)到底如何面试,我们该掌握啥?看看下面这张图,OC 面试主要就是围绕着分类、关联对象、拓展、代理、通知、KVO、KVC以及属性关键字这几个方面,其中需要重点掌握分类实现的原理,关联对象的底层实现机制、通知以及 KVO、KVC的底层实现原理和属性关键字的作用,拓展和代理简单的看看即可。
分类
1. 分类结构体
struct_category_t{
//类名
constchar *name;
//分类所属的类
struct_class_t *cls;
//category中所有给类添加的实例方法的列表(instanceMethods)
conststruct_method_list_t *instance_methods;
//category中所有添加的类方法的列表(classMethods)
conststruct_method_list_t *class_methods;
//category实现的所有协议的列表(protocols)
conststruct_protocol_list_t*protocols;
//category中添加的所有属性(instanceProperties)
conststruct_prop_list_t*properties;
};
由上可得,分类在编译过程中,会生成 类方法列表、实例方法列表、属性列表 等,但是却 没有 实例变量列表(_ivar_list_t),可对比分类所属类的编译结果看,分类所属类是存在实例变量列表的。然后,再来对比 实例方法列表 ,还能发现分类的实例方法列表中,并未对分类属性生成 getter/setter 方法。
2. 分类加载调用栈
//runtime的初始化函数,进行初始化操作,注册了镜像状态改变时的回调函数
_objc_init
//加锁并调用 map_images_nolock
└── map_2_images
//完成所有 class 的注册、fixup等工作,还有初始化自动释放池、初始化 side table 等工作并在函数后端调用了
└── map_images_nolock _read_images
//加载类、Protocol、Category,加载分类的代码就写在 _read_images 函数的尾部
└── _read_images
_objc_init 函数在 objc-os.mm 中,_read_images 方法在objc-runtime-new.mm 中。
加载过程
- 把分类的
实例方法
、属性
、协议
添加到类的实例对象中原本存储的实例方法
、属性
、协议列表
的 前面 ; - 把分类的
类方法
和协议
添加到类的元类
上。
这么做的目的就是保证分类方法 优先调用,注意,不是覆盖,而是共同存在在实例方法列表中,只是分类在前而已。
3. 你用分类干了啥?
- 声明私有方法
- 分解体积庞大的类文件
- 把 Framework 的私有方法公开化
4. 分类和拓展的区别?
1. 分类
- 运行时决议
- 可以为系统类添加分类
- 添加实例方法
- 类方法
- 协议
- 属性 (只声明了 get 和 set 方法,但是却没有添加实例变量)
2. 拓展 - 编译时决议
- 只以声明的形式存在
- 不能为系统类添加拓展
- 声明私有属性
- 声明私有方法
- 声明私有成员变量
- 分类可以有声明,可以有实现
关联对象以及关联对象底层实现机制
1. 为分类添加成员变量代码实现
-(void)setName:(NSString *)name {
// 设置 value,通过 key 和 value 建立映射,通过Policy策略将映射关系关联到设置的对象上
objc_setAssociatedObject(self, @"name",name, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)name {
// 根据指定的 key,到 obj 中找到与当前 key 相对应的关联值
return objc_getAssociatedObject(self, @"name");
}
问题 1:使用关联对象为分类添加成员变量,这个成员变量添加到了哪里呢?
为分类添加的实例变量是没有添加到原宿主类中,而分类的结构体中也没有接收成员变量的列表,所以也是没有添加到分类中。关联对象是由AssociationsManager
管理并在 AssociationsHashMap
中存储,所有类的关联内容都在同一个全局容器中
关联对象的本质
其中
ObjcAssociation
中有两个成员结构,一个是成员变量的关联策略 Policy,一个是我们要关联对象的值(Value);然后进行进一步的封装,数据结构是 ObjectAssociationMap
,ObjectAssociationMap
中有成员变量Value
就是ObjcAssociation
,key 往往就是分类中的某一个属性或者选择器的方法名 @selector(text),从而让ObjcAssociation和 key 建立映射;最后我们将ObjectAssociationMap
放到上面提到的AssociationsHashMap
全局容器中,主要是根据当前对象被关联的指针值DISGUISE(obj)
建立和ObjectAssociationMap
的映射,从而实现将一个值关联到某一个对象上。
源码解析
我们看到其实内部调用的是_object_set_associative_reference函数,我们来到_object_set_associative_reference函数中
_object_set_associative_reference函数内部我们可以全部找到我们上面说过的实现关联对象技术的核心对象。接下来我们来一个一个看其内部实现原理探寻他们之间的关系。
AssociationsManager 就是关联对象的管理类,是C++实现的一个类
AssociationsHashMap 关联对象管理类维护的一个 hashmap,可以理解为一个字典,是一个全局的容器。上述代码大概意思是:
- 第一步是根据策略之对 value 进行加工
acquireValue
,其实就是 copy、retain 操作 - 接着通过 DISGUISE 设置关联的对象在全局容器中的 key 或者说是唯一标志
- 判断 new_value,传递进来的准备被关联的值,如果该值存在,那么根据对象指针查找AssociationsHashMap的 Map,如果是第一次进行关联值的设置,那么该对象在全局容器中为空,那么我们需要重新创建并且关联到全局容器中,如果已经创建过,那么判断其值是否为空,如果为空那么将新值关联进去,如果为空那么就新值添加进去并且关联一个 key
-
判断 new_value,传递进来的准备被关联的值不存在,那么会通过AssociationsHashMap去查找,如果我们设置value为nil时,就会将关联对象从ObjectAssociationMap中移除
参考了下面这个博客,建议大家看这个,我感觉我写的有点儿迷
https://www.jianshu.com/p/0f9b990e8b0a
代理
- 代理其实就是一种软件设计模式
- 代理是一对一
- 在 ios 中,代理以@protocol 存在
- 代理在声明的时候一般是以 weak 声明,避免循环引用
通知(NSNotification)
通知是使用
1. 如何实现通知机制?(NS开头的代码未开源)
在通知中心维护一个 NotificationMap,然后在 NotificationMap 中有一个key通知名NotificationName 和一个value观察者的列表Observers_list,因为通知是一对多,所以这里用列表来管理观察者,列中的观察者接收到通知也会有对应的执行方法,这样就简单的设计出通知的实现机制了,如下图2. 通知是同步的还是异步的?
具体情况具体分析,一般我们所使用的 NSNOtification 都是同步的,就是说 post 消息发出时,接收者接收且执行了相关的动作时,才会进行后续操作,如果涉及到NSNOtificationQueue时,则是异步的
3. 通知和代理的区别?
- 设计模式的区别
- 传递方式的区别(通知一对多,代理一对一)
KVO
- 系统对观察者设计模式的一种实现
- Apple 使用 isa (isa-swizzling)混写技术实现 KVO
1. KVO的实现原理
如下图所示
NSKVONotification_A
,并且将 A的指针指向NSKVONotification_A,将 isa pointer进行修改,实际上就是 isa 混写技术的一个标志,NSKVONotification_A是 A 的一个子类,然后由NSKVONotification_A重写 setter 方法并且通知观察者 A 的变化,从而实现 KVO
2. 为什么通过 KVC 设置 value 也可以使 KVO 生效呢?
- [setValeue:forKey] 最终会走value的setter方法
通过成员变量直接赋值是否能使 KVO 生效呢?手动 KVO 是什么样的呢?
- 使用 setter 方法改变值才会触发 KVO
- 使用 [setValue:forKey]改变值也可以触发 KVO
- 成员变量直接修改需要手动调用 KVO 才会生效,赋值前调用willChangeValueForkey,赋值结束后调用didChangeValueForKey
KVC
- key-value coding 的缩写 键值编码
KVC 主要涉及的方法就是下面这些 - value:Forkey
- setValue:ForKey
kvc这种键值编码技术是会破坏面向对象编程思想,因为我们知道某一个类的私有成员变量名的时候,可以通过kvc 技术直接在外部修改其成员变量的值
valueForKey 系统调用流程
系统通过 Key 去访问当前实例变量是否存在 getter 方法,如果存在则调用,然后结束,如果不存在,则先判断实例变量是否存在,如果实例变量存在,直接获取实例变量的值,结束调用,如果不存在,系统就会调用 [valueForUndinedKey:]最后抛出异常
Accessor Method
访问器方法是否存在的判断规则
依次匹配这些样式:_
setValue:ForKey 系统调用流程
如下图,同 valueForKey
属性关键字
属性关键字分类
- 读写权限 readonly readwrite
- 原子性 atomic(系统默认) ,(非原子性)noatomic
- 引用计数
atomic(系统默认) noatomic 的区别
- atomic是线程安全的,但它运行效率慢,noatomic不是线程安全的, 但它效率高。这个设计其实就是上帝为了关了一扇门却又为你开了一扇窗。
- atomic的seter getter内部实现是用了互斥锁来保证seter getter方法在多线程中的安全,但atomic修饰的对象是我们自定义的,可能并没有加锁,在多线程中atomic修饰对象并不能保证线程安全。
- nonatomic对象setter和getter方法的实现并么有加互斥锁,所以nonatomic修饰的对象是非线程安全的,同时nonatomic对象setter和getter方法也是非线程安全的,但也正因为没有互斥锁所以性能要比atomic好
- 如果既要atomic修饰的对象线程安全也要atomic的seter getter方法具有原子性,那就需要给atomic修饰的对象加互斥锁,但这会进一步降低atomic的性能。
引用计数
- retain/strong retain一般是在 mrc 中使用,strong 是在 arc
- assign修饰基本数据类型和对象类型、unsafe_unretained
关键字 assign 和 weak 的区别
assign
- 修饰基本数据类型,如 int,bool 等
- 修饰对象时,其引用计数不改变
- 会产生悬垂指针(assign 对象被释放后,其指针仍然指向原对象地址)
weak
- 不改变被修饰对象的引用计数
- 所修饰对象在释放后会自动置为 nil
weak修饰的对象为什么在被废弃后会被置为nil ?(内存管理)
copy
当一个可变数组对象在声明时用 copy 修饰,那么在使用该数组对象时,会发生什么?为什么?
- 如果赋值过来的是可变对象,copy 之后是不可变对象(深拷贝)
- 如果赋值过来的对象是不可变对象,那么 copy 以后是一个不可变对象,是一个浅拷贝
浅拷贝
浅拷贝是对内存地址的复制,让目标对象指针和源对象指针指向同一片内存空间
深拷贝
深拷贝让目标对象指针和源对象指针指向两块内容相同的内存空间
深拷贝和浅拷贝的区别?
- 是否开辟了新的内存空间,深拷贝开辟了新的内存空间,浅拷贝内存空间不变
-
是否影响了引用计数,深拷贝不改变当前对象的引用计数,而浅拷贝是会改变
copy 总结
- 可变对象的 copy/mutableCopy 都是深拷贝
- 不可变对象的 copy 是浅拷贝,mutableCopy是深拷贝
- copy方法返回的对象都是不可变对象
OC 笔试题
MRC 下如何重写 retain 修饰变量的 setter 方法?
- (void)setObj:(id)obj {
// 为啥这里要做一个判断?
// 如果不做判断,就会有这样的问题,当传递进来的对象恰好是原来的对象时,那么我们会对原来的对象进行 release 操作,有可能我们传进来的 obj 对象被我们提前释放了,那么我们的 obj 对象就已经是 nil 了,如果对一个 nil 对象再次 retain,则会造成程序的 crash,所以这里要加一个判断
if (_obj != obj) {
[_obj release];
_obj = [obj retain];
}
}
请简述分类的实现原理?
分类是在运行时决议,不同分类当中,含有同名的方法,谁最终生效,取决于谁最后参与编译,最后参与的分类中的方法最终生效;如果分类中含有和宿主类同名的方法,那么最终宿主类方法会被分类方法覆盖,是由于消息在传递的过程中,会优先查找数组靠前的元素,如果找到同名方法就会被调用