前言:
我们直接用面试题的方式来回答和解读oc语法的底层原理。看下面的面试题:
有关oc面相对象的:
1,面向对象编程OOP是什么意思?谈谈你对OOP的理解。
2,一个nsobject占用多少内存空间?
3,对象的isa指针和superclass指向哪里?
4,oc对象的类信息存放在哪?
5,oc对象的本质是什么?
6,谈谈你对isa指针的了解?
KVO有关的:
1,oc是如何实现对一个对象的KVO?
2,如何手动触发KVO?
3,直接修改成员变量会触发KVO吗?
KVC相关的:
1,KVC的实现原理是怎样的?
2,通过KVC修改属性会触发KVO吗?
Category分类相关的:
1,Caregory是什么?什么场合使用?
2,Caregory的实现原理?
3,Catefory和Class Extension的区别是什么?
4,Category中有load方法吗?load方法和intialize方法的区别是什么?
关联对象相关的 :
1,关联对象常用的api有哪些?
2,关联对象的底层是怎么实现的?
Block相关(写了另一篇来讲了)
链接:https://www.jianshu.com/p/eed13c33c8bc
答案和详解
一, OC对象的本质
面试题1:面向对象编程OOP是什么意思?谈谈你对OOP的理解。
面试题2: 一个nsobject对象占用多少内存空间?
答: 系统给nsobject分配了16个字节的空间(可以通过#import<malloc/malloc.h>框架下的malloc_size()方法来获取);
但实际上在64bit环境下nsobject对象内部只使用了8字节的空间(可以通过#import<objc/runtime.h>的class_getInstanceSize()方法来获取).
以上是一个没有属性和成员变量的对象所占用的字节, 通过上面两个方法, 我们还可以发现, 对于拥有多个成员变量和属性的对象, 系统给其分配的内存空间是以16的倍数增长的, 而实际内部占用的空间则不一定是16的倍数.
面试题3: oc对象的分类有几种? isa指针是怎样的?
答: 三种, 分别是:
instance对象(实例对象);
class对象(类对象);
meta-class对象(元类对象).
下面是他们的isa指针指向和superclass指针的指向。其中需要注意的是:基类元对象的isa指针指向自己;基类元对象的superclass指针指向基类的class对象,而基类对象的superclass指针指向nil。
isa指向.png
面试题4:oc对象的本质是什么?
OC对象的本质是一个结构体。通过将OC的文件编译成cpp文件之后可以看到,一个OC的类编译之后就是一个cpp的结构体,结构是这样的:
类的本质.png
对于自定义的类,例如GQStudent类,编译后的结构体跟上面是一样的,只是编译后的结构体的名称一般都是student_IMPL{}.
面试题5:oc对象的类信息存放在哪?
根据上面的两个题,那么这个问题就很好回答了。类对象的信息是存放在objc_class结构体的bits结构体中;并且通过FAST_DATA_MASK掩码可以得到bits结构体地址,在bits中的ro结构体里面存放着oc对象的类信息。ro包括的信息有:instanceSize表示对象占用内存空间大小,name类名,ivars成员变量列表等等。
面试题6:谈谈你对isa指针的了解?
在64bits之前,isa指针指示单纯的指针,里面存放着所指向的对象地址。在64bits之后,isa指针被优化成tagged pointer,里面不仅能存放地址,也能存放相对较小的number string等对象,而不需要为这样的对象开辟新的内存地址,用来减小内存开销和调用效率。并且在64bit之后,需要通过ISA_MASK掩码才能获得真正的isa地址
二, KVO有关的
面试题1: oc是如何实现对一个对象的KVO?
当我们对一个instance对象进行KVO的时候,runtime的API会动态生成一个全新的中间对象,当修改instance中的属性时,会调用Fundation的NSSet**ValueAndNotify{
[self willChangeValueForkey:@“”];
[原来的instance的setter方法实现];
[self didChageValueForKey:@“”];
},并且会触发内部的observe监听器的监听方法(observeValueForKeyPath:ofObject:change:context:)以实现对对象的监听。
以MJPerson为例,如图所示:
原来的MJPerson的instance对象和class对象之间新建一个中间对象NSKVONotifying_MJPerson对象,并且把instance的isa指向NSKVONotifying_MJPerson,把NSKVONotifying_MJPerson的superclass指针指向原来的class对象。NSKVONotifying_MJPerson中对setter方法进行了重写,重写的原理是在原来的setter方法赋值之前调用[self willChangeValueForKey:@""]和赋值之后调用[self didChangeValueForKey:@""]. 并且重写后的方法会触发NSFundation_NSSetValueAndNotify方法发出通知。所以这是实现的原理。
面试题 2:如何手动触发KVO?
手动的调用willChangeValueForKey:或者didChangeValueForKey:方法都能触发KVO。
面试题 3:直接接修改成员变量会触发KVO吗?
不会,因为不经过上面1说的那个过程。
三, KVC相关的:
面试题 1:KVC的实现原理是怎样的?
KVC全程是Key_Value Coding也就是俗称的“键值编码”,可以通过key来访问某个属性。常见的KVC的API接口有:
kvc接口.png
当我们调用setValue:forKey:方法时,会先按照次序调用setKey:和_setKey:方法修改属性值,而如果调用不成功则会调用accessInstanceVariableDirectly:方法,如果返回YES则按照次序调用_key:和 _isKey: 和 key:和 _isKey:方法获取属性并直接赋值;而如果accessInstanceVariableDirectly:返回YES后调用了前面四个方法之后没找到属性值,或者accessInstanceVariableDirectly返回了NO,则调用setValue:forUndefineKey:方法并抛出异常NSunknownKeyException。下图是调用的过程图:
kvc赋值.png
当我们调用valueForKey:方法时候,过程也是类似的。会首先依次调用getKey:→key:→isKey:→_key方法,找到了则直接调用获取属性;找不到则会调用accessInstanceVariablesDirectly如果返回了YES,则依次调用_key:→ _isKey:→ key:→ isKey:方法找到了则直接调用取值。如果上面找不到或者accessInstanceVariablesDirectly直接返回NO,则调用setValue:forUndefineKey:方法并抛出异常NSunknownKeyException。下图是调用的过程图:
kvc取值.png
面试题 2:通过KVC修改属性会触发KVO吗?
会。因为KVO调用过程中会调用setter方法,进而触发KVO。
四,Category(也叫分类)相关的:
面试题 1:Caregory是什么?什么场合使用?
category也叫分类,是runtime支持实现的对类进行动态添加拓展, 是oc语法的一种。
分类使用的场合主要有:
1,降低单个类的体积,降低耦合性,可以多人开发;
2,可以对系统的类进行拓展;
3,可以模拟多继承;
4,可以用来对静态库方法进行公开。
面试题 2:Caregory的实现原理?
category在编译后是一个结构体category_t,里面存放着分类的类方法、方法、协议信息等,但是这个结构体里并没有属性列表,所以不能往分类里添加属性。在程序运行时候,runtime会将一个类的分类动态的合并到类信息里面(包括类对象和元类对象)。并且后编译的category会排在class的methods列表前面,所以当一个类有多个category时,后面编译的category的类方法和方法等会优先被调用。但这不是覆盖,而是优先调用。
面试题 3:Catefory和Class Extension的区别是什么?
category和class extension是两个不同的概念,都是oc语法中的一种,只是中文叫法有点类似。
它们的区别主要有:
1,category一般只能用来对类进行方法的扩展,而不能添加成员变量属性变量;而class extension可以添加属性和方法并且会自动生成setter和getter方法。
2,category中的方法是运行时合并到原来的类信息之中,而class extension中的属性和方法等是编译时候就添加到原来的类信息之中了。所以当category中声明的方法没有实现时候,编译时候也不会被提醒,而class extension就会报警。
3,class extension没有独立的.m文件,是需要在原来的类的.m文件中进行实现的,也就是需要有原来的类代码,需要依托源代码实现。而category有独立的.m文件,可以在.m中实现方法。
面试题 4:Category中有load方法吗?load方法什么时候调用?load方法可以被继承吗?load方法和intialize方法的区别是什么?
category中是有load方法的。+load方法是在runtime加载类和分类的时候调用,并且在整个程序运行过程中知知调用一次。load方法可以被继承但实际开发中我们都最好不要主动调用laod方法,让系统自动调用。而且调用的次序分别是:
先调用类的+load方法(其中,是按照编译顺序来调用的,先编译先调用,而且调用子类的load方法之前会先调用父类的load);
然后再调用分类的laod(其中,分类是按照编译顺序来调用的,先编译的会先被调用)。
而且由上面的顺序可以看出,load方法是通过方法地址直接调用的,而不是通过objc_msgSend方法来调用。
intialize方法是在类第一次接收到信息时候调用。并且是先调用父类的intialize方法再调用子类的intialize方法(其实就是:先初始化父类在初始化子类,并且每个类只会初始化一次)。
intialize方法跟load方法最大的区别是调用不同,intialize方法是通过消息objc_msgSend方法调用的,而load方法是通过内存地址直接调用的。所以他们有以下各自的特点:
当子类的intialize方法没有实现的时候,就会调用父类的intialize方法(所以父类的intialize方法有可能会被多次调用);
如果分类中实现了intialize方法,则会覆盖父类的intialize方法的调用。
面试题 5:如何给Category添加属性?
category的底层结构限制了无法给分类添加属性。但是可以通过关联对象来间接地实现给分类添加属性。
五,关联对象(associatedObject)相关的:
面试题 1:关联对象常用的api有哪些?
常用的有三个接口:
image.png
其中,对于key,除了可以使用属性名来作为key以外,还可以是使用方法签名来作为key这样可以较好的保证其唯一性。例如可以像下面这样设置key:
image.png
然后,关于objc_AssociationPolicy可以参考下面的表格:
image.png
面试题 1:关联对象的实现原理?
关联对象技术的核心对象有以下四个:
AssosiationManager;
AssosiationHashMap;
ObjectAssosiationMap;
ObjectAssosiation.
他们的之间的关系是这样的:
image.png
原理是这样的:关联对象并不是存储在被关联对象本身的内存中,而是存储在一个由runtime来保持的全局的统一的AssosiationManager对象中;这个对象持有一个AssosiationHashMap,在AssosiationHashMap里面存储着被关联对象的key以及被关联的属性ObjectAssosiationMap,而ObjectAssosiationMap中就存储着要关联的属性。
所以,当我们往一个对象里添加关联对象的时候,AssosiationManager就会往自己的AssosiationHashMap中添加一个键值对,这个键值对里就存着需要添加的属性信息。当我们要移除关联对象时候,除了可以调用移除方法以外,还可以通过设置nil来移除。
网友评论