面试题一: [self class] & [super class]
以下打印输出什么?
@interface LGTeacher : LGPerson
@end
@implementation LGTeacher
- (instancetype)init {
if (self = [super init]) {
NSLog(@"%@ - %@", [self class], [super class]);
}
return self;
}
@end
直接运行,
-
[self class]
消息接收者为self
,class的源码
- (Class)class {
return object_getClass(self);
}
Class object_getClass(id obj)
{
if (obj) return obj->getIsa();
else return Nil;
}
很简单,获取的是isa指针。
综上,
self的isa指针指向谁?-->LGTeacher
-
[super class]
,注意super是关键字
,那对应的底层代码是什么?-->clang一下
xcrun -sdk iphonesimulator clang -rewrite-objc LGTeacher.m
上图可知,
[super class] --> objc_msgSendSuper
,第一个参数是结构体__rw_objc_super
,这是一个中间结构体,源码中搜不到,我们只能再搜索objc_msgSendSuper
的定义
OBJC_EXPORT void
objc_msgSendSuper(void /* struct objc_super *super, SEL op, ... */ )
OBJC_AVAILABLE(10.0, 2.0, 9.0, 1.0, 2.0);
查看注释,objc_super
是消息接收者,
/// Specifies the superclass of an instance.
struct objc_super {
/// Specifies an instance of a class.
__unsafe_unretained _Nonnull id receiver;
/// Specifies the particular superclass of the instance to message.
#if !defined(__cplusplus) && !__OBJC2__
/* For compatibility with old objc-runtime.h header */
__unsafe_unretained _Nonnull Class class;
#else
__unsafe_unretained _Nonnull Class super_class;
#endif
/* super_class is the first class to search */
};
objc_super
有2个参数,分别是id类型的receiver变量和Class类型的父类变量。
再回到clang得到的C++代码
((void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("LGTeacher"))}, sel_registerName("class"))
那么objc_super
的receiver --> self,真正的消息接收者,第二个参数Class --> objc_getClass("LGTeacher")。
综上分析,
- objc_msgSendSuper的消息接收者还是
self
[super class]其实等价于[self class] --> LGTeacher
注意 事实真的是这样吗?
我们直接去汇编层查看[super class]
- 打开汇编调试: XCode菜单栏Debug-->Debug Workfiow-->勾上Always show Disassembly
- 只看
[super class]
- (instancetype)init{
self = [super init];
if (self) {
NSLog(@"%@",[super class]);
}
return self;
}
进入汇编层
红框处是NSLog那句代码的汇编,发现
[super class]
对应的是objc_msgSendSuper2
。
再看
objc_msgSendSuper2
源码
// objc_msgSendSuper2() takes the current search class, not its superclass.
OBJC_EXPORT id _Nullable
objc_msgSendSuper2(struct objc_super * _Nonnull super, SEL _Nonnull op, ...)
OBJC_AVAILABLE(10.6, 2.0, 9.0, 1.0, 2.0);
看注释,objc_msgSendSuper2() takes the current search class, not its superclass
,是不是很明白了!
至此,这道面试题完整答案如下:
[self class]:本质是 发送消息
objc_msgSend
,消息接收者是self,class是拿isa指针,self的isa指针指向类LGTeacher,所以是LGTeacher[super class] :super是一个关键字,本质是调用
objc_msgSendSuper2
,其消息接收者和[self class]是一模一样的,都是self
,所以返回的也是LGTeacher
面试题二:经典的 内存平移 问题
@interface LGPerson : NSObject
@property (copy, nonatomic) NSString *name;
- (void)sayHello;
@end
@implementation LGPerson
- (void)sayHello {
NSLog(@"%s - %@", __func__, self.name);
}
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc sayHello];
}
@end
以上打印输出什么?
直接运行,查看结果
-[LGPerson sayHello] -
why?我们一行一行的查看代码:
-
Class cls = [LGPerson class];
--> 很简单,cls就是类LGPerson。 -
void *p = &cls;
--> 指针p指向cls的地址,也就是类LGPerson的地址。 -
[(__bridge id)p sayHello];
--> 因为p是指针,是C++底层结构,要调用OC的方法,所以要桥接一下,即(__bridge id)。
回到问题,为什么能运行成功,并且打印的结果是ViewController?
我们先看看一个普通方法的调用过程:
LGPerson *person = [LGPerson alloc];
[person sayHello];
- [person sayHello]中,person是消息接收者,然后person的isa指针指向类LGPerson,那么person的首地址就是类LGPerson的首地址。
-
然后根据类LGPerson的首地址进行内存平移,找到缓存cache,在cache中查找方法sayHello。
再看void *p = &cls;
, p是一个指针,也指向类LGPerson的首地址
上图可知,指针p和person的isa指针一样,其实都是指向类LGPerson的首地址,所以
[(__bridge id)p sayHello] ==> [person sayHello]
,所以[(__bridge id)p sayHello]
可正常运行。
接下来看-->为什么打印的值是ViewController呢?
//下面这两种方式调用
//方式一
Class cls = [LGPerson class];
void *kc = &cls;
[(__bridge id)kc sayHello];
//方式二:常规调用
LGPerson *person = [LGPerson alloc];
[person sayHello];
运行查看结果
why?
-
[person sayHello]
的方式获取属性name的值: 本质是由于self指向person的内存结构,然后通过内存平移8字节(平移8字节是因为self的首地址是isa指针,指针占8字节大小)
,取出name,即self指针首地址平移8字节获得,但是属性name没有赋值,所以打印为null。 -
[(__bridge id)kc sayHello]
的方式:因为kc是指针,self.name就相当于kc指针平移8字节去查找name,那么kc的地址是什么?平移8字节后又是多少?
我们还是要通过clang查看ViewDidLoad对应的底层C++代码:
static void _I_ViewController_viewDidLoad(ViewController * self, SEL _cmd) {
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
Class cls = ((Class (*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("class"));
void *p = &cls;
((void (*)(id, SEL))(void *)objc_msgSend)((id)p, sel_registerName("sayHello"));
LGPerson *person = ((LGPerson *(*)(id, SEL))(void *)objc_msgSend)((id)objc_getClass("LGPerson"), sel_registerName("alloc"));
((void (*)(id, SEL))(void *)objc_msgSend)((id)person, sel_registerName("sayHello"));
}
- 首先我们知道,kc指针是局部变量,内存中是在
栈区
,栈区
是先进后出,后进先出
的原则,参数的传入就是一个不断压栈的过程,压栈的意思就是,先传进来的地址高,后传进来的地址低,栈在释放时,先释放地址低的变量(后进先出),再释放地址高的变量(先进后出)。 - 根据上面C++代码,我们看到ViewDidLoad有两个入参
(ViewController * self, SEL _cmd)
,这是隐藏参数
,就好比消息的发送是objc_msgSend,它有两个参数receiver 和 方法的sel。而方法的入参压栈的过程和先进后出,后进先出
的原则一样,所以也是从高地址->低地址分配的。 -
[super viewDidLoad];
-->
((void (*)(__rw_objc_super *, SEL))(void *)objc_msgSendSuper)((__rw_objc_super){(id)self, (id)class_getSuperclass(objc_getClass("ViewController"))}, sel_registerName("viewDidLoad"));
objc_msgSendSuper的第一个入参是结构体__rw_objc_super
,它里面也有2个参数selfreceiver
和 (id)class_getSuperclass(objc_getClass("ViewController"))SEL
,那结构体中的成员变量的地址是如何分配的呢?
结构体中成员变量的内存地址分配
举例验证:
首先定义结构体
struct kc_struct{
NSNumber *num1;
NSNumber *num2;
} kc_struct;
然后我们在ViewDidLoad中这么调用
struct kc_struct kcStruct = {@(10), @(20)};
LGPerson *person = [LGPerson alloc];
接着我们lldb查看内存栈区地址
上图我们发现,num1和num2这两个成员变量的内存地址是
从低到高
进行分配的,与栈的从高到低
相反,即结构体内部的成员是反向压栈
。
回到之前clang得出的C++源码,目前为止,栈区中所有变量地址
从高到低
的顺序是:
self - _cmd - (id)class_getSuperclass(objc_getClass("ViewController")) - self - cls - kc - person
。
-
self
和_cmd
是viewDidLoad
方法的两个隐藏参数
,是高地址->低地址正向压栈
的。 -
objc_msgSendSuper2
中的结构体成员self
和class_getSuperClass
,是低地址->高地址反向压栈
的。 - 剩下的
cls
-kc
-person
就是单纯的栈区变量,前面的高地址,后面的低地址,也是正向压栈
。
验证以上结论
self是地址最高,而person是地址最低,我们直接取它们的地址,进行一个for循环,看看
Class cls = [LGPerson class];
void *kc = &cls;
LGPerson *person = [LGPerson alloc];
NSLog(@"%p - %p",&person,kc);
// 隐藏参数 会压入栈帧
void *sp = (void *)&self;
void *end = (void *)&person;
long count = (sp - end) / 0x8;
for (long i = 0; i
[(__bridge id)kc sayHello]的值是ViewController的原因
根据以上我们的分析,知道了kc指针在底层其实和person的性质是一样的,都是指向类LGPerson的首地址,[(__bridge id)kc sayHello]
中调用self.name
,kc是
,是一个实例对象,那么此时的操作与普通的LGPerson对象是一致的,其中调用self.name
, 即LGPerson的首地址内存平移8字节
--> 0x7ffeeccfc0d8 + 0x80 = 0x7ffeeccfc0e0
,看上图,0x7ffeeccfc0e0
对应的就是
。
小结
由于kc是指向LGPerson的关系,编译器会认为 kc也是LGPerson的一个实例化对象,即kc相当于isa指向LGPerson,即类LGPerson首地址,具有和person一样的效果,简单来说,我们已经完全将编译器骗过了,即kc也有属性name,然后由于person查找name是通过内存平移8字节,所以kc也是通过内存平移8字节,正好是objc_msgSendSuper2的入参self,而objc_msgSendSuper2是在当前类中查找的,所以内存平移后的值就是类ViewController。