本人参考GitHub《招聘一个靠谱的iOS》面试题参考答案(上)
21. 一个objc对象如何进行内存布局(考虑有父类的情况)?内存中的区域是怎么划分的
22. 一个objc对象的isa的指针指向什么?有什么作用?
23. 下面的代码输出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
24. runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)
25. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
21. 一个OBJC对象如何进行内存布局(考虑有父类的情况)?内存中的区域是怎么划分的?
1. 一个OBJC对象如何进行内存布局(考虑有父类的情况)
(1) 所有父类的成员变量和自己的成员变量都会存放在该对象所对应的存储空间中。
(2) 每一个对象内部都有一个isa指针,指向该对象的类对象,类对象中存放着此对象的:对象方法列表,成员变量列表和协议列表。
类对象内部有一个isa指针指向元对象(meta class),元对象内部存放的是类方法列表,类对象内部还有一个superclass指针,指向它的父类对象。
每个Objective-C对象都有相同的结构,其如下图所示:
翻译过来就是:
Objective-C对象结构图
根对象就是NSObject,它的superclass指针指向nil。类对象也是一个对象,其中也有一个isa指针指向该类的元类(meta class),即类对象是元类的实例。元类内部存放的是类方法列表,根元类的isa指针指向自己,superclass指针指向NSObject类。NSObject的superclass指针指向nil,isa指针指向根元类。
2. 内存中的区域是怎么划分的?
内存中的区域划分成:栈区(stack)、堆(heap)、静态存储区、常量区和代码区五类。
(1)栈区(stack):系统自动分配和释放,存放局部变量的值,容量小速度快,有序。
(2)堆(heap):由程序员分配和释放,若不释放则导致内存泄漏,程序自动回收,容量大,速度慢,无序。
(3)静态存储区:存放全局变量和静态变量,程序结束后,系统回收。
(4)常量区:存放常量的内存区域,程序结束时,系统回收。
(5)代码区:存放二进制代码的区域。
22. 一个Objective-C对象的isa指针指向什么?有声明作用?
Objective-C对象的isa指针指向其类对象,从而可以找到类对象中保存的对象方法。
23. 下面的代码输出什么?
@implementation Son : Father
- (id)init
{
self = [super init];
if (self) {
NSLog(@"%@", NSStringFromClass([self class]));
NSLog(@"%@", NSStringFromClass([super class]));
}
return self;
}
@end
答案:
都输出Son
NSStringFromClass([self class]) = Son
NSStringFromClass([super class]) = Son
这个题目主要是考察关于Objective-C中对self和super的理解。
self是类的隐藏参数,指向当前调用方法的这个类的实例。很多人会误认为“super和self类似,应该是指向父类的指针”,这是一个很普遍的误区。其实super是一个Magic Keyword,它本质上是一个编译器标示符,和self指向的是同一个消息接收者,他们的区别在于:super会告诉编译器,调用某个方法时,去父类的方法列表中找,但实际调用者仍然是self。
上述的例子,不管调用[self class]还是[super class],接收消息的对象都是当前的Son *xxx;
这个对象。
self和super的区别:
- 当使用self调用方法时,会从当前累的方法列表中开始找,如果没有,就从父类的方法列表里再找;而当使用super时,则从父类的方法列表开始找,然后调用父类的这个方法(调用者仍为self)。
- 当使用self调用时,会使用
objc_msgSend(id theReceiver, SEL theSelector,…);
第一个参数是消息接收者,第二个参数是调用的具体方法,后面是selector方法的可变参数。以[self class];
为例,编译器会替换成调用objc_msgSend()
,其中receiver是self,theSelector是@selector(class),这个selector是从当前的self的类的方法列表开始找的class,找到后把对应的selector传递过去。 - 当使用super调用时,会使用
objc_msgSendSuper(struct objc_super *super, SEL theSelector,…)
函数,第一个参数是objc_super的结构体,第二个方法是类方法列表中的selector。
struct objc_super{
id receiver;
Class superClass;
};
当编译器遇到[super class];
时,开始做以下两件事:
(1)构建objc_super的结构体,此时这个结构体的第一个成员变量receiver就是子类即self,第二个成员变量superClass是指父类。
(2)函数从objc_super结构体指向的superClass的方法列表开始找class方法的selector,找到以后再用objc_super->receiver去调这个selector。
这是为什么“不推荐在init方法中使用点语法”,如果想访问实例变量iVar应该使用下划线(_iVar),而不是点语法self.ivar。
点语法(self.iVar)的坏处就是子类可能覆写setter。假设在子类中调用了[super init];
方法,又在父类的init方法中调用了self.iVar = xxx
,此时就会调用子类的setIVar:
方法(此时self指子类对象)对iVar进行初始化,父类对iVar的初始化就可能会失效。
24. runtime如何通过selector找到对应的IMP地址?(分别考虑类方法和实例方法)
Objective-C类对象本质是一个结构体,其中有一个指针指向方法列表,该列表中保存着该类的实例方法(对象方法),每个实例方法也是一个结构体。
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 OBJC_UNAVAILABLE; // 方法缓存,对象接到一个消息会根据isa指针查找消息对象,这时会在method Lists中遍历,如果cache了,常用的方法调用时就能够提高调用的效率
struct objc_protocol_list *portocols OBJC2_UNAVAILABLE;// 协议链表
#endif
} OBJC2_UNAVAILABLE;
struct objc_method
{
SEL method_name OBJC2_UNAVAILABLE; //方法名
char *method_types OBJC2_UNAVAILABLE; // 方法类型
IMP method_imp OBJC2_UNAVAILABLE; // 方法实现
};
selector本质就是方法名称,runtime通过这个方法名称就能在方法列表中找到对应的实现(IMP)。
25. 使用runtime Associate方法关联的对象,需要在主对象dealloc的时候释放么?
在ARC和MRC下都不需要。
网友评论