我们前面文章讲了App加载的有关内容,我将文章进行了总结。OC底层原理系列,OC底层系列讲了不少,这边文章就来说说比较经典的相关面试题。
[self class]和[super class]的区别
准备代码,预测结果
我们准备下代码
@implementation LGTeacher
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@ - %@",[self class],[super class]);
}
return self;
}
@end
int main(int argc, const char * argv[]) {
@autoreleasepool {
// insert code here...
LGTeacher *teacher = [[LGTeacher alloc] init];;
NSLog(@"%@",teacher);
}
return 0;
}
我们预测的答案应该是LGTeacher和NSObject(LGTeacher的父类是NSObject)。是这样吗?我们验证下
我们发现两个答案的都是LGTeacher,并没有我们想要的NSObject。这是为什么呢?
分析,答疑
上面都是调用class,调用class,我们看下class的实现
我们看到调用class结果就是如歌obj存在就拿到obj的isa,[self class]很好理解,因为self的isa就是LGTeacher。所以[self class]就是LGTeacher,[super class]中的super是什么?其实是关键字,来自寄存器底层的指令。
我们看下[super class]在C++下是什么样呢?我们用clang去看下,我们寻找一下init方法
我们看到NSLog打印的时候发送了两个消息,分别是
((Class (*)(id, SEL))(void *)objc_msgSend)((id)self, sel_registerName("class"))
((Class (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class")))
他们相对应的就是[self class]和[super class],我们看到他们区别在于[self class]发送消息用的是objc_msgSend,而[super class]发送消息用的是objc_msgSendSuper,那么objc_msgSendSuper方法实现是什么样的呢?
我们看到objc_super是个结构体类型,receiver是消息接受者,而super_class只是一个类型,我们再看[super class]在C++的代码,objc_msgSendSuper传入的第一个参数为(id)self,所以这个消息的接收者就是self,就是LGTeacher。补充:objc_super的class和super_calss是为了快速查找(是去类对象class中查找还是直接去super_class中查找)
我们验证下,我们在class方法内打断点,运行代码,看下进来的self是否都为LGTeacher
符合期望
总结
上面的问题我们知道
- 1.super是个关键字
- 2.[super class] -> (class)(id self , sel_cmd)其中self,cmd是隐藏参数
- 3.self->isa 就是LGPerson类
- 4.[self class]的消息接受者是self,[super class]的消息接收者也是self。
指针平移
在网上看过一道面试题,准备代码
// ViewController
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc saySomething];
LGPerson *person = [LGPerson alloc];
[person saySomething];
}
@end
// LGPerson.h
@interface LGPerson : NSObject
@property (nonatomic, copy) NSString *lg_name;
@property (nonatomic, copy) NSString *lg_hobby;
- (void)saySomething;
@end
// LGPerson.m
@implementation LGPerson
- (void)saySomething {
NSLog(@"%s",__func__);
}
@end
问题回事代码运行会怎么样?我们看下代码,saySomething是对象方法,我们一般都是通过对象去调用的对象方法,而cls是类名,所以应该会报错。(__bridge id)是桥接,因为kc带*。
下面我们运行代码
我们发现没有报错,很正常的执行了方法,为什么能够调用成功?
首先我们要明白方法的调用,我们创建LGPerson对象,对象结构中bits存有saySomething方法,创建person对象,它的isa指向了LGPerson对象,通过指针平移找到saySomething方法进行调用,如下图:
同理Class cls = [LGPerson class];创建类对象,void *kc = &cls的指向到LGPerson,就跟也去查找方法进行调用。
拓展
上面我们知道为什么[(__bridge id)kc saySomething]方法能够调用成功,下面我们进行扩展,对LGPerson类的saySomething方法进行扩展,改为下面样子
@implementation LGPerson
- (void)saySomething {
NSLog(@"%s - %@",__func__, self.lg_name);
}
@end
我们打印一下self.lg_name,因为lg_name并没有赋值,猜测应该打印null。下面我们运行代码验证下:
看到上面方法打印的结果是ViewController,这是什么原因呢?下面我们就解释一下
内存平移
我们知道person对象调用saySomething方法,self.lg_name是通过指针平移8字节得到的,如下图:
我们看对kc进行打印分析
我们对kc打印,发现奇怪的东西,
kc是LGPerson类的对象,他的isa指针指向了cls,这个时候就好理解了吧!kc调用saySomething,跟对象调用一样,通过isa指针找到类,在类中找到方法调用,self.lg_name是首地址进行偏移8字节的到的值,我们偏移8字节就是0x0000000103dab670,我们进行打印,发现就是ViewController。我们发现kc虽然是LGPerson对象,但是内存里存的和我们常看到LGPerson对象不同。这是什么情况呢?
栈
我们知道方法执行的时候,都会将方法,对象入栈,我们知道参数会从前往后压入。我们下面看下结构体是怎样的一个压栈情况。看到下面方法
struct kc_struct{
NSNumber *num1;
NSNumber *num2;
} kc_struct;
- (void)viewDidLoad {
[super viewDidLoad];
struct lg_struct lgs = {@(10),@(20)};
LGPerson *person = [LGPerson alloc];
NSLog(@"%p",&person);
}
我们创建一个结构体kc_struct,然后在viewDidLoad进行初始化,我们在NSLog上打断点,看下入栈的顺序,运行代码在断点进行打印:
通过我们的打印可以知道结构体入栈是从低到高压入的。下面我们写如下代码:
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [LGPerson class];
void *kc = &cls; //
LGPerson *person = [LGPerson alloc];
[(__bridge id)kc saySomething];
[person saySomething];
}
我们先推测下栈内都有什么,我们知道每个方法都有2个隐藏参数self,_cmd。 所以调用viewDidLoad就会有self,cmd方法,紧接着调用[super viewDidLoad]就会有(id)class_getSuperclass(objc_getClass("ViewController"))和self(看上面super调用方法的c++代码),接着就会调用[LGPerson class],这个时候就会有cls以及person对象。
下面我们写代码进行验证。准备如下代码:
void *sp = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
for (long i = 0; i<count; i++) {
void *address = sp - 0x8 * i;
if ( i == 1) {
NSLog(@"%p : %s",address, *(char **)address);
}else{
NSLog(@"%p : %@",address, *(void **)address);
}
}
这几句代码的意思:拿到self的地址作为首地址,拿到person的地址作为尾地址,因为地址是连续的,都是8字节。将首地址减去尾地址在除以8得到存了多少内容。然后开始遍历,将指针地址取出来,进行打印,因为第0个是self也就是ViewController,是字符串形式。这几句代码将所有入栈信息都打印出来
我们运行代码,看下打印结果,如下图
我们上面分析的是对的。而且连续。推测:kc是个特殊的Person,它的存储是入栈信息,它的isa指向创建的Person类cls
扩展
我们对LGPerson修改代码
@interface LGPerson : NSObject
@property (nonatomic, assign) int kc_name;
@property (nonatomic, copy) NSString *kc_hobby;
- (void)saySomething;
@end
然后运行,我们发现报错了,原是因为int是4字节,所以再平移4字节是是找不到内容的,所以会报错!
总结
这两道面试题,第一个将的self跟super调用class的区别,通过cpp的代码发现接收者都为self,super为关键字,只是super会让编译期直接去父类查找,节约时间。 第二题是内存平移,以及入栈的顺序问题,也看看到kc为特殊LGPerson,类似入栈顺序(我这部分没有弄清kc到底是什么东西,如果有人知道可以说一下)。